merge develop and fix conflicts.

This commit is contained in:
piuvas 2025-06-03 10:56:10 -03:00
commit 1120ad19ae
No known key found for this signature in database
166 changed files with 2933 additions and 1079 deletions

View file

@ -23,10 +23,10 @@ import type MkNote from '@/components/MkNote.vue';
import type SkNote from '@/components/SkNote.vue';
import { prefer } from '@/preferences';
const XNote = computed(() =>
prefer.r.noteDesign.value === 'misskey'
? defineAsyncComponent(() => import('@/components/MkNote.vue'))
: defineAsyncComponent(() => import('@/components/SkNote.vue')),
const XNote = defineAsyncComponent(() =>
prefer.s.noteDesign === 'misskey'
? import('@/components/MkNote.vue')
: import('@/components/SkNote.vue')
);
const rootEl = useTemplateRef<ComponentExposed<typeof MkNote | typeof SkNote>>('rootEl');

View file

@ -20,10 +20,10 @@ import type MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import type SkNoteDetailed from '@/components/SkNoteDetailed.vue';
import { prefer } from '@/preferences';
const XNoteDetailed = computed(() =>
prefer.r.noteDesign.value === 'misskey'
? defineAsyncComponent(() => import('@/components/MkNoteDetailed.vue'))
: defineAsyncComponent(() => import('@/components/SkNoteDetailed.vue')),
const XNoteDetailed = defineAsyncComponent(() =>
prefer.s.noteDesign === 'misskey'
? import('@/components/MkNoteDetailed.vue')
: import('@/components/SkNoteDetailed.vue'),
);
const rootEl = useTemplateRef<ComponentExposed<typeof MkNoteDetailed | typeof SkNoteDetailed>>('rootEl');

View file

@ -21,10 +21,10 @@ import type MkNoteSimple from '@/components/MkNoteSimple.vue';
import type SkNoteSimple from '@/components/SkNoteSimple.vue';
import { prefer } from '@/preferences';
const XNoteSimple = computed(() =>
prefer.r.noteDesign.value === 'misskey'
? defineAsyncComponent(() => import('@/components/MkNoteSimple.vue'))
: defineAsyncComponent(() => import('@/components/SkNoteSimple.vue')),
const XNoteSimple = defineAsyncComponent(() =>
prefer.s.noteDesign === 'misskey'
? import('@/components/MkNoteSimple.vue')
: import('@/components/SkNoteSimple.vue'),
);
const rootEl = useTemplateRef<ComponentExposed<typeof MkNoteSimple | typeof SkNoteSimple>>('rootEl');

View file

@ -16,6 +16,7 @@ import { instance } from '@/instance.js';
import { prefer } from '@/preferences.js';
import { getDateText } from '@/utility/timeline-date-separate.js';
import { $i } from '@/i.js';
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
export default defineComponent({
props: {
@ -146,14 +147,12 @@ export default defineComponent({
[$style['direction-up']]: props.direction === 'up',
};
return () => prefer.s.animation ? h(TransitionGroup, {
return () => h(SkTransitionGroup, {
class: classes,
name: 'list',
tag: 'div',
onBeforeLeave,
onLeaveCancelled,
}, { default: renderChildren }) : h('div', {
class: classes,
}, { default: renderChildren });
},
});

View file

@ -53,6 +53,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { instance } from '@/instance.js';
import { prefer } from '@/preferences.js';
const emit = defineEmits<{
(ev: 'closed'): void;
@ -66,6 +67,7 @@ function close() {
}
function neverShow() {
prefer.commit('neverShowDonationInfo', 'true');
miLocalStorage.setItem('neverShowDonationInfo', 'true');
close();
}

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
<MkStickyContainer>
<MkStickyContainer :sticky="sticky">
<template #header>
<button :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<KeepAlive>
<div v-show="opened">
<MkStickyContainer>
<MkStickyContainer :sticky="sticky">
<template #header>
<div v-if="$slots.header" :class="$style.inBodyHeader">
<slot name="header"></slot>
@ -73,12 +73,14 @@ const props = withDefaults(defineProps<{
withSpacer?: boolean;
spacerMin?: number;
spacerMax?: number;
sticky?: boolean;
}>(), {
defaultOpen: false,
maxHeight: null,
withSpacer: true,
spacerMin: 14,
spacerMax: 22,
sticky: true,
});
const rootEl = useTemplateRef('rootEl');

View file

@ -154,13 +154,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.footerButton" @mousedown.prevent="translate()">
<button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.footerButton" :disabled="translating || !!translation" @click.stop="translate()">
<i class="ti ti-language-hiragana"></i>
</button>
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
<button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@ -226,7 +226,7 @@ import { getNoteSummary } from '@/utility/get-note-summary.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
import { instance, isEnabledUrlPreview } from '@/instance.js';
import { instance, isEnabledUrlPreview, policies } from '@/instance.js';
import { focusPrev, focusNext } from '@/utility/focus.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
@ -360,7 +360,7 @@ const keymap = {
clip();
},
't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) {
if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) {
translate();
}
},

View file

@ -166,13 +166,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @click.stop="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="translate()">
<button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()">
<i class="ti ti-language-hiragana"></i>
</button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@ -278,7 +278,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
import { instance, isEnabledUrlPreview } from '@/instance.js';
import { instance, isEnabledUrlPreview, policies } from '@/instance.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
@ -388,7 +388,7 @@ const keymap = {
clip();
},
't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) {
if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) {
translate();
}
},

View file

@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
@click.stop="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
@ -42,24 +42,30 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton"
class="_button"
:class="$style.noteFooterButton"
@mousedown="quote()"
@click.stop="quote()"
>
<i class="ph-quotes ph-bold ph-lg"></i>
</button>
<button v-else class="_button" :class="$style.noteFooterButton" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i>
</button>
<button v-if="note.myReaction == null && note.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
<button v-if="note.myReaction == null && note.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @click.stop="like()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="note.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
<button v-if="note.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @click.stop="react()">
<i v-if="note.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
</button>
<button v-if="note.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(note)">
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.noteFooterButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()">
<i class="ti ti-language-hiragana"></i>
</button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()">
<i class="ph-dots-three ph-bold ph-lg"></i>
</button>
</footer>
@ -78,10 +84,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef, watch } from 'vue';
import { computed, inject, ref, shallowRef, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import * as config from '@@/js/config.js';
import type { Ref } from 'vue';
import type { Visibility } from '@/utility/boost-quote.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
@ -101,11 +108,12 @@ import { showMovedDialog } from '@/utility/show-moved-dialog.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { reactionPicker } from '@/utility/reaction-picker.js';
import { claimAchievement } from '@/utility/achievements.js';
import { getNoteMenu } from '@/utility/get-note-menu.js';
import { getNoteClipMenu, getNoteMenu, translateNote } from '@/utility/get-note-menu.js';
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
import { prefer } from '@/preferences.js';
import { useNoteCapture } from '@/use/use-note-capture.js';
import SkMutedNote from '@/components/SkMutedNote.vue';
import { instance, policies } from '@/instance';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@ -128,6 +136,7 @@ const translating = ref(false);
const isDeleted = ref(false);
const renoted = ref(false);
const reactButton = shallowRef<HTMLElement>();
const clipButton = useTemplateRef('clipButton');
const renoteButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
@ -153,6 +162,8 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`,
}));
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
async function addReplyTo(replyNote: Misskey.entities.Note) {
replies.value.unshift(replyNote);
appearNote.value.repliesCount += 1;
@ -376,6 +387,14 @@ function menu(): void {
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
}
async function clip(): Promise<void> {
os.popupMenu(await getNoteClipMenu({ note: props.note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
async function translate() {
await translateNote(appearNote.value.id, translation, translating);
}
if (props.detail) {
misskeyApi('notes/children', {
noteId: props.note.id,

View file

@ -15,13 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notes }">
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: pagination.reversed }]">
<template v-for="(note, i) in notes" :key="note.id">
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
<DynamicNote :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true"/>
<div :class="$style.ad">
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
</div>
</div>
<DynamicNote v-else :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true" :data-scroll-anchor="note.id"/>
<DynamicNote :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true" :data-scroll-anchor="note.id"/>
<MkAd v-if="note._shouldInsertAd_" :preferForms="['horizontal', 'horizontal-big']" :class="$style.ad"/>
</template>
</div>
</template>

View file

@ -242,13 +242,18 @@ watch(props, async () => {
const type = props.notification.type;
// To avoid extra lookups, only do the query when it actually matters.
if (type === 'follow' || type === 'receiveFollowRequest') {
const user = await misskeyApi('users/show', {
userId: props.notification.userId,
});
if ((type === 'follow' || type === 'receiveFollowRequest') && props.notification.userId) {
try {
const user = await misskeyApi('users/show', {
userId: props.notification.userId,
});
userDetailed.value = user;
followRequestDone.value = !user.hasPendingFollowRequestToYou;
userDetailed.value = user;
followRequestDone.value = !user.hasPendingFollowRequestToYou;
} catch {
userDetailed.value = null;
followRequestDone.value = false;
}
} else {
userDetailed.value = null;
followRequestDone.value = false;

View file

@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{ items: notifications }">
<component
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
<SkTransitionGroup
:class="[$style.notifications]"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
@ -23,11 +23,11 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass=" $style.transition_x_move"
tag="div"
>
<template v-for="(notification, i) in notifications" :key="notification.id">
<div v-for="(notification, i) in notifications" :key="notification.id">
<DynamicNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/>
<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/>
</template>
</component>
</div>
</SkTransitionGroup>
</template>
</MkPagination>
</MkPullToRefresh>
@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { prefer } from '@/preferences.js';
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];

View file

@ -557,6 +557,7 @@ async function toggleLocalOnly() {
if (confirm.result === 'no') return;
if (confirm.result === 'neverShow') {
prefer.commit('neverShowLocalOnlyInfo', 'true');
miLocalStorage.setItem('neverShowLocalOnlyInfo', 'true');
}
}

View file

@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<component
:is="prefer.s.animation ? TransitionGroup : 'div'"
<SkTransitionGroup
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
@ -14,8 +13,10 @@ SPDX-License-Identifier: AGPL-3.0-only
tag="div" :class="$style.root"
>
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
<slot v-if="hasMoreReactions" name="more"/>
</component>
<div v-if="hasMoreReactions" :key="'$more'" :class="$style.moreReactions">
<slot name="more"/>
</div>
</SkTransitionGroup>
</template>
<script lang="ts" setup>
@ -25,6 +26,7 @@ import { TransitionGroup } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@ -102,7 +104,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
position: absolute;
}
.root {
.root, .moreReactions {
display: flex;
flex-wrap: wrap;
align-items: center;

View file

@ -14,8 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{ items: notes }">
<component
:is="prefer.s.animation ? TransitionGroup : 'div'"
<SkTransitionGroup
:class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: paginationQuery.reversed }]"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
@ -24,16 +23,11 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass=" $style.transition_x_move"
tag="div"
>
<template v-for="(note, i) in notes" :key="note.id">
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
<DynamicNote :class="$style.note" :note="note" :withHardMute="true"/>
<div :class="$style.ad">
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
</div>
</div>
<DynamicNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/>
</template>
</component>
<div v-for="(note, i) in notes" :key="note.id" :class="{ '_gaps': !noGap }">
<DynamicNote :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true" :data-scroll-anchor="note.id"/>
<MkAd v-if="note._shouldInsertAd_" :preferForms="['horizontal', 'horizontal-big']" :class="$style.ad"/>
</div>
</SkTransitionGroup>
</template>
</MkPagination>
</MkPullToRefresh>
@ -54,6 +48,7 @@ import DynamicNote from '@/components/DynamicNote.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
const props = withDefaults(defineProps<{
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';

View file

@ -0,0 +1,84 @@
<!--
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.badges">
<div
v-for="badge of badges"
:key="badge.key"
:class="[$style.badge, semanticClass(badge)]"
>
{{ badge.label }}
</div>
</div>
</template>
<script lang="ts">
export interface Badge {
/**
* ID/key of this badge, must be unique within the strip.
*/
key: string;
/**
* Label text to display.
* Should already be translated.
*/
label: string;
/**
* Semantic style of the badge.
* Defaults to "neutral" if unset.
*/
style?: 'success' | 'neutral' | 'warning' | 'error';
}
</script>
<script setup lang="ts">
import { useCssModule } from 'vue';
const $style = useCssModule();
defineProps<{
badges: Badge[],
}>();
function semanticClass(badge: Badge): string {
const style = badge.style ?? 'neutral';
return $style[`semantic_${style}`];
}
</script>
<style module lang="scss">
.badges {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--MI-margin);
}
.badge {
display: inline-block;
border: solid 1px;
border-radius: var(--MI-radius-sm);
padding: 2px 6px;
font-size: 85%;
}
.semantic_error {
color: var(--MI_THEME-error);
border-color: var(--MI_THEME-error);
}
.semantic_warning {
color: var(--MI_THEME-warn);
border-color: var(--MI_THEME-warn);
}
.semantic_success {
color: var(--MI_THEME-success);
border-color: var(--MI_THEME-success);
}
</style>

View file

@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{ items: notes }">
<!-- TODO replace with SkDateSeparatedList when merged -->
<MkDateSeparatedList v-slot="{ item: note }" :items="notes" :class="$style.panel" :noGap="true">
<SkFollowingFeedEntry :note="note" :class="props.selectedUserId == note.userId && $style.selected" @select="u => selectUser(u.id)"/>
</MkDateSeparatedList>

View file

@ -99,6 +99,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.unixtime }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.unixtimeDescription }}</p>
<div class="preview">
<Mfm :text="preview_unixtime"/>
<MkTextarea v-model="preview_unixtime"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.inlineCode }}</div>
<div class="content">
@ -429,6 +439,9 @@ const preview_small = ref(
const preview_center = ref(
`<center>${i18n.ts._mfm.dummy}</center>`,
);
const preview_unixtime = ref(
`$[unixtime ${Math.floor(Date.now() / 1000)}]`,
);
const preview_inlineCode = ref('`<: "Hello, world!"`');
const preview_blockCode = ref(
'```ai\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="note.user"/>
</template>
</I18n>
<I18n v-else-if="prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
<I18n v-else-if="!prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>

View file

@ -155,13 +155,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.footerButton" @mousedown.prevent="translate()">
<button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" class="_button" :class="$style.footerButton" :disabled="translating || !!translation" @click.stop="translate()">
<i class="ti ti-language-hiragana"></i>
</button>
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
<button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@ -226,7 +226,7 @@ import { getNoteSummary } from '@/utility/get-note-summary.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
import { instance, isEnabledUrlPreview } from '@/instance.js';
import { instance, isEnabledUrlPreview, policies } from '@/instance.js';
import { focusPrev, focusNext } from '@/utility/focus.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
@ -360,7 +360,7 @@ const keymap = {
clip();
},
't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) {
if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) {
translate();
}
},

View file

@ -171,13 +171,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @click.stop="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="translate()">
<button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()">
<i class="ti ti-language-hiragana"></i>
</button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@ -283,7 +283,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
import { instance, isEnabledUrlPreview } from '@/instance.js';
import { instance, isEnabledUrlPreview, policies } from '@/instance.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
@ -394,7 +394,7 @@ const keymap = {
clip();
},
't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) {
if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) {
translate();
}
},

View file

@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
@click.stop="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
@ -50,24 +50,30 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton"
class="_button"
:class="$style.noteFooterButton"
@mousedown="quote()"
@click.stop="quote()"
>
<i class="ph-quotes ph-bold ph-lg"></i>
</button>
<button v-else class="_button" :class="$style.noteFooterButton" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i>
</button>
<button v-if="note.myReaction == null && note.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
<button v-if="note.myReaction == null && note.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @click.stop="like()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="note.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
<button v-if="note.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @click.stop="react()">
<i v-if="note.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
</button>
<button v-if="note.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(note)">
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.noteFooterButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()">
<i class="ti ti-language-hiragana"></i>
</button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()">
<i class="ph-dots-three ph-bold ph-lg"></i>
</button>
</footer>
@ -86,10 +92,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject, ref, shallowRef, watch } from 'vue';
import { computed, inject, ref, shallowRef, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import * as config from '@@/js/config.js';
import type { Ref } from 'vue';
import type { Visibility } from '@/utility/boost-quote.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import SkNoteHeader from '@/components/SkNoteHeader.vue';
@ -109,11 +116,12 @@ import { showMovedDialog } from '@/utility/show-moved-dialog.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { reactionPicker } from '@/utility/reaction-picker.js';
import { claimAchievement } from '@/utility/achievements.js';
import { getNoteMenu } from '@/utility/get-note-menu.js';
import { getNoteClipMenu, getNoteMenu, translateNote } from '@/utility/get-note-menu.js';
import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js';
import { prefer } from '@/preferences.js';
import { useNoteCapture } from '@/use/use-note-capture.js';
import SkMutedNote from '@/components/SkMutedNote.vue';
import { instance, policies } from '@/instance';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@ -142,6 +150,7 @@ const translating = ref(false);
const isDeleted = ref(false);
const renoted = ref(false);
const reactButton = shallowRef<HTMLElement>();
const clipButton = useTemplateRef('clipButton');
const renoteButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
@ -167,6 +176,8 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: appearNote.value.url ?? appearNote.value.uri ?? `${config.url}/notes/${appearNote.value.id}`,
}));
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
async function addReplyTo(replyNote: Misskey.entities.Note) {
replies.value.unshift(replyNote);
appearNote.value.repliesCount += 1;
@ -390,6 +401,14 @@ function menu(): void {
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
}
async function clip(): Promise<void> {
os.popupMenu(await getNoteClipMenu({ note: props.note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
async function translate() {
await translateNote(appearNote.value.id, translation, translating);
}
if (props.detail) {
misskeyApi('notes/children', {
noteId: props.note.id,

View file

@ -0,0 +1,43 @@
<!--
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<TransitionGroup v-if="animate ?? prefer.s.animation" v-bind="props" :class="props.class">
<slot></slot>
</TransitionGroup>
<component :is="tag" v-else :class="props.class">
<slot></slot>
</component>
</template>
<script setup lang="ts">
import type { TransitionGroupProps } from 'vue';
import { prefer } from '@/preferences';
// This is a "best guess" type.
// If any valid :class binding produces a type error here, then please change this to match.
type ClassBinding = string | Record<string, boolean | undefined>;
// This can be an inline type, but pulling it out makes TS errors clearer.
interface SkTransitionGroupProps extends TransitionGroupProps {
/**
* Override CSS styles for the TransitionGroup or native element.
*/
class?: undefined | ClassBinding | ClassBinding[];
/**
* If true, will render a TransitionGroup.
* If false, will render a native element.
* If null or undefined (default), will respect the value of prefer.s.animation.
*/
animate?: boolean | undefined | null;
}
const props = withDefaults(defineProps<SkTransitionGroupProps>(), {
tag: 'div',
class: undefined,
animate: undefined,
});
</script>

View file

@ -5,17 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div ref="rootEl">
<div ref="headerEl" :class="$style.header">
<div ref="headerEl" :class="{ [$style.header]: sticky }">
<slot name="header"></slot>
</div>
<div
:class="$style.body"
:class="{ [$style.body]: sticky }"
:data-sticky-container-header-height="headerHeight"
:data-sticky-container-footer-height="footerHeight"
>
<slot></slot>
</div>
<div ref="footerEl" :class="$style.footer">
<div ref="footerEl" :class="{ [$style.footer]: sticky }">
<slot name="footer"></slot>
</div>
</div>
@ -25,6 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, provide, inject, ref, watch, useTemplateRef } from 'vue';
import { DI } from '@/di.js';
withDefaults(defineProps<{
sticky?: boolean,
}>(), {
sticky: true,
});
const rootEl = useTemplateRef('rootEl');
const headerEl = useTemplateRef('headerEl');
const footerEl = useTemplateRef('footerEl');

View file

@ -4,12 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<TransitionGroup
:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
<SkTransitionGroup
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
:moveClass="$style.transition_x_move"
:duration="200"
tag="div" :class="$style.tabs"
>
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
</TransitionGroup>
</SkTransitionGroup>
</template>
<script lang="ts" setup>
@ -47,6 +47,7 @@ import { prefer } from '@/preferences.js';
import MkLoadingPage from '@/pages/_loading_.vue';
import { DI } from '@/di.js';
import { deepEqual } from '@/utility/deep-equal.js';
import SkTransitionGroup from '@/components/SkTransitionGroup.vue';
const props = defineProps<{
router?: Router;