Render all notes via Magnetar
ci/woodpecker/push/ociImagePush Pipeline was successful Details

This commit is contained in:
Natty 2023-12-27 03:50:56 +01:00
parent 76c4d0267f
commit 69cb1c5220
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
46 changed files with 675 additions and 1756 deletions

View File

@ -11,7 +11,7 @@
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
:id="appearNote.id" :id="appearNote.id"
> >
<MkNoteSub <XNoteSub
v-if="appearNote.parent_note && !detailedView && !collapsedReply" v-if="appearNote.parent_note && !detailedView && !collapsedReply"
:note="appearNote.parent_note" :note="appearNote.parent_note"
class="reply-to" class="reply-to"
@ -99,7 +99,7 @@
<XNoteHeader class="header" :note="appearNote" /> <XNoteHeader class="header" :note="appearNote" />
</div> </div>
<div class="body"> <div class="body">
<MkSubNoteContent <XSubNoteContent
class="text" class="text"
:note="appearNote" :note="appearNote"
:detailed="true" :detailed="true"
@ -108,7 +108,7 @@
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()" @focusfooter="footerEl.focus()"
@expanded="(e) => setPostExpanded(e)" @expanded="(e) => setPostExpanded(e)"
></MkSubNoteContent> ></XSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
<div v-else class="translated"> <div v-else class="translated">
@ -229,14 +229,14 @@ import type { Ref } from "vue";
import { computed, inject, onMounted, ref, toRaw } from "vue"; import { computed, inject, onMounted, ref, toRaw } from "vue";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import MkNoteSub from "@/components/MkNoteSub.vue"; import XNoteSub from "@/components/MagNoteSub.vue";
import MkSubNoteContent from "./MkSubNoteContent.vue"; import XSubNoteContent from "./MagSubNoteContent.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MagNoteHeader.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XRenoteButton from "@/components/MagRenoteButton.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue"; import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue"; import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue"; import XQuoteButton from "@/components/MagQuoteButton.vue";
import MkVisibility from "@/components/MkVisibility.vue"; import MkVisibility from "@/components/MkVisibility.vue";
import copyToClipboard from "@/scripts/copy-to-clipboard"; import copyToClipboard from "@/scripts/copy-to-clipboard";
import { url } from "@/config"; import { url } from "@/config";
@ -298,13 +298,13 @@ if (noteViewInterruptors.length > 0) {
const isRenote = magIsRenote(note); const isRenote = magIsRenote(note);
const el = ref<HTMLElement>(); const el = ref<HTMLElement | null>(null);
const footerEl = ref<HTMLElement>(); const footerEl = ref<HTMLElement | null>(null);
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement | null>(null);
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const renoteTime = ref<HTMLElement>(); const renoteTime = ref<HTMLElement | null>(null);
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement | null>(null);
let appearNote = $computed( let appearNote = $computed(
() => magEffectiveNote(note) as packed.PackNoteMaybeFull () => magEffectiveNote(note) as packed.PackNoteMaybeFull
); );
@ -386,7 +386,7 @@ function onContextmenu(ev: MouseEvent): void {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
}; };
if (isLink(ev.target)) return; if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== "") return; if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
@ -451,11 +451,11 @@ function menu(viaKeyboard = false): void {
note: note, note: note,
translating, translating,
translation, translation,
menuButton, menuButton: menuButton.value,
isDeleted, isDeleted,
currentClipPage, currentClipPage: currentClipPage?.value,
}), }),
menuButton.value, menuButton.value ?? undefined,
{ {
viaKeyboard, viaKeyboard,
} }
@ -478,7 +478,7 @@ function showRenoteMenu(viaKeyboard = false): void {
}, },
}, },
], ],
renoteTime.value, renoteTime.value ?? undefined,
{ {
viaKeyboard: viaKeyboard, viaKeyboard: viaKeyboard,
} }

View File

@ -9,7 +9,7 @@
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : null"
:class="{ renote: magIsRenote(note) }" :class="{ renote: magIsRenote(note) }"
> >
<MkNoteSub <MagNoteSub
v-if="conversation" v-if="conversation"
v-for="note in conversation" v-for="note in conversation"
:key="note.id" :key="note.id"
@ -18,20 +18,20 @@
:detailedView="true" :detailedView="true"
/> />
<MkLoading v-else-if="appearNote.parent_note_id" mini /> <MkLoading v-else-if="appearNote.parent_note_id" mini />
<MkNoteSub <MagNoteSub
v-if="parentNote" v-if="parentNote"
:note="parentNote" :note="parentNote"
class="reply-to" class="reply-to"
:detailedView="true" :detailedView="true"
/> />
<MagNote <XNote
ref="noteEl" ref="noteEl"
@contextmenu.stop="onContextmenu" @contextmenu.stop="onContextmenu"
tabindex="-1" tabindex="-1"
:note="note" :note="note"
detailedView detailedView
></MagNote> ></XNote>
<MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab"> <MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab">
<option value="replies"> <option value="replies">
@ -53,7 +53,7 @@
</option> </option>
<option value="quotes" v-if="directQuotes?.length > 0"> <option value="quotes" v-if="directQuotes?.length > 0">
<!-- <i class="ph-quotes ph-bold ph-lg"></i> --> <!-- <i class="ph-quotes ph-bold ph-lg"></i> -->
<span class="count">{{ directQuotes.length }}</span> <span class="count">{{ directQuotes!.length }}</span>
{{ i18n.ts._notification._types.quote }} {{ i18n.ts._notification._types.quote }}
</option> </option>
<option value="clips" v-if="clips?.length > 0"> <option value="clips" v-if="clips?.length > 0">
@ -63,8 +63,8 @@
</option> </option>
</MkTab> </MkTab>
<MkNoteSub <MagNoteSub
v-if="directReplies.length && tab === 'replies'" v-if="directReplies && tab === 'replies'"
v-for="note in directReplies" v-for="note in directReplies"
:key="note.id" :key="note.id"
:note="note" :note="note"
@ -75,7 +75,7 @@
/> />
<MkLoading v-else-if="tab === 'replies' && note.reply_count > 0" /> <MkLoading v-else-if="tab === 'replies' && note.reply_count > 0" />
<MkNoteSub <MagNoteSub
v-if="directQuotes && tab === 'quotes'" v-if="directQuotes && tab === 'quotes'"
v-for="note in directQuotes" v-for="note in directQuotes"
:key="note.id" :key="note.id"
@ -85,7 +85,7 @@
:detailedView="true" :detailedView="true"
:parentId="appearNote.id" :parentId="appearNote.id"
/> />
<MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" /> <MkLoading v-else-if="tab === 'quotes' && directQuotes?.length > 0" />
<!-- <MkPagination <!-- <MkPagination
v-if="tab === 'renotes'" v-if="tab === 'renotes'"
@ -152,11 +152,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, onUpdated, ref, toRaw, watch } from "vue"; import { onMounted, onUnmounted, onUpdated, Ref, ref, toRaw, watch } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import MkTab from "@/components/MkTab.vue"; import MkTab from "@/components/MkTab.vue";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MagNoteSub from "@/components/MagNoteSub.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XNote from "@/components/MagNote.vue";
import XRenoteButton from "@/components/MagRenoteButton.vue";
import MkUserCardMini from "@/components/MkUserCardMini.vue"; import MkUserCardMini from "@/components/MkUserCardMini.vue";
import MkReactedUsers from "@/components/MkReactedUsers.vue"; import MkReactedUsers from "@/components/MkReactedUsers.vue";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
@ -177,8 +178,9 @@ import {
magIsRenote, magIsRenote,
magReactionCount, magReactionCount,
magReactionToLegacy, magReactionToLegacy,
resolveNote,
} from "@/scripts-mag/mag-util"; } from "@/scripts-mag/mag-util";
import MagNote from "@/components/MagNote.vue"; import { shallowRef } from "@vue/runtime-core";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteMaybeFull; note: packed.PackNoteMaybeFull;
@ -210,11 +212,11 @@ if (noteViewInterruptors.length > 0) {
}); });
} }
const el = ref<HTMLElement>(); const el = ref<HTMLElement | null>(null);
const noteEl = $ref(); const noteEl = ref<HTMLElement | null>(null);
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement | null>(null);
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement | null>(null);
let appearNote = $computed( let appearNote = $computed(
() => magEffectiveNote(note) as packed.PackNoteMaybeFull () => magEffectiveNote(note) as packed.PackNoteMaybeFull
); );
@ -226,12 +228,12 @@ const muted = ref(
); );
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
let conversation = $ref<null | misskey.entities.Note[]>([]); let conversation = shallowRef<packed.PackNoteMaybeFull[]>([]);
const replies = ref<misskey.entities.Note[]>([]); const replies = shallowRef<packed.PackNoteMaybeFull[]>([]);
let directReplies = $ref<misskey.entities.Note[]>([]); let directReplies = shallowRef<packed.PackNoteMaybeFull[] | null>(null);
let directQuotes = $ref<null | misskey.entities.Note[]>([]); let directQuotes = shallowRef<packed.PackNoteMaybeFull[] | null>(null);
let clips = $ref(); let clips = ref<misskey.entities.Clip[]>([]);
let renotes = $ref(); let renotes = ref<misskey.entities.Note[] | null>();
let isScrolling; let isScrolling;
const keymap = { const keymap = {
@ -244,7 +246,7 @@ const keymap = {
}; };
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el as Ref<HTMLElement>,
note: $$(appearNote), note: $$(appearNote),
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
@ -283,7 +285,7 @@ function onContextmenu(ev: MouseEvent): void {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
}; };
if (isLink(ev.target)) return; if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== "") return; if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
@ -295,7 +297,7 @@ function onContextmenu(ev: MouseEvent): void {
note: note, note: note,
translating, translating,
translation, translation,
menuButton, menuButton: menuButton.value,
isDeleted, isDeleted,
}), }),
ev ev
@ -309,10 +311,10 @@ function menu(viaKeyboard = false): void {
note: note, note: note,
translating, translating,
translation, translation,
menuButton, menuButton: menuButton.value,
isDeleted, isDeleted,
}), }),
menuButton.value, menuButton.value ?? undefined,
{ {
viaKeyboard, viaKeyboard,
} }
@ -320,11 +322,11 @@ function menu(viaKeyboard = false): void {
} }
function focus() { function focus() {
noteEl.focus(); noteEl.value?.focus();
} }
function blur() { function blur() {
noteEl.blur(); noteEl.value?.blur();
} }
async function updateParent(note: packed.PackNoteMaybeFull) { async function updateParent(note: packed.PackNoteMaybeFull) {
@ -347,7 +349,6 @@ async function updateParent(note: packed.PackNoteMaybeFull) {
watch(appearNote, updateParent); watch(appearNote, updateParent);
updateParent(note); updateParent(note);
directReplies = [];
os.api("notes/children", { os.api("notes/children", {
noteId: appearNote.id, noteId: appearNote.id,
limit: 30, limit: 30,
@ -362,29 +363,34 @@ os.api("notes/children", {
}, },
[] []
); );
replies.value = res;
directReplies = res Promise.all(res.map(resolveNote)).then((a) => {
.filter((resNote) => resNote.replyId === appearNote.id) replies.value = a;
.reverse(); directReplies.value = a
directQuotes = res.filter((resNote) => resNote.renoteId === appearNote.id); .filter((resNote) => resNote.parent_note_id === appearNote.id)
.reverse();
directQuotes.value = a.filter(
(resNote) => resNote.renoted_note_id === appearNote.id
);
});
}); });
conversation = null;
if (appearNote.parent_note_id) { if (appearNote.parent_note_id) {
os.api("notes/conversation", { os.api("notes/conversation", {
noteId: appearNote.parent_note_id, noteId: appearNote.parent_note_id,
limit: 30, limit: 30,
}).then((res) => { }).then(async (res) => {
conversation = res.reverse(); conversation.value = await Promise.all<packed.PackNoteMaybeFull>(
res?.reverse().map(resolveNote)
);
focus(); focus();
}); });
} }
clips = null;
os.api("notes/clips", { os.api("notes/clips", {
noteId: appearNote.id, noteId: appearNote.id,
}).then((res) => { }).then((res) => {
clips = res; clips.value = res as misskey.entities.Clip[];
}); });
// const pagination = { // const pagination = {
@ -395,14 +401,13 @@ os.api("notes/clips", {
// const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); // const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
renotes = null;
function loadTab() { function loadTab() {
if (tab === "renotes" && !renotes) { if (tab === "renotes" && !renotes) {
os.api("notes/renotes", { os.api("notes/renotes", {
noteId: appearNote.id, noteId: appearNote.id,
limit: 100, limit: 100,
}).then((res) => { }).then((res) => {
renotes = res; renotes.value = res;
}); });
} }
} }
@ -430,13 +435,23 @@ async function onNoteUpdated(noteData: NoteUpdatedEvent): Promise<void> {
switch (type) { switch (type) {
case "replied": case "replied":
const { id: createdId } = body; const { id: createdId } = body;
const replyNote = await os.api("notes/show", { const replyNote = await os.magApi(
noteId: createdId, endpoints.GetNoteById,
}); {
context: true,
attachments: true,
},
{
id: createdId,
}
);
replies.value.splice(found, 0, replyNote); replies.value.splice(found, 0, replyNote);
if (found === 0) { if (found === 0) {
directReplies.push(replyNote); directReplies.value = [
...(directReplies.value ?? []),
replyNote,
];
} }
break; break;
@ -457,12 +472,12 @@ document.addEventListener("wheel", () => {
onMounted(() => { onMounted(() => {
stream.on("noteUpdated", onNoteUpdated); stream.on("noteUpdated", onNoteUpdated);
isScrolling = false; isScrolling = false;
noteEl.scrollIntoView(); noteEl.value?.scrollIntoView();
}); });
onUpdated(() => { onUpdated(() => {
if (!isScrolling) { if (!isScrolling) {
noteEl.scrollIntoView(); noteEl.value?.scrollIntoView();
if (location.hash) { if (location.hash) {
location.replace(location.hash); // Jump to highlighted reply location.replace(location.hash); // Jump to highlighted reply
} }

View File

@ -9,13 +9,7 @@
@click.stop @click.stop
> >
<MkUserName :user="note.user" class="mkusername"> <MkUserName :user="note.user" class="mkusername">
<span <span v-if="note.user.is_bot" class="is-bot">bot</span>
v-if="
magTransProperty(note.user, 'is_bot', 'isBot')
"
class="is-bot"
>bot</span
>
</MkUserName> </MkUserName>
</MkA> </MkA>
<div class="username"><MkAcct :user="note.user" /></div> <div class="username"><MkAcct :user="note.user" /></div>
@ -23,38 +17,16 @@
<div> <div>
<div class="info"> <div class="info">
<MkA class="created-at" :to="notePage(note)"> <MkA class="created-at" :to="notePage(note)">
<MkTime <MkTime :time="note.created_at" />
:time="
magTransProperty(
note,
'created_at',
'createdAt'
)
"
/>
<i <i
v-if=" v-if="note.updated_at"
magTransProperty(
note,
'updated_at',
'updatedAt'
)
"
v-tooltip.noDelay=" v-tooltip.noDelay="
i18n.t('edited', { i18n.t('edited', {
date: new Date( date: new Date(
magTransProperty( note.updated_at
note,
'updated_at',
'updatedAt'
)!
).toLocaleDateString(), ).toLocaleDateString(),
time: new Date( time: new Date(
magTransProperty( note.updated_at
note,
'updated_at',
'updatedAt'
)!
).toLocaleTimeString(), ).toLocaleTimeString(),
}) })
" "
@ -75,7 +47,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type * as misskey from "calckey-js";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import MkVisibility from "@/components/MkVisibility.vue"; import MkVisibility from "@/components/MkVisibility.vue";
import MkInstanceTicker from "@/components/MkInstanceTicker.vue"; import MkInstanceTicker from "@/components/MkInstanceTicker.vue";
@ -83,18 +54,15 @@ import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteBase | misskey.entities.Note; note: packed.PackNoteBase;
pinned?: boolean; pinned?: boolean;
}>(); }>();
let note = $ref(props.note);
const showTicker = const showTicker =
defaultStore.state.instanceTicker === "always" || defaultStore.state.instanceTicker === "always" ||
(defaultStore.state.instanceTicker === "remote" && note.user.host); (defaultStore.state.instanceTicker === "remote" && props.note.user.host);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -0,0 +1,44 @@
<template>
<XNote
v-if="noteData"
:note="noteData"
:collapsed-reply="collapsedReply"
:detailed-view="detailedView"
:pinned="pinned"
>
</XNote>
<div v-else>
<i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import XNote from "@/components/MagNote.vue";
import { packed } from "magnetar-common";
import { resolveNote } from "@/scripts-mag/mag-util";
const props = defineProps<{
note: packed.PackNoteMaybeFull | string;
pinned?: boolean;
detailedView?: boolean;
collapsedReply?: boolean;
}>();
let noteData = ref<packed.PackNoteMaybeFull | null>(null);
watch(
() => props.note,
(val) => {
if (typeof val !== "string") {
noteData.value = val;
return;
}
resolveNote({ id: val }).then((n) => {
noteData.value = n;
});
},
{ immediate: true }
);
</script>

View File

@ -4,20 +4,19 @@
<div class="main"> <div class="main">
<XNoteHeader class="header" :note="note" :mini="true" /> <XNoteHeader class="header" :note="note" :mini="true" />
<div class="body"> <div class="body">
<MkSubNoteContent class="text" :note="note" /> <MagSubNoteContent class="text" :note="note" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as misskey from "calckey-js"; import XNoteHeader from "@/components/MagNoteHeader.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import MagSubNoteContent from "@/components/MagSubNoteContent.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteMaybeFull | misskey.entities.Note; note: packed.PackNoteMaybeFull;
pinned?: boolean; pinned?: boolean;
}>(); }>();
</script> </script>

View File

@ -29,13 +29,13 @@
<div class="body"> <div class="body">
<XNoteHeader class="header" :note="note" :mini="true" /> <XNoteHeader class="header" :note="note" :mini="true" />
<div class="body"> <div class="body">
<MkSubNoteContent <XSubNoteContent
class="text" class="text"
:note="note" :note="note"
:parentId="parentId" :parentId="parentId"
:conversation="conversation" :conversation="conversation"
:detailedView="detailedView" :detailedView="detailedView"
@focusfooter="footerEl.focus()" @focusfooter="footerEl?.focus()"
/> />
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -68,25 +68,9 @@
@click="reply()" @click="reply()"
> >
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<template <template v-if="appearNote.reply_count > 0">
v-if="
Number(
magTransProperty(
appearNote,
'reply_count',
'repliesCount'
)
) > 0
"
>
<p class="count"> <p class="count">
{{ {{ appearNote.reply_count }}
magTransProperty(
appearNote,
"reply_count",
"repliesCount"
)
}}
</p> </p>
</template> </template>
</button> </button>
@ -94,13 +78,7 @@
ref="renoteButton" ref="renoteButton"
class="button" class="button"
:note="appearNote" :note="appearNote"
:count=" :count="appearNote.renote_count"
magTransProperty(
appearNote,
'renote_count',
'renoteCount'
)
"
/> />
<XStarButtonNoEmoji <XStarButtonNoEmoji
v-if="!enableEmojiReactions" v-if="!enableEmojiReactions"
@ -150,7 +128,7 @@
</div> </div>
</div> </div>
<template v-if="conversation"> <template v-if="conversation">
<MkNoteSub <MagNoteSub
v-if="replyLevel < 11 && depth < 5" v-if="replyLevel < 11 && depth < 5"
v-for="reply in replies" v-for="reply in replies"
:key="reply.id" :key="reply.id"
@ -194,13 +172,13 @@
import type { Ref } from "vue"; import type { Ref } from "vue";
import { inject, ref, toRaw } from "vue"; import { inject, ref, toRaw } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MagNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue"; import XSubNoteContent from "@/components/MagSubNoteContent.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue"; import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue"; import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XRenoteButton from "@/components/MagRenoteButton.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue"; import XQuoteButton from "@/components/MagQuoteButton.vue";
import copyToClipboard from "@/scripts/copy-to-clipboard"; import copyToClipboard from "@/scripts/copy-to-clipboard";
import { url } from "@/config"; import { url } from "@/config";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
@ -217,21 +195,21 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
import { import {
magEffectiveNote,
magHasReacted, magHasReacted,
magIsRenote,
magReactionCount, magReactionCount,
magReactionSelf,
magReactionToLegacy, magReactionToLegacy,
magTransProperty,
} from "@/scripts-mag/mag-util"; } from "@/scripts-mag/mag-util";
const router = useRouter(); const router = useRouter();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
note: packed.PackNoteMaybeFull | misskey.entities.Note; note: packed.PackNoteMaybeFull;
conversation?: misskey.entities.Note[]; conversation?: packed.PackNoteMaybeAttachments[];
parentId?; parentId?: string;
detailedView?; detailedView?: boolean;
// how many notes are in between this one and the note being viewed in detail // how many notes are in between this one and the note being viewed in detail
depth?: number; depth?: number;
@ -244,9 +222,7 @@ const props = withDefaults(
} }
); );
let note = $ref<packed.PackNoteMaybeFull | misskey.entities.Note>( let note = $ref<packed.PackNoteMaybeFull>(structuredClone(toRaw(props.note)));
structuredClone(toRaw(props.note))
);
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason; if (what === "note") return i18n.ts.userSaysSomethingReason;
@ -258,36 +234,32 @@ const softMuteReasonI18nSrc = (what?: string) => {
return i18n.ts.userSaysSomething; return i18n.ts.userSaysSomething;
}; };
const el = ref<HTMLElement>(); const el = ref<HTMLElement | null>(null);
const footerEl = ref<HTMLElement>(); const footerEl = ref<HTMLElement | null>(null);
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement | null>(null);
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement | null>(null);
let appearNote = $computed(() => let appearNote = $computed(
magIsRenote(note) () => magEffectiveNote(props.note) as packed.PackNoteMaybeFull
? (magTransProperty(note, "renoted_note", "renote") as
| packed.PackNoteMaybeFull
| misskey.entities.Note)
: note
); );
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords)); const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const replies: misskey.entities.Note[] = const replies: packed.PackNoteMaybeFull[] =
props.conversation props.conversation
?.filter( ?.filter(
(item) => (item) =>
item.replyId === appearNote.id || item.parent_note_id === appearNote.id ||
item.renoteId === appearNote.id item.renoted_note_id === appearNote.id
) )
.reverse() ?? []; .reverse() ?? [];
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick; const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el as Ref<HTMLElement>,
note: $$(appearNote), note: $$(appearNote),
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
@ -319,8 +291,8 @@ function react(viaKeyboard = false): void {
); );
} }
function undoReact(note): void { function undoReact(note: packed.PackNoteBase): void {
const oldReaction = note.myReaction; const oldReaction = magReactionSelf(note);
if (!oldReaction) return; if (!oldReaction) return;
os.api("notes/reactions/delete", { os.api("notes/reactions/delete", {
noteId: note.id, noteId: note.id,
@ -338,11 +310,11 @@ function menu(viaKeyboard = false): void {
note: note, note: note,
translating, translating,
translation, translation,
menuButton, menuButton: menuButton.value,
isDeleted, isDeleted,
currentClipPage, currentClipPage: currentClipPage?.value,
}), }),
menuButton.value, menuButton.value ?? undefined,
{ {
viaKeyboard, viaKeyboard,
} }

View File

@ -2,49 +2,32 @@
<div class="tivcixzd" :class="{ done: closed || isVoted }"> <div class="tivcixzd" :class="{ done: closed || isVoted }">
<ul> <ul>
<li <li
v-for="(choice, i) in magTransProperty( v-for="(choice, i) in note.poll.options"
note.poll,
'options',
'choices'
)"
:key="i" :key="i"
:class="{ voted: magTransProperty(choice, 'voted', 'isVoted') }" :class="{ voted: choice.voted }"
@click.stop="vote(i)" @click.stop="vote(i)"
> >
<div <div
class="backdrop" class="backdrop"
:style="{ :style="{
width: `${ width: `${
showResult showResult ? (choice.votes_count / total) * 100 : 0
? (magTransProperty(
choice,
'votes_count',
'votes'
) /
total) *
100
: 0
}%`, }%`,
}" }"
></div> ></div>
<span> <span>
<template <template v-if="choice.voted"
v-if="magTransProperty(choice, 'voted', 'isVoted')"
><i class="ph-check ph-bold ph-lg"></i ><i class="ph-check ph-bold ph-lg"></i
></template> ></template>
<Mfm <Mfm
:text="magTransProperty(choice, 'title', 'text')" :text="choice.title"
:plain="true" :plain="true"
:custom-emojis="note.emojis" :custom-emojis="note.emojis"
/> />
<span v-if="showResult" class="votes" <span v-if="showResult" class="votes"
>({{ >({{
i18n.t("_poll.votesCount", { i18n.t("_poll.votesCount", {
n: magTransProperty( n: choice.votes_count,
choice,
"votes_count",
"votes"
),
}) })
}})</span }})</span
> >
@ -78,20 +61,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import * as misskey from "calckey-js";
import { sum } from "@/scripts/array"; import { sum } from "@/scripts/array";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { useInterval } from "@/scripts/use-interval"; import { useInterval } from "@/scripts/use-interval";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
note: note: packed.PackNoteMaybeFull & { poll: {} };
| (packed.PackNoteMaybeFull & { poll: {} })
| (misskey.entities.Note &
Required<Pick<misskey.entities.Note, "poll">>);
readOnly?: boolean; readOnly?: boolean;
}>(); }>();
@ -99,20 +77,14 @@ const pollRefreshing = ref(false);
const remaining = ref(-1); const remaining = ref(-1);
const total = computed(() => const total = computed(() =>
sum( sum(props.note.poll.options.map((x) => x.votes_count))
magTransProperty(props.note.poll, "options", "choices").map(
(x) => magTransProperty(x, "votes_count", "votes") as number
)
)
); );
const closed = computed(() => remaining.value === 0); const closed = computed(() => remaining.value === 0);
const isLocal = computed(() => !props.note.uri); const isLocal = computed(() => !props.note.uri);
const isVoted = computed( const isVoted = computed(
() => () =>
!magTransProperty(props.note.poll, "multiple_choice", "multiple") && !props.note.poll.multiple_choice &&
magTransProperty(props.note.poll, "options", "choices").some( props.note.poll.options.some((c) => c.voted ?? false)
(c) => magTransProperty(c, "voted", "isVoted") ?? false
)
); );
const timer = computed(() => const timer = computed(() =>
i18n.t( i18n.t(
@ -135,17 +107,11 @@ const timer = computed(() =>
const showResult = ref(props.readOnly || isVoted.value); const showResult = ref(props.readOnly || isVoted.value);
// //
if (magTransProperty(props.note.poll, "expires_at", "expiresAt")) { if (props.note.poll.expires_at) {
const tick = () => { const tick = () => {
remaining.value = Math.floor( remaining.value = Math.floor(
Math.max( Math.max(
new Date( new Date(props.note.poll.expires_at!).getTime() - Date.now(),
magTransProperty(
props.note.poll,
"expires_at",
"expiresAt"
)!
).getTime() - Date.now(),
0 0
) / 1000 ) / 1000
); );
@ -168,7 +134,16 @@ async function refresh() {
os.api("ap/show", { uri: props.note.uri }) os.api("ap/show", { uri: props.note.uri })
.then((obj) => { .then((obj) => {
if (obj && obj.type === "Note" && obj.object.poll) { if (obj && obj.type === "Note" && obj.object.poll) {
props.note.poll = obj.object.poll; props.note.poll = {
...props.note.poll,
expires_at: obj.object.poll.expiresAt,
multiple_choice: obj.object.poll.multiple,
options: obj.object.poll.choices?.map((c) => ({
title: c.text,
votes_count: c.votes,
voted: c.isVoted,
})),
};
} }
}) })
.catch((err) => { .catch((err) => {
@ -190,11 +165,7 @@ const vote = async (id: number) => {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: "question", type: "question",
text: i18n.t("voteConfirm", { text: i18n.t("voteConfirm", {
choice: magTransProperty( choice: props.note.poll.options[id].title,
magTransProperty(props.note.poll, "options", "choices")[id],
"title",
"text"
),
}), }),
}); });
if (canceled) return; if (canceled) return;
@ -203,12 +174,7 @@ const vote = async (id: number) => {
noteId: props.note.id, noteId: props.note.id,
choice: id, choice: id,
}); });
if (!showResult.value) if (!showResult.value) showResult.value = !props.note.poll.multiple_choice;
showResult.value = !magTransProperty(
props.note.poll,
"multiple_choice",
"multiple"
);
}; };
</script> </script>

View File

@ -11,7 +11,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import * as Misskey from "calckey-js";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
import * as os from "@/os"; import * as os from "@/os";
@ -19,7 +18,7 @@ import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteBase | Misskey.entities.Note; note: packed.PackNoteBase;
}>(); }>();
const canRenote = computed( const canRenote = computed(

View File

@ -33,34 +33,21 @@ import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { MenuItem } from "@/types/menu"; import { MenuItem } from "@/types/menu";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
import { import { magLegacyVisibility } from "@/scripts-mag/mag-util";
magLegacyVisibility,
magTransMap,
magTransProperty,
magVisibility,
} from "@/scripts-mag/mag-util";
import * as Misskey from "calckey-js";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteMaybeFull | Misskey.entities.Note; note: packed.PackNoteMaybeFull;
count: number; count: number;
detailedView?; detailedView?: boolean;
}>(); }>();
const buttonRef = ref<HTMLElement>(); const buttonRef = ref<HTMLElement>();
const hasRenotedBefore = ref<boolean>( const hasRenotedBefore = ref<boolean>((props.note.self_renote_count ?? 0) > 0);
magTransMap(
props.note,
"self_renote_count",
"hasRenotedBefore",
(n) => (n ?? 0) > 0
) ?? false
);
const canRenote = computed( const canRenote = computed(
() => () =>
["Public", "Home"].includes(magVisibility(props.note.visibility)) || ["Public", "Home"].includes(props.note.visibility) ||
($i && props.note.user.id === $i.id) ($i && props.note.user.id === $i.id)
); );
@ -92,7 +79,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
let buttonActions: Array<MenuItem> = []; let buttonActions: Array<MenuItem> = [];
if (magVisibility(props.note.visibility) === "Public") { if (props.note.visibility === "Public") {
buttonActions.push({ buttonActions.push({
text: i18n.ts.renote, text: i18n.ts.renote,
icon: "ph-repeat ph-bold ph-lg", icon: "ph-repeat ph-bold ph-lg",
@ -119,7 +106,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
}); });
} }
if (["Public", "Home"].includes(magVisibility(props.note.visibility))) { if (["Public", "Home"].includes(props.note.visibility)) {
buttonActions.push({ buttonActions.push({
text: `${i18n.ts.renote} (${i18n.ts._visibility.home})`, text: `${i18n.ts.renote} (${i18n.ts._visibility.home})`,
icon: "ph-house ph-bold ph-lg", icon: "ph-house ph-bold ph-lg",
@ -146,7 +133,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
}); });
} }
if (magVisibility(props.note.visibility) === "Direct") { if (props.note.visibility === "Direct") {
buttonActions.push({ buttonActions.push({
text: `${i18n.ts.renote} (${i18n.ts.recipient})`, text: `${i18n.ts.renote} (${i18n.ts.recipient})`,
icon: "ph-envelope-simple-open ph-bold ph-lg", icon: "ph-envelope-simple-open ph-bold ph-lg",
@ -155,12 +142,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
os.api("notes/create", { os.api("notes/create", {
renoteId: props.note.id, renoteId: props.note.id,
visibility: "specified", visibility: "specified",
visibleUserIds: visibleUserIds: props.note.visible_user_ids ?? [],
magTransProperty(
props.note,
"visible_user_ids",
"visibleUserIds"
) ?? [],
}); });
hasRenotedBefore.value = true; hasRenotedBefore.value = true;
const el = const el =
@ -212,18 +194,13 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
action: () => { action: () => {
os.api( os.api(
"notes/create", "notes/create",
magVisibility(props.note.visibility) === "Direct" props.note.visibility === "Direct"
? { ? {
renoteId: props.note.id, renoteId: props.note.id,
visibility: magLegacyVisibility( visibility: magLegacyVisibility(
props.note.visibility props.note.visibility
), ),
visibleUserIds: visibleUserIds: props.note.visible_user_ids ?? [],
magTransProperty(
props.note,
"visible_user_ids",
"visibleUserIds"
) ?? [],
localOnly: true, localOnly: true,
} }
: { : {

View File

@ -1,11 +1,7 @@
<template> <template>
<p v-if="note.cw != null" class="cw"> <p v-if="note.cw != null" class="cw">
<MkA <MkA
v-if=" v-if="conversation && note.renoted_note_id == parentId"
conversation &&
magTransProperty(note, 'renoted_note_id', 'renoteId') ==
parentId
"
:to=" :to="
detailedView ? `#${parentId}` : `${notePage(note)}#${parentId}` detailedView ? `#${parentId}` : `${notePage(note)}#${parentId}`
" "
@ -16,17 +12,11 @@
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</MkA> </MkA>
<MkA <MkA
v-else-if=" v-else-if="!detailed && note.parent_note_id"
!detailed && magTransProperty(note, 'parent_note_id', 'replyId')
"
:to=" :to="
detailedView detailedView
? `#${magTransProperty(note, 'parent_note_id', 'replyId')}` ? `#${note.parent_note_id}`
: `${notePage(note)}#${magTransProperty( : `${note.parent_note_id}`
note,
'parent_note_id',
'replyId'
)}`
" "
behavior="browser" behavior="browser"
v-tooltip="i18n.ts.jumpToPrevious" v-tooltip="i18n.ts.jumpToPrevious"
@ -38,7 +28,7 @@
<Mfm <Mfm
v-if="note.cw != ''" v-if="note.cw != ''"
class="text" class="text"
:mm="magMaybeProperty(note, 'cw_mm')" :mm="note.cw_mm"
:text="note.cw" :text="note.cw"
:author="note.user" :author="note.user"
:i="$i" :i="$i"
@ -51,8 +41,7 @@
:class="{ :class="{
collapsed, collapsed,
isLong, isLong,
manyImages: manyImages: note.attachments?.length > 4,
magTransProperty(note, 'attachments', 'files')?.length > 4,
showContent: note.cw && !showContent, showContent: note.cw && !showContent,
animatedMfm: !disableMfm, animatedMfm: !disableMfm,
}" }"
@ -80,14 +69,7 @@
> >
<template v-if="!note.cw"> <template v-if="!note.cw">
<MkA <MkA
v-if=" v-if="conversation && note.renoted_note_id == parentId"
conversation &&
magTransProperty(
note,
'renoted_note_id',
'renoteId'
) == parentId
"
:to=" :to="
detailedView detailedView
? `#${parentId}` ? `#${parentId}`
@ -100,22 +82,11 @@
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</MkA> </MkA>
<MkA <MkA
v-else-if=" v-else-if="!detailed && note.parent_note_id"
!detailed &&
magTransProperty(note, 'parent_note_id', 'replyId')
"
:to=" :to="
detailedView detailedView
? `#${magTransProperty( ? `#${note.parent_note_id}`
note, : `${notePage(note)}#${note.parent_note_id}`
'parent_note_id',
'replyId'
)}`
: `${notePage(note)}#${magTransProperty(
note,
'parent_note_id',
'replyId'
)}`
" "
behavior="browser" behavior="browser"
v-tooltip="i18n.ts.jumpToPrevious" v-tooltip="i18n.ts.jumpToPrevious"
@ -134,24 +105,14 @@
:custom-emojis="note.emojis" :custom-emojis="note.emojis"
/> />
<MkA <MkA
v-if=" v-if="!detailed && note.renoted_note_id"
!detailed &&
magTransProperty(note, 'renoted_note_id', 'renoteId')
"
class="rp" class="rp"
:to="`/notes/${magTransProperty( :to="`/notes/${note.renoted_note_id}`"
note,
'renoted_note_id',
'renoteId'
)}`"
>{{ i18n.ts.quoteAttached }}: ...</MkA >{{ i18n.ts.quoteAttached }}: ...</MkA
> >
<XMediaList <XMediaList
v-if=" v-if="note.attachments?.length > 0"
magTransProperty(note, 'attachments', 'files')?.length > :media-list="note.attachments!"
0
"
:media-list="magTransProperty(note, 'attachments', 'files')!"
/> />
<XPoll v-if="note.poll" :note="note" class="poll" /> <XPoll v-if="note.poll" :note="note" class="poll" />
<template v-if="detailed"> <template v-if="detailed">
@ -164,18 +125,11 @@
class="url-preview" class="url-preview"
/> />
<div <div
v-if="magTransProperty(note, 'renoted_note', 'renote')" v-if="note.renoted_note"
class="renote" class="renote"
@click.stop=" @click.stop="emit('push', note.renoted_note)"
emit(
'push',
magTransProperty(note, 'renoted_note', 'renote')
)
"
> >
<XNoteSimple <XNoteSimple :note="note.renoted_note!" />
:note="magTransProperty(note, 'renoted_note', 'renote')!"
/>
</div> </div>
</template> </template>
<div <div
@ -222,12 +176,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import * as misskey from "calckey-js";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import * as os from "@/os"; import * as os from "@/os";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MagNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue"; import XMediaList from "@/components/MkMediaList.vue";
import XPoll from "@/components/MkPoll.vue"; import XPoll from "@/components/MagPoll.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue"; import MkUrlPreview from "@/components/MkUrlPreview.vue";
import XShowMoreButton from "@/components/MkShowMoreButton.vue"; import XShowMoreButton from "@/components/MkShowMoreButton.vue";
import XCwButton from "@/components/MkCwButton.vue"; import XCwButton from "@/components/MkCwButton.vue";
@ -238,13 +191,13 @@ import { extractMfmWithAnimation } from "@/scripts/extract-mfm";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { $i } from "@/account"; import { $i } from "@/account";
import { magMaybeProperty, magTransProperty } from "@/scripts-mag/mag-util"; import { magMaybeProperty } from "@/scripts-mag/mag-util";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteMaybeFull | misskey.entities.Note; note: packed.PackNoteMaybeFull;
parentId?; parentId?: string;
conversation?; conversation?: packed.PackNoteMaybeAttachments[];
detailed?: boolean; detailed?: boolean;
detailedView?: boolean; detailedView?: boolean;
}>(); }>();
@ -264,8 +217,7 @@ const isLong =
((props.note.text != null && ((props.note.text != null &&
(props.note.text.split("\n").length > 10 || (props.note.text.split("\n").length > 10 ||
props.note.text.length > 800)) || props.note.text.length > 800)) ||
(magTransProperty(props.note, "attachments", "files") ?? []).length > (props.note.attachments ?? []).length > 4);
4);
const collapsed = $ref(props.note.cw == null && isLong); const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5) ? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)

View File

@ -35,12 +35,13 @@ import { i18n } from "@/i18n";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { globalEvents } from "@/events"; import { globalEvents } from "@/events";
import { packed } from "magnetar-common";
const router = useRouter(); const router = useRouter();
const emit = defineEmits(["refresh"]); const emit = defineEmits(["refresh"]);
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User; user: packed.PackUserBase | Misskey.entities.User;
}>(); }>();
let state = $ref(i18n.ts.processing); let state = $ref(i18n.ts.processing);
@ -48,14 +49,14 @@ const connection = stream.useChannel("main");
let waitIncoming = $ref(false); let waitIncoming = $ref(false);
function accept(user: Misskey.entities.User) { function accept(user: packed.PackUserBase | Misskey.entities.User) {
waitIncoming = true; waitIncoming = true;
os.api("following/requests/accept", { userId: user.id }).then(() => { os.api("following/requests/accept", { userId: user.id }).then(() => {
globalEvents.emit("followeeProcessed", user); globalEvents.emit("followeeProcessed", user);
}); });
} }
function reject(user: Misskey.entities.User) { function reject(user: packed.PackUserBase | Misskey.entities.User) {
waitIncoming = true; waitIncoming = true;
os.api("following/requests/reject", { userId: user.id }).then(() => { os.api("following/requests/reject", { userId: user.id }).then(() => {
globalEvents.emit("followeeProcessed", user); globalEvents.emit("followeeProcessed", user);

View File

@ -1,941 +0,0 @@
<template>
<div
:aria-label="accessibleLabel"
v-if="!muted.muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
v-size="{ max: [500, 350] }"
class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: magIsRenote(note) }"
:id="appearNote.id"
>
<MkNoteSub
v-if="appearNote.reply && !detailedView && !collapsedReply"
:note="appearNote.reply"
class="reply-to"
/>
<div
v-if="!detailedView"
class="note-context"
@click="noteClick"
:class="{
collapsedReply: collapsedReply && appearNote.reply,
}"
>
<div class="line"></div>
<div v-if="appearNote._prId_" class="info">
<i class="ph-megaphone-simple-bold ph-lg"></i>
{{ i18n.ts.promotion
}}<button class="_textButton hide" @click.stop="readPromo()">
{{ i18n.ts.hideThisNote }}
<i class="ph-x ph-bold ph-lg"></i>
</button>
</div>
<div v-if="appearNote._featuredId_" class="info">
<i class="ph-lightning ph-bold ph-lg"></i>
{{ i18n.ts.featured }}
</div>
<div v-if="pinned" class="info">
<i class="ph-push-pin ph-bold ph-lg"></i
>{{ i18n.ts.pinnedNote }}
</div>
<div v-if="magIsRenote(note)" class="renote">
<i class="ph-repeat ph-bold ph-lg"></i>
<I18n :src="i18n.ts.renotedBy" tag="span">
<template #user>
<MkA
v-user-preview="note.userId"
class="name"
:to="userPage(note.user)"
@click.stop
>
<MkUserName :user="note.user" />
</MkA>
</template>
</I18n>
<div class="info">
<button
ref="renoteTime"
class="_button time"
@click.stop="showRenoteMenu()"
>
<i
v-if="isMyRenote"
class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"
></i>
<MkTime :time="note.createdAt" />
</button>
<MkVisibility :note="note" />
</div>
</div>
<div v-if="collapsedReply && appearNote.reply" class="info">
<MkAvatar class="avatar" :user="appearNote.reply.user" />
<MkUserName
class="username"
:user="appearNote.reply.user"
></MkUserName>
<Mfm
class="summary"
:text="getNoteSummary(appearNote.reply)"
:plain="true"
:nowrap="true"
:custom-emojis="note.emojis"
/>
</div>
</div>
<article
class="article"
@contextmenu.stop="onContextmenu"
@click="noteClick"
:style="{
cursor: expandOnNoteClick && !detailedView ? 'pointer' : '',
}"
>
<div class="main">
<div class="header-container">
<MkAvatar class="avatar" :user="appearNote.user" />
<XNoteHeader class="header" :note="appearNote" />
</div>
<div class="body">
<MkSubNoteContent
class="text"
:note="appearNote"
:detailed="true"
:detailedView="detailedView"
:parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
@expanded="(e) => setPostExpanded(e)"
></MkSubNoteContent>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini />
<div v-else class="translated">
<b
>{{
i18n.t("translatedFrom", {
x: translation.sourceLang,
})
}}:
</b>
<Mfm
:text="translation.text"
:author="appearNote.user"
:i="$i"
:custom-emojis="appearNote.emojis"
/>
</div>
</div>
</div>
<div v-if="detailedView" class="info">
<MkA class="created-at" :to="notePage(appearNote)">
<MkTime :time="appearNote.createdAt" mode="absolute" />
</MkA>
</div>
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer
v-if="enableEmojiReactions"
ref="reactionsViewer"
:note="appearNote"
/>
<button
v-tooltip.noDelay.bottom="i18n.ts.reply"
class="button _button"
@click="reply()"
>
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<template
v-if="appearNote.repliesCount > 0 && !detailedView"
>
<p class="count">{{ appearNote.repliesCount }}</p>
</template>
</button>
<XRenoteButton
ref="renoteButton"
class="button"
:note="appearNote"
:count="appearNote.renoteCount"
:detailedView="detailedView"
/>
<XStarButtonNoEmoji
v-if="!enableEmojiReactions"
class="button"
:note="appearNote"
:count="
Object.values(appearNote.reactions).reduce(
(partialSum, val) => partialSum + val,
0
)
"
:reacted="appearNote.myReaction != null"
/>
<XStarButton
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="starButton"
class="button"
:note="appearNote"
/>
<button
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.reaction"
class="button _button"
@click="react()"
>
<i class="ph-smiley ph-bold ph-lg"></i>
</button>
<button
v-if="
enableEmojiReactions &&
appearNote.myReaction != null
"
ref="reactButton"
class="button _button reacted"
@click="undoReact(appearNote)"
v-tooltip.noDelay.bottom="i18n.ts.removeReaction"
>
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<XQuoteButton class="button" :note="appearNote" />
<button
ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more"
class="button _button"
@click="menu()"
>
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button>
</footer>
</div>
</article>
</div>
<button v-else class="muted _button" @click="muted.muted = false">
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
<template #name>
<MkA
v-user-preview="note.userId"
class="name"
:to="userPage(note.user)"
>
<MkUserName :user="note.user" />
</MkA>
</template>
<template #reason>
<b class="_blur_text">{{ muted.matched.join(", ") }}</b>
</template>
</I18n>
</button>
</template>
<script lang="ts" setup>
import type { Ref } from "vue";
import { computed, inject, onMounted, ref, toRaw } from "vue";
import * as mfm from "mfm-js";
import type * as misskey from "calckey-js";
import MkNoteSub from "@/components/MkNoteSub.vue";
import MkSubNoteContent from "./MkSubNoteContent.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue";
import MkVisibility from "@/components/MkVisibility.vue";
import copyToClipboard from "@/scripts/copy-to-clipboard";
import { url } from "@/config";
import { pleaseLogin } from "@/scripts/please-login";
import { focusNext, focusPrev } from "@/scripts/focus";
import { getWordSoftMute } from "@/scripts/check-word-mute";
import { useRouter } from "@/router";
import { userPage } from "@/filters/user";
import * as os from "@/os";
import { defaultStore, noteViewInterruptors } from "@/store";
import { reactionPicker } from "@/scripts/reaction-picker";
import { $i } from "@/account";
import { i18n } from "@/i18n";
import { getNoteMenu } from "@/scripts/get-note-menu";
import { useNoteCapture } from "@/scripts/use-note-capture";
import { notePage } from "@/filters/note";
import { getNoteSummary } from "@/scripts/get-note-summary";
import {
magEffectiveNote,
magIsRenote,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
const router = useRouter();
const props = defineProps<{
note: misskey.entities.Note;
pinned?: boolean;
detailedView?: boolean;
collapsedReply?: boolean;
}>();
let note = $ref<misskey.entities.Note>(structuredClone(toRaw(props.note)));
const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason;
if (what === "reply") return i18n.ts.userSaysSomethingReasonReply;
if (what === "renote") return i18n.ts.userSaysSomethingReasonRenote;
if (what === "quote") return i18n.ts.userSaysSomethingReasonQuote;
// I don't think here is reachable, but just in case
return i18n.ts.userSaysSomething;
};
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result = structuredClone(toRaw(note));
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
note = result;
});
}
const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const renoteTime = ref<HTMLElement>();
const reactButton = ref<HTMLElement>();
let appearNote = $computed(
() => magEffectiveNote(note) as misskey.entities.Note
);
const isMyRenote = $i && $i.id === note.userId;
const showContent = ref(false);
const isDeleted = ref(false);
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));
const translation = ref(null);
const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const keymap = {
r: () => reply(true),
"e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true),
"up|k": focusBefore,
"down|j": focusAfter,
esc: blur,
"m|o": () => menu(true),
s: () => showContent.value !== showContent.value,
};
useNoteCapture({
rootEl: el,
note: $$(appearNote),
isDeletedRef: isDeleted,
});
function reply(viaKeyboard = false): void {
pleaseLogin();
os.post(
{
reply: appearNote,
animation: !viaKeyboard,
},
() => {
focus();
}
);
}
function react(viaKeyboard = false): void {
pleaseLogin();
blur();
reactionPicker.show(
reactButton.value,
(reaction) => {
os.api("notes/reactions/create", {
noteId: appearNote.id,
reaction: magReactionToLegacy(reaction),
});
},
() => {
focus();
}
);
}
function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api("notes/reactions/delete", {
noteId: note.id,
});
}
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>(
"currentClipPage",
null
);
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === "A") return true;
// The Audio element's context menu is the browser default, such as for selecting playback speed.
if (el.tagName === "AUDIO") return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
os.contextMenu(
[
{
type: "label",
text: notePage(appearNote),
},
{
icon: "ph-browser ph-bold ph-lg",
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(notePage(appearNote));
},
},
notePage(appearNote) != location.pathname
? {
icon: "ph-arrows-out-simple ph-bold ph-lg",
text: i18n.ts.showInPage,
action: () => {
router.push(notePage(appearNote), "forcePage");
},
}
: undefined,
null,
{
type: "a",
icon: "ph-arrow-square-out ph-bold ph-lg",
text: i18n.ts.openInNewTab,
href: notePage(appearNote),
target: "_blank",
},
{
icon: "ph-link-simple ph-bold ph-lg",
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(`${url}${notePage(appearNote)}`);
},
},
appearNote.user.host != null
? {
type: "a",
icon: "ph-arrow-square-up-right ph-bold ph-lg",
text: i18n.ts.showOnRemote,
href: appearNote.url ?? appearNote.uri ?? "",
target: "_blank",
}
: undefined,
],
ev
);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(
getNoteMenu({
note: note,
translating,
translation,
menuButton,
isDeleted,
currentClipPage,
}),
menuButton.value,
{
viaKeyboard,
}
).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
os.popupMenu(
[
{
text: i18n.ts.unrenote,
icon: "ph-trash ph-bold ph-lg",
danger: true,
action: () => {
os.api("notes/delete", {
noteId: note.id,
});
isDeleted.value = true;
},
},
],
renoteTime.value,
{
viaKeyboard: viaKeyboard,
}
);
}
function focus() {
el.value.focus();
}
function blur() {
el.value.blur();
}
function focusBefore() {
focusPrev(el.value);
}
function focusAfter() {
focusNext(el.value);
}
function scrollIntoView() {
el.value.scrollIntoView();
}
function noteClick(e) {
if (
document.getSelection().type === "Range" ||
props.detailedView ||
!expandOnNoteClick
) {
e.stopPropagation();
} else {
router.push(notePage(appearNote));
}
}
function readPromo() {
os.api("promo/read", {
noteId: appearNote.id,
});
isDeleted.value = true;
}
let postIsExpanded = ref(false);
function setPostExpanded(val: boolean) {
postIsExpanded.value = val;
}
const accessibleLabel = computed(() => {
let label = `${appearNote.user.username}; `;
if (appearNote.renote) {
label += `${i18n.t("renoted")} ${appearNote.renote.user.username}; `;
if (appearNote.renote.cw) {
label += `${i18n.t("cw")}: ${appearNote.renote.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.renote.text}; `;
}
} else {
label += `${appearNote.renote.text}; `;
}
} else {
if (appearNote.cw) {
label += `${i18n.t("cw")}: ${appearNote.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.text}; `;
}
} else {
label += `${appearNote.text}; `;
}
}
const date = new Date(appearNote.createdAt);
label += `${date.toLocaleTimeString()}`;
return label;
});
defineExpose({
focus,
blur,
scrollIntoView,
});
</script>
<style lang="scss" scoped>
.tkcbzcuz {
position: relative;
transition: box-shadow 0.1s ease;
font-size: 1.05em;
overflow: clip;
contain: content;
-webkit-tap-highlight-color: transparent;
//
//
// contain-intrinsic-size
//
// ()
//content-visibility: auto;
//contain-intrinsic-size: 0 128px;
&:focus-visible {
outline: none;
&:after {
content: "";
pointer-events: none;
display: block;
position: absolute;
z-index: 10;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
border: solid 1px var(--focus);
border-radius: var(--radius);
box-sizing: border-box;
}
}
& > .article > .main {
&:hover,
&:focus-within {
:deep(.footer .button) {
opacity: 1;
}
}
}
> .reply-to {
& + .note-context {
.line::before {
content: "";
display: block;
margin-bottom: -4px;
margin-top: 16px;
border-left: 2px solid currentColor;
margin-left: calc((var(--avatarSize) / 2) - 1px);
opacity: 0.25;
}
}
}
.note-context {
position: relative;
padding: 0 32px 0 32px;
display: flex;
z-index: 1;
&:first-child {
margin-top: 20px;
}
> :not(.line) {
width: 0;
flex-grow: 1;
position: relative;
line-height: 28px;
}
> .line {
position: relative;
z-index: 2;
width: var(--avatarSize);
display: flex;
margin-right: 14px;
margin-top: 0;
flex-grow: 0;
pointer-events: none;
}
> div > i {
margin-left: -0.5px;
}
> .info {
display: flex;
align-items: center;
font-size: 90%;
white-space: pre;
color: #f6c177;
> i {
margin-right: 4px;
}
> .hide {
margin-left: auto;
color: inherit;
}
}
> .renote {
display: flex;
align-items: center;
white-space: pre;
color: var(--renote);
cursor: pointer;
> i {
margin-right: 4px;
}
> span {
overflow: hidden;
flex-shrink: 1;
text-overflow: ellipsis;
white-space: nowrap;
> .name {
font-weight: bold;
}
}
> .info {
margin-left: auto;
font-size: 0.9em;
display: flex;
> .time {
flex-shrink: 0;
color: inherit;
display: inline-flex;
align-items: center;
> .dropdownIcon {
margin-right: 4px;
}
}
}
}
&.collapsedReply {
.line {
opacity: 0.25;
&::after {
content: "";
position: absolute;
border-left: 2px solid currentColor;
border-top: 2px solid currentColor;
margin-left: calc(var(--avatarSize) / 2 - 1px);
width: calc(var(--avatarSize) / 2 + 14px);
border-top-left-radius: calc(var(--avatarSize) / 4);
top: calc(50% - 1px);
height: calc(50% + 5px);
}
}
.info {
color: var(--fgTransparentWeak);
transition: color 0.2s;
}
.avatar {
width: 1.2em;
height: 1.2em;
border-radius: 2em;
overflow: hidden;
margin-right: 0.4em;
background: var(--panelHighlight);
}
.username {
font-weight: 700;
flex-shrink: 0;
max-width: 30%;
&::after {
content: ": ";
}
}
&:hover,
&:focus-within {
.info {
color: var(--fg);
}
}
}
}
> .article {
position: relative;
overflow: clip;
padding: 20px 32px 10px;
margin-top: -16px;
&:first-child,
&:nth-child(2) {
margin-top: -100px;
padding-top: 104px;
}
@media (pointer: coarse) {
cursor: default;
}
.header-container {
display: flex;
position: relative;
z-index: 2;
> .avatar {
flex-shrink: 0;
display: block;
margin: 0 14px 0 0;
width: var(--avatarSize);
height: var(--avatarSize);
position: relative;
top: 0;
left: 0;
}
> .header {
width: 0;
flex-grow: 1;
}
}
> .main {
flex: 1;
min-width: 0;
> .body {
margin-top: 0.7em;
> .translation {
border: solid 0.5px var(--divider);
border-radius: var(--radius);
padding: 12px;
margin-top: 8px;
}
> .renote {
padding-top: 8px;
> * {
padding: 16px;
border: solid 1px var(--renote);
border-radius: 8px;
transition: background 0.2s;
&:hover,
&:focus-within {
background-color: var(--panelHighlight);
}
}
}
}
> .info {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.7em;
margin-top: 16px;
opacity: 0.7;
font-size: 0.9em;
}
> .footer {
position: relative;
z-index: 2;
display: flex;
flex-wrap: wrap;
pointer-events: none; // Allow clicking anything w/out pointer-events: all; to open post
margin-top: 0.4em;
> :deep(.button) {
position: relative;
margin: 0;
padding: 8px;
opacity: 0.7;
flex-grow: 1;
max-width: 3.5em;
width: max-content;
min-width: max-content;
pointer-events: all;
height: auto;
transition: opacity 0.2s;
&::before {
content: "";
position: absolute;
inset: 0;
bottom: 2px;
background: var(--panel);
z-index: -1;
transition: background 0.2s;
}
&:first-of-type {
margin-left: -0.5em;
&::before {
border-radius: 100px 0 0 100px;
}
}
&:last-of-type {
&::before {
border-radius: 0 100px 100px 0;
}
}
&:hover {
color: var(--fgHighlighted);
}
> i {
display: inline !important;
}
> .count {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
&.reacted {
color: var(--accent);
}
}
}
}
}
> .reply {
border-top: solid 0.5px var(--divider);
}
&.max-width_500px {
font-size: 0.975em;
--avatarSize: 46px;
padding-top: 6px;
> .note-context {
padding-inline: 16px;
margin-top: 8px;
> :not(.line) {
margin-top: 0px;
}
> .line {
margin-right: 10px;
&::before {
margin-top: 8px;
}
}
}
> .article {
padding: 18px 16px 8px;
&:first-child,
&:nth-child(2) {
padding-top: 104px;
}
> .main > .header-container > .avatar {
margin-right: 10px;
// top: calc(14px + var(--stickyTop, 0px));
}
}
}
&.max-width_300px {
--avatarSize: 40px;
}
}
.muted {
padding: 8px;
text-align: center;
opacity: 0.7;
width: 100%;
._blur_text {
pointer-events: auto;
}
}
</style>

View File

@ -23,10 +23,10 @@
:ad="true" :ad="true"
class="notes" class="notes"
> >
<XNote <XNoteResolvingProxy
:key="note._featuredId_ || note._prId_ || note.id" :key="note._featuredId_ || note._prId_ || note.id"
class="qtqtichx" class="qtqtichx"
:note="note" :note="note.id"
/> />
</XList> </XList>
</div> </div>
@ -37,9 +37,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import type { Paging } from "@/components/MkPagination.vue"; import type { Paging } from "@/components/MkPagination.vue";
import XNote from "@/components/MkNote.vue";
import XList from "@/components/MkDateSeparatedList.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import XNoteResolvingProxy from "@/components/MagNoteResolvingProxy.vue";
import XList from "@/components/MkDateSeparatedList.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const props = defineProps<{ const props = defineProps<{

View File

@ -124,12 +124,12 @@
v-if="notification.type === 'renote'" v-if="notification.type === 'renote'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="getNoteSummary(notification.note.renote)" :title="getNoteSummary(notification.note.renote!)"
> >
<span>{{ i18n.ts._notification.renoted }}</span> <span>{{ i18n.ts._notification.renoted }}</span>
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
<Mfm <Mfm
:text="getNoteSummary(notification.note.renote)" :text="getNoteSummary(notification.note.renote!)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.renote.emojis" :custom-emojis="notification.note.renote.emojis"

View File

@ -18,14 +18,14 @@
:items="notifications" :items="notifications"
:no-gap="true" :no-gap="true"
> >
<XNote <XNoteResolvingProxy
v-if=" v-if="
['reply', 'quote', 'mention'].includes( ['reply', 'quote', 'mention'].includes(
notification.type notification.type
) )
" "
:key="notification.id" :key="notification.id"
:note="notification.note" :note="notification.note.id"
:collapsedReply=" :collapsedReply="
notification.type === 'reply' || notification.type === 'reply' ||
(notification.type === 'mention' && (notification.type === 'mention' &&
@ -46,20 +46,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { computed, onMounted, onUnmounted, ref } from "vue";
defineComponent,
markRaw,
onUnmounted,
onMounted,
computed,
ref,
} from "vue";
import { notificationTypes } from "calckey-js"; import { notificationTypes } from "calckey-js";
import MkPagination, { Paging } from "@/components/MkPagination.vue"; import MkPagination, { Paging } from "@/components/MkPagination.vue";
import XNotification from "@/components/MkNotification.vue"; import XNotification from "@/components/MkNotification.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MkNote.vue"; import XNoteResolvingProxy from "@/components/MagNoteResolvingProxy.vue";
import * as os from "@/os";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -78,7 +70,7 @@ const pagination: Paging = {
includeTypes: props.includeTypes ?? undefined, includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes excludeTypes: props.includeTypes
? undefined ? undefined
: $i.mutingNotificationTypes, : $i?.mutingNotificationTypes,
unreadOnly: props.unreadOnly, unreadOnly: props.unreadOnly,
})), })),
}; };
@ -86,7 +78,7 @@ const pagination: Paging = {
const onNotification = (notification) => { const onNotification = (notification) => {
const isMuted = props.includeTypes const isMuted = props.includeTypes
? !props.includeTypes.includes(notification.type) ? !props.includeTypes.includes(notification.type)
: $i.mutingNotificationTypes.includes(notification.type); : $i?.mutingNotificationTypes?.includes(notification.type);
if (isMuted || document.visibilityState === "visible") { if (isMuted || document.visibilityState === "visible") {
stream.send("readNotification", { stream.send("readNotification", {
id: notification.id, id: notification.id,

View File

@ -231,7 +231,7 @@ import insertTextAtCursor from "insert-text-at-cursor";
import { length } from "stringz"; import { length } from "stringz";
import { toASCII } from "punycode/"; import { toASCII } from "punycode/";
import * as Acct from "calckey-js/built/acct"; import * as Acct from "calckey-js/built/acct";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MagNoteSimple.vue";
import XNotePreview from "@/components/MkNotePreview.vue"; import XNotePreview from "@/components/MkNotePreview.vue";
import XPostFormAttaches from "@/components/MkPostFormAttaches.vue"; import XPostFormAttaches from "@/components/MkPostFormAttaches.vue";
import XPollEditor from "@/components/MkPollEditor.vue"; import XPollEditor from "@/components/MkPollEditor.vue";
@ -255,18 +255,14 @@ import { uploadFile } from "@/scripts/upload";
import XCheatSheet from "@/components/MkCheatSheetDialog.vue"; import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
import { preprocess } from "@/scripts/preprocess"; import { preprocess } from "@/scripts/preprocess";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
import { import { magLegacyVisibility } from "@/scripts-mag/mag-util";
magLegacyVisibility,
magTransMap,
magTransProperty,
} from "@/scripts-mag/mag-util";
const modal = inject("modal"); const modal = inject("modal");
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
reply?: packed.PackNoteMaybeFull | misskey.entities.Note; reply?: packed.PackNoteMaybeFull;
renote?: packed.PackNoteMaybeFull | misskey.entities.Note; renote?: packed.PackNoteMaybeFull;
mention?: misskey.entities.User; mention?: misskey.entities.User;
specified?: misskey.entities.User; specified?: misskey.entities.User;
initialText?: string; initialText?: string;
@ -277,7 +273,7 @@ const props = withDefaults(
)[]; )[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: misskey.entities.User[]; initialVisibleUsers?: misskey.entities.User[];
initialNote?: packed.PackNoteMaybeFull | misskey.entities.Note; initialNote?: packed.PackNoteMaybeFull;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;
@ -332,7 +328,7 @@ if (props.initialVisibleUsers) {
} }
let autocomplete = $ref(null); let autocomplete = $ref(null);
let draghover = $ref(false); let draghover = $ref(false);
let quoteId = $ref(null); let quoteId = $ref<string | null>(null);
let hasNotSpecifiedMentions = $ref(false); let hasNotSpecifiedMentions = $ref(false);
let recentHashtags = $ref(JSON.parse(localStorage.getItem("hashtags") || "[]")); let recentHashtags = $ref(JSON.parse(localStorage.getItem("hashtags") || "[]"));
let imeText = $ref(""); let imeText = $ref("");
@ -483,11 +479,7 @@ if (
visibility = magLegacyVisibility(props.reply.visibility); visibility = magLegacyVisibility(props.reply.visibility);
} }
if (visibility === "specified") { if (visibility === "specified") {
const ids = magTransProperty( const ids = props.reply.visible_user_ids;
props.reply,
"visible_user_ids",
"visibleUserIds"
);
if (ids) { if (ids) {
os.api("users/show", { os.api("users/show", {
userIds: ids.filter( userIds: ids.filter(
@ -730,7 +722,9 @@ async function onPaste(ev: ClipboardEvent) {
return; return;
} }
quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; quoteId = paste
.substring(url.length)
.match(/^\/notes\/(.+?)\/?$/)[1];
}); });
} }
} }
@ -996,38 +990,21 @@ onMounted(() => {
const init = props.initialNote; const init = props.initialNote;
text = init.text ? init.text : ""; text = init.text ? init.text : "";
files = magTransProperty(init, "attachments", "files") ?? []; files = init.attachments ?? [];
cw = init.cw; cw = init.cw;
useCw = init.cw != null; useCw = init.cw != null;
if (init.poll) { if (init.poll) {
poll = { poll = {
choices: magTransMap( choices: init.poll.options.map((x) => x.title),
init.poll, multiple: init.poll.multiple_choice,
"options", expiresAt: init.poll.expires_at,
"choices",
(a) => a.map((x) => x.title),
(b) => b.map((x) => x.text)
),
multiple: magTransProperty(
init.poll,
"multiple_choice",
"multiple"
),
expiresAt: magTransProperty(
init.poll,
"expires_at",
"expiresAt"
),
// TODO(Natty) // TODO(Natty)
expiredAfter: null, expiredAfter: null,
}; };
} }
visibility = magLegacyVisibility(init.visibility); visibility = magLegacyVisibility(init.visibility);
localOnly = localOnly = init.local_only ?? false;
magTransProperty(init, "local_only", "localOnly") ?? false; quoteId = init.renoted_note ? init.renoted_note.id : null;
quoteId = magTransProperty(init, "renote", "renoted_note")
? magTransProperty(init, "renote", "renoted_note")!.id
: null;
} }
nextTick(() => watchForDraft()); nextTick(() => watchForDraft());

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="tdflqwzn" :class="{ isMe }"> <div class="tdflqwzn" :class="{ isMe }">
<XReaction <XReaction
v-for="r in reactions" v-for="r in note.reactions"
:key="magReactionToLegacy(r[0])" :key="magReactionToLegacy(r[0])"
:reaction="r[0]" :reaction="r[0]"
:count="r[1]" :count="r[1]"
@ -13,54 +13,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import * as misskey from "calckey-js";
import { $i } from "@/account"; import { $i } from "@/account";
import XReaction from "@/components/MkReactionsViewer.reaction.vue"; import XReaction from "@/components/MkReactionsViewer.reaction.vue";
import { packed, types } from "magnetar-common"; import { packed } from "magnetar-common";
import { import {
magConvertReaction, magConvertReaction,
magReactionEquals,
magReactionToLegacy, magReactionToLegacy,
} from "@/scripts-mag/mag-util"; } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteMaybeFull | misskey.entities.Note; note: packed.PackNoteMaybeFull;
}>(); }>();
const reactions = computed(() => {
const myReactionMk = (props.note as misskey.entities.Note).myReaction
? magConvertReaction((props.note as misskey.entities.Note).myReaction!)
: null;
return Array.isArray(props.note.reactions)
? props.note.reactions
: Object.entries(props.note.reactions)
.filter(([e]) => e)
.map(([e, cnt]) => {
const parsed = magConvertReaction(
e,
(name, host) =>
(
props.note
.emojis as misskey.entities.Note["emojis"]
).find((e) =>
magReactionEquals(
magConvertReaction(`:${e.name}:`),
{ name, host, url: null! }
)
)?.url ?? null!
);
return [
parsed,
cnt,
myReactionMk
? magReactionEquals(myReactionMk, parsed)
: undefined,
] as types.ReactionPair;
});
});
const initialReactions = new Set( const initialReactions = new Set(
Array.isArray(props.note.reactions) Array.isArray(props.note.reactions)
? props.note.reactions.map((v) => magReactionToLegacy(v[0])) ? props.note.reactions.map((v) => magReactionToLegacy(v[0]))

View File

@ -2,7 +2,10 @@ import { markRaw } from "vue";
import { locale } from "@/config"; import { locale } from "@/config";
import { I18n } from "@/scripts/i18n"; import { I18n } from "@/scripts/i18n";
export const i18n = markRaw(new I18n(locale)); // HACK: Type checking the locale
type LocaleMap = { [property: string]: LocaleMap } & string;
export const i18n = markRaw(new I18n<LocaleMap>(locale));
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない // このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
declare module "@vue/runtime-core" { declare module "@vue/runtime-core" {

View File

@ -150,7 +150,6 @@ import MkLink from "@/components/MkLink.vue";
import { physics } from "@/scripts/physics"; import { physics } from "@/scripts/physics";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import * as os from "@/os";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
let easterEggReady = false; let easterEggReady = false;
@ -181,13 +180,6 @@ function gravity() {
easterEggEngine = physics(containerEl); easterEggEngine = physics(containerEl);
} }
function iLoveMisskey() {
os.post({
initialText: "I $[jelly ❤] #Calckey",
instant: true,
});
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (easterEggEngine) { if (easterEggEngine) {
easterEggEngine.stop(); easterEggEngine.stop();

View File

@ -22,7 +22,7 @@
:no-gap="false" :no-gap="false"
:ad="false" :ad="false"
> >
<XNote <XNoteResolvingProxy
:key="item.id" :key="item.id"
:note="item.note" :note="item.note"
:class="$style.note" :class="$style.note"
@ -37,7 +37,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import XNote from "@/components/MkNote.vue"; import XNoteResolvingProxy from "@/components/MagNoteResolvingProxy.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";

View File

@ -39,7 +39,7 @@ import { ref, watch } from "vue";
import XContainer from "../page-editor.container.vue"; import XContainer from "../page-editor.container.vue";
import MkInput from "@/components/form/input.vue"; import MkInput from "@/components/form/input.vue";
import MkSwitch from "@/components/form/switch.vue"; import MkSwitch from "@/components/form/switch.vue";
import XNote from "@/components/MkNote.vue"; import XNote from "@/components/MagNote.vue";
import XNoteDetailed from "@/components/MagNoteDetailed.vue"; import XNoteDetailed from "@/components/MagNoteDetailed.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";

View File

@ -25,11 +25,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import * as misskey from "calckey-js";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
user: misskey.entities.User; user: packed.PackUserBase;
}>(); }>();
const pagination = { const pagination = {

View File

@ -24,12 +24,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import * as misskey from "calckey-js";
import MkUserInfo from "@/components/MkUserInfo.vue"; import MkUserInfo from "@/components/MkUserInfo.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
user: misskey.entities.User; user: packed.PackUserBase;
type: "following" | "followers"; type: "following" | "followers";
}>(); }>();

View File

@ -16,20 +16,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { computed, watch } from "vue";
defineAsyncComponent,
computed,
inject,
onMounted,
onUnmounted,
watch,
} from "vue";
import * as Acct from "calckey-js/built/acct";
import * as misskey from "calckey-js";
import XFollowList from "./follow-list.vue"; import XFollowList from "./follow-list.vue";
import * as os from "@/os"; import * as os from "@/os";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { endpoints, packed } from "magnetar-common";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -38,13 +30,14 @@ const props = withDefaults(
{} {}
); );
let user = $ref<null | misskey.entities.UserDetailed>(null); let user = $ref<null | packed.PackUserBase>(null);
let error = $ref(null); let error = $ref(null);
function fetchUser(): void { function fetchUser(): void {
if (props.acct == null) return; if (!props.acct) return;
user = null; user = null;
os.api("users/show", Acct.parse(props.acct))
os.magApi(endpoints.GetUserByAcct, {}, { user_acct: props.acct })
.then((u) => { .then((u) => {
user = u; user = u;
}) })
@ -66,9 +59,7 @@ definePageMetadata(
user user
? { ? {
icon: "ph-user ph-bold ph-lg", icon: "ph-user ph-bold ph-lg",
title: user.name title: `${user.display_name} (@${user.username})`,
? `${user.name} (@${user.username})`
: `@${user.username}`,
subtitle: i18n.ts.followers, subtitle: i18n.ts.followers,
userName: user, userName: user,
avatar: user, avatar: user,

View File

@ -16,20 +16,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { computed, watch } from "vue";
defineAsyncComponent,
computed,
inject,
onMounted,
onUnmounted,
watch,
} from "vue";
import * as Acct from "calckey-js/built/acct";
import * as misskey from "calckey-js";
import XFollowList from "./follow-list.vue"; import XFollowList from "./follow-list.vue";
import * as os from "@/os"; import * as os from "@/os";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { endpoints, packed } from "magnetar-common";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -38,13 +30,14 @@ const props = withDefaults(
{} {}
); );
let user = $ref<null | misskey.entities.UserDetailed>(null); let user = $ref<null | packed.PackUserBase>(null);
let error = $ref(null); let error = $ref(null);
function fetchUser(): void { function fetchUser(): void {
if (props.acct == null) return; if (!props.acct) return;
user = null; user = null;
os.api("users/show", Acct.parse(props.acct))
os.magApi(endpoints.GetUserByAcct, {}, { user_acct: props.acct })
.then((u) => { .then((u) => {
user = u; user = u;
}) })
@ -66,9 +59,7 @@ definePageMetadata(
user user
? { ? {
icon: "ph-user ph-bold ph-lg", icon: "ph-user ph-bold ph-lg",
title: user.name title: `${user.display_name} (@${user.username})`,
? `${user.name} (@${user.username})`
: `@${user.username}`,
subtitle: i18n.ts.following, subtitle: i18n.ts.following,
userName: user, userName: user,
avatar: user, avatar: user,

View File

@ -15,13 +15,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import * as misskey from "calckey-js";
import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue"; import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import { packed } from "magnetar-common";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
user: misskey.entities.User; user: packed.PackUserBase;
}>(), }>(),
{} {}
); );

View File

@ -9,13 +9,13 @@
<div class="main"> <div class="main">
<div class="profile"> <div class="profile">
<MkMoved <MkMoved
v-if="user.movedToUri" v-if="user.moved_to"
:host="user.movedToUri.host" :host="user.moved_to.host"
:acct="user.movedToUri.username" :acct="user.moved_to.username"
/> />
<MkRemoteCaution <MkRemoteCaution
v-if="user.host != null" v-if="user.host != null"
:href="user.url" :href="user.url!"
class="warn" class="warn"
/> />
@ -36,7 +36,7 @@
/> />
<div v-if="$i?.isModerator || $i?.isAdmin"> <div v-if="$i?.isModerator || $i?.isAdmin">
<span <span
v-if="user.isSilenced" v-if="user.is_silenced"
style=" style="
color: var(--warn); color: var(--warn);
padding: 5px; padding: 5px;
@ -48,7 +48,7 @@
{{ i18n.ts.silenced }} {{ i18n.ts.silenced }}
</span> </span>
<span <span
v-if="user.isSuspended" v-if="user.is_suspended"
style=" style="
color: var(--error); color: var(--error);
padding: 5px; padding: 5px;
@ -64,7 +64,7 @@
v-if=" v-if="
$i && $i &&
$i.id != user.id && $i.id != user.id &&
user.isFollowed user.follows_you
" "
class="followed" class="followed"
>{{ i18n.ts.followsYou }}</span >{{ i18n.ts.followsYou }}</span
@ -73,7 +73,7 @@
v-if=" v-if="
$i && $i &&
$i.id != user.id && $i.id != user.id &&
user.hasPendingFollowRequestToYou user.they_request_follow
" "
class="followed" class="followed"
>{{ i18n.ts.followRequestYou }}: >{{ i18n.ts.followRequestYou }}:
@ -89,7 +89,7 @@
><MkAcct :user="user" :detail="true" ><MkAcct :user="user" :detail="true"
/></span> /></span>
<span <span
v-if="user.isAdmin" v-if="user.is_admin"
v-tooltip.noDelay="i18n.ts.isAdmin" v-tooltip.noDelay="i18n.ts.isAdmin"
style="color: var(--badge)" style="color: var(--badge)"
><i ><i
@ -97,7 +97,9 @@
></i ></i
></span> ></span>
<span <span
v-if="!user.isAdmin && user.isModerator" v-if="
!user.is_admin && user.is_moderator
"
v-tooltip.noDelay="i18n.ts.isModerator" v-tooltip.noDelay="i18n.ts.isModerator"
style="color: var(--badge)" style="color: var(--badge)"
><i ><i
@ -105,12 +107,12 @@
></i ></i
></span> ></span>
<span <span
v-if="user.isLocked" v-if="user.is_locked"
v-tooltip.noDelay="i18n.ts.isLocked" v-tooltip.noDelay="i18n.ts.isLocked"
><i class="ph-lock ph-bold ph-lg"></i ><i class="ph-lock ph-bold ph-lg"></i
></span> ></span>
<span <span
v-if="user.isBot" v-if="user.is_bot"
v-tooltip.noDelay="i18n.ts.isBot" v-tooltip.noDelay="i18n.ts.isBot"
><i class="ph-robot ph-bold ph-lg"></i ><i class="ph-robot ph-bold ph-lg"></i
></span> ></span>
@ -134,7 +136,7 @@
v-if=" v-if="
$i && $i &&
$i.id != user.id && $i.id != user.id &&
user.isFollowed user.you_follow
" "
class="followed" class="followed"
>{{ i18n.ts.followsYou }}</span >{{ i18n.ts.followsYou }}</span
@ -143,7 +145,7 @@
v-if=" v-if="
$i && $i &&
$i.id != user.id && $i.id != user.id &&
user.hasPendingFollowRequestToYou user.they_request_follow
" "
class="followed" class="followed"
> >
@ -156,14 +158,14 @@
</span> </span>
<div v-if="$i?.isModerator || $i?.isAdmin"> <div v-if="$i?.isModerator || $i?.isAdmin">
<span <span
v-if="user.isSilenced" v-if="user.is_silenced"
style="color: var(--warn); padding: 5px" style="color: var(--warn); padding: 5px"
> >
<i class="ph-warning ph-bold ph-lg"></i> <i class="ph-warning ph-bold ph-lg"></i>
{{ i18n.ts.silenced }} {{ i18n.ts.silenced }}
</span> </span>
<span <span
v-if="user.isSuspended" v-if="user.is_suspended"
style=" style="
color: var(--error); color: var(--error);
padding: 5px; padding: 5px;
@ -179,7 +181,7 @@
><MkAcct :user="user" :detail="true" ><MkAcct :user="user" :detail="true"
/></span> /></span>
<span <span
v-if="user.isAdmin" v-if="user.is_admin"
v-tooltip.noDelay="i18n.ts.isAdmin" v-tooltip.noDelay="i18n.ts.isAdmin"
style="color: var(--badge)" style="color: var(--badge)"
><i ><i
@ -187,7 +189,7 @@
></i ></i
></span> ></span>
<span <span
v-if="!user.isAdmin && user.isModerator" v-if="!user.is_admin && user.is_moderator"
v-tooltip.noDelay="i18n.ts.isModerator" v-tooltip.noDelay="i18n.ts.isModerator"
style=" style="
color: var(--badge); color: var(--badge);
@ -198,12 +200,12 @@
></i ></i
></span> ></span>
<span <span
v-if="user.isLocked" v-if="user.is_locked"
v-tooltip.noDelay="i18n.ts.isLocked" v-tooltip.noDelay="i18n.ts.isLocked"
><i class="ph-lock ph-bold ph-lg"></i ><i class="ph-lock ph-bold ph-lg"></i
></span> ></span>
<span <span
v-if="user.isBot" v-if="user.is_bot"
v-tooltip.noDelay="i18n.ts.isBot" v-tooltip.noDelay="i18n.ts.isBot"
><i class="ph-robot ph-bold ph-lg"></i ><i class="ph-robot ph-bold ph-lg"></i
></span> ></span>
@ -272,14 +274,14 @@
<dd class="value"> <dd class="value">
{{ {{
new Date( new Date(
user.createdAt user.created_at
).toLocaleString() ).toLocaleString()
}} }}
(<MkTime :time="user.createdAt" />) (<MkTime :time="user.created_at" />)
</dd> </dd>
</dl> </dl>
</div> </div>
<div v-if="user.fields.length > 0" class="fields"> <div v-if="user.fields?.length > 0" class="fields">
<dl <dl
v-for="(field, i) in user.fields" v-for="(field, i) in user.fields"
:key="i" :key="i"
@ -295,6 +297,7 @@
</dt> </dt>
<dd class="value"> <dd class="value">
<Mfm <Mfm
:mm="field.value_mm ?? undefined"
:text="field.value" :text="field.value"
:author="user" :author="user"
:i="$i" :i="$i"
@ -310,7 +313,7 @@
:to="userPage(user)" :to="userPage(user)"
:class="{ active: page === 'index' }" :class="{ active: page === 'index' }"
> >
<b>{{ number(user.notesCount) }}</b> <b>{{ number(user.note_count) }}</b>
<span>{{ i18n.ts.notes }}</span> <span>{{ i18n.ts.notes }}</span>
</MkA> </MkA>
<MkA <MkA
@ -318,7 +321,7 @@
:to="userPage(user, 'following')" :to="userPage(user, 'following')"
:class="{ active: page === 'following' }" :class="{ active: page === 'following' }"
> >
<b>{{ number(user.followingCount) }}</b> <b>{{ number(user.following_count) }}</b>
<span>{{ i18n.ts.following }}</span> <span>{{ i18n.ts.following }}</span>
</MkA> </MkA>
<MkA <MkA
@ -326,7 +329,7 @@
:to="userPage(user, 'followers')" :to="userPage(user, 'followers')"
:class="{ active: page === 'followers' }" :class="{ active: page === 'followers' }"
> >
<b>{{ number(user.followersCount) }}</b> <b>{{ number(user.follower_count) }}</b>
<span>{{ i18n.ts.followers }}</span> <span>{{ i18n.ts.followers }}</span>
</MkA> </MkA>
</div> </div>
@ -334,9 +337,9 @@
</div> </div>
<div class="contents _gap"> <div class="contents _gap">
<div v-if="user.pinnedNotes.length > 0" class="_gap"> <div v-if="user.pinned_notes?.length > 0" class="_gap">
<XNote <XNote
v-for="note in user.pinnedNotes" v-for="note in user.pinned_notes"
:key="note.id" :key="note.id"
class="note _block" class="note _block"
:note="note" :note="note"
@ -375,16 +378,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onMounted, onUnmounted } from "vue"; import { defineAsyncComponent, onMounted, onUnmounted } from "vue";
import number from "@/filters/number";
import calcAge from "s-age"; import calcAge from "s-age";
import cityTimezones from "city-timezones"; import cityTimezones from "city-timezones";
import XUserTimeline from "./index.timeline.vue"; import XUserTimeline from "./index.timeline.vue";
import type * as misskey from "calckey-js";
import XNote from "@/components/MkNote.vue";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import MkRemoteCaution from "@/components/MkRemoteCaution.vue"; import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
import MkInfo from "@/components/MkInfo.vue"; import MkInfo from "@/components/MkInfo.vue";
import MkMoved from "@/components/MkMoved.vue"; import MkMoved from "@/components/MkMoved.vue";
import XNote from "@/components/MagNote.vue";
import { getScrollPosition } from "@/scripts/scroll"; import { getScrollPosition } from "@/scripts/scroll";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -396,6 +397,8 @@ import Mfm from "@/components/mfm.vue";
import MkTime from "@/components/global/MkTime.vue"; import MkTime from "@/components/global/MkTime.vue";
import MkA from "@/components/global/MkA.vue"; import MkA from "@/components/global/MkA.vue";
import page from "@/components/page/page.vue"; import page from "@/components/page/page.vue";
import { packed } from "magnetar-common";
import number from "../../filters/number";
const XPhotos = defineAsyncComponent(() => import("./index.photos.vue")); const XPhotos = defineAsyncComponent(() => import("./index.photos.vue"));
const XActivity = defineAsyncComponent(() => import("./index.activity.vue")); const XActivity = defineAsyncComponent(() => import("./index.activity.vue"));
@ -403,7 +406,7 @@ const XActivity = defineAsyncComponent(() => import("./index.activity.vue"));
const emit = defineEmits(["refresh"]); const emit = defineEmits(["refresh"]);
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
user: misskey.entities.UserDetailed; user: packed.PackUserMaybeAll;
}>(), }>(),
{} {}
); );
@ -414,9 +417,9 @@ let rootEl = $ref<null | HTMLElement>(null);
let bannerEl = $ref<null | HTMLElement>(null); let bannerEl = $ref<null | HTMLElement>(null);
const style = $computed(() => { const style = $computed(() => {
if (props.user.bannerUrl == null) return {}; if (props.user.banner_url == null) return {};
return { return {
backgroundImage: `url(${props.user.bannerUrl})`, backgroundImage: `url(${props.user.banner_url})`,
}; };
}); });

View File

@ -29,16 +29,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue";
import * as misskey from "calckey-js";
import MkContainer from "@/components/MkContainer.vue"; import MkContainer from "@/components/MkContainer.vue";
import MkChart from "@/components/MkChart.vue"; import MkChart from "@/components/MkChart.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
user: misskey.entities.User; user: packed.PackUserBase;
limit?: number; limit?: number;
}>(), }>(),
{ {

View File

@ -37,9 +37,10 @@ import MkContainer from "@/components/MkContainer.vue";
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue"; import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
user: misskey.entities.UserDetailed; user: packed.PackUserBase;
}>(); }>();
let fetching = $ref(true); let fetching = $ref(true);

View File

@ -12,15 +12,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from "vue"; import { computed, ref } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import XNotes from "@/components/MkNotes.vue"; import XNotes from "@/components/MkNotes.vue";
import MkTab from "@/components/MkTab.vue"; import MkTab from "@/components/MkTab.vue";
import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
user: misskey.entities.UserDetailed; user: packed.PackUserBase | misskey.entities.UserDetailed;
}>(); }>();
const include = ref<string | null>(null); const include = ref<string | null>(null);

View File

@ -29,18 +29,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, computed, watch } from "vue"; import { computed, defineAsyncComponent, watch } from "vue";
import calcAge from "s-age";
import * as Acct from "calckey-js/built/acct";
import type * as misskey from "calckey-js";
import { getScrollPosition } from "@/scripts/scroll";
import number from "@/filters/number";
import { userPage, acct as getAcct } from "@/filters/user";
import * as os from "@/os"; import * as os from "@/os";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { $i } from "@/account"; import { $i } from "@/account";
import { endpoints, MagApiError, packed } from "magnetar-common";
import * as Acct from "calckey-js/built/acct";
const XHome = defineAsyncComponent(() => import("./home.vue")); const XHome = defineAsyncComponent(() => import("./home.vue"));
const XReactions = defineAsyncComponent(() => import("./reactions.vue")); const XReactions = defineAsyncComponent(() => import("./reactions.vue"));
@ -61,24 +57,62 @@ const props = withDefaults(
const router = useRouter(); const router = useRouter();
let tab = $ref(props.page); let tab = $ref(props.page);
let user = $ref<null | misskey.entities.UserDetailed>(null); let user = $ref<null | packed.PackUserSelfMaybeAll>(null);
let error = $ref(null); let error = $ref<any>(null);
function fetchUser(): void { function fetchUser(refetch: boolean = true): void {
if (props.acct == null) return; if (!props.acct) return;
user = null; user = null;
os.api("users/show", Acct.parse(props.acct))
os.magApi(
endpoints.GetUserByAcct,
{ relation: true, detail: true, pins: true, profile: true },
{ user_acct: props.acct }
)
.then((u) => { .then((u) => {
user = u; user = u;
const dayMs = 86400 * 1000;
if (
(!user.last_fetched_at ||
new Date().getTime() -
new Date(user.last_fetched_at).getTime() >
dayMs) &&
refetch
) {
os.api("users/show", Acct.parse(props.acct))
.then(() => {
fetchUser(false);
})
.catch((e) => {
error = e;
});
}
}) })
.catch((err) => { .catch((err) => {
error = err; const magError = err as MagApiError;
if (!refetch || magError.status !== 404) {
error = magError;
return;
}
os.api("users/show", Acct.parse(props.acct))
.then(() => {
fetchUser(false);
})
.catch((e) => {
error = e;
});
}); });
} }
watch(() => props.acct, fetchUser, { watch(
immediate: true, () => props.acct,
}); () => fetchUser(),
{
immediate: true,
}
);
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
@ -90,7 +124,7 @@ const headerTabs = $computed(() =>
title: i18n.ts.overview, title: i18n.ts.overview,
icon: "ph-user ph-bold ph-lg", icon: "ph-user ph-bold ph-lg",
}, },
...(($i && $i.id === user.id) || user.publicReactions ...(($i && $i.id === user.id) || user.has_public_reactions
? [ ? [
{ {
key: "reactions", key: "reactions",
@ -127,15 +161,15 @@ definePageMetadata(
user user
? { ? {
icon: "ph-user ph-bold ph-lg", icon: "ph-user ph-bold ph-lg",
title: user.name title: user.display_name
? `${user.name} (@${user.username})` ? `${user.display_name} (@${user.username})`
: `@${user.username}`, : `@${user.username}`,
subtitle: `@${getAcct(user)}`, subtitle: user.acct,
userName: user, userName: user,
avatar: user, avatar: user,
path: `/@${user.username}`, path: `/@${user.acct}`,
share: { share: {
title: user.name, title: user.display_name,
}, },
} }
: null : null

View File

@ -13,12 +13,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import * as misskey from "calckey-js";
import MkPagePreview from "@/components/MkPagePreview.vue"; import MkPagePreview from "@/components/MkPagePreview.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
user: misskey.entities.User; user: packed.PackUserBase;
}>(); }>();
const pagination = { const pagination = {

View File

@ -17,7 +17,7 @@
/> />
<MkTime :time="item.createdAt" class="createdAt" /> <MkTime :time="item.createdAt" class="createdAt" />
</div> </div>
<MkNote :key="item.id" :note="item.note" /> <XNoteResolvingProxy :key="item.id" :note="item.note.id" />
</div> </div>
</MkPagination> </MkPagination>
</MkSpacer> </MkSpacer>
@ -27,7 +27,7 @@
import { computed } from "vue"; import { computed } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import MkNote from "@/components/MkNote.vue"; import XNoteResolvingProxy from "@/components/MagNoteResolvingProxy.vue";
import MkReactionIcon from "@/components/MkReactionIcon.vue"; import MkReactionIcon from "@/components/MkReactionIcon.vue";
const props = defineProps<{ const props = defineProps<{

View File

@ -9,9 +9,9 @@
<div class="content _panel" v-if="note.cw == null"> <div class="content _panel" v-if="note.cw == null">
<div class="body"> <div class="body">
<MkA <MkA
v-if="note.replyId" v-if="note.parent_note_id"
class="reply" class="reply"
:to="`/notes/${note.replyId}`" :to="`/notes/${note.parent_note_id}`"
><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i ><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i
></MkA> ></MkA>
<Mfm <Mfm
@ -23,8 +23,11 @@
/> />
<!-- <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> --> <!-- <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> -->
</div> </div>
<div v-if="note.files.length > 0" class="richcontent"> <div
<XMediaList :media-list="note.files" /> v-if="note.attachments?.length > 0"
class="richcontent"
>
<XMediaList :media-list="note.attachments!" />
</div> </div>
<div v-if="note.poll"> <div v-if="note.poll">
<XPoll :note="note" :readOnly="true" /> <XPoll :note="note" :readOnly="true" />
@ -40,10 +43,18 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XMediaList from "@/components/MkMediaList.vue"; import XMediaList from "@/components/MkMediaList.vue";
import XPoll from "@/components/MkPoll.vue"; import XPoll from "@/components/MagPoll.vue";
import * as os from "@/os"; import * as os from "@/os";
import { packed } from "magnetar-common";
import { resolveNote } from "@/scripts-mag/mag-util";
import { $i } from "@/account";
export default defineComponent({ export default defineComponent({
computed: {
$i() {
return $i;
},
},
components: { components: {
XReactionsViewer, XReactionsViewer,
XMediaList, XMediaList,
@ -52,14 +63,14 @@ export default defineComponent({
data() { data() {
return { return {
notes: [], notes: [] as packed.PackNoteMaybeFull[],
isScrolling: false, isScrolling: false,
}; };
}, },
created() { created() {
os.api("notes/featured").then((notes) => { os.api("notes/featured").then(async (notes) => {
this.notes = notes; this.notes = await Promise.all(notes.map(resolveNote));
}); });
}, },

View File

@ -1,6 +1,7 @@
import * as Misskey from "calckey-js"; import * as Misskey from "calckey-js";
import { packed, types } from "magnetar-common"; import { endpoints, packed, types } from "magnetar-common";
import { UnicodeEmojiDef } from "@/scripts/emojilist"; import { UnicodeEmojiDef } from "@/scripts/emojilist";
import * as os from "@/os";
// https://stackoverflow.com/a/50375286 Evil magic // https://stackoverflow.com/a/50375286 Evil magic
type Dist<U> = U extends any ? (k: U) => void : never; type Dist<U> = U extends any ? (k: U) => void : never;
@ -331,3 +332,10 @@ export function magReactionIndex(
return magReactionEquals(r, reactionType); return magReactionEquals(r, reactionType);
}); });
} }
export const resolveNote = async ({ id }: { id: string }) =>
os.magApi(
endpoints.GetNoteById,
{ attachments: true, context: true },
{ id }
);

View File

@ -9,23 +9,18 @@ import { url } from "@/config";
import { noteActions } from "@/store"; import { noteActions } from "@/store";
import { shareAvailable } from "@/scripts/share-available"; import { shareAvailable } from "@/scripts/share-available";
import { getUserMenu } from "@/scripts/get-user-menu"; import { getUserMenu } from "@/scripts/get-user-menu";
import { import { magEffectiveNote, magTransUsername } from "@/scripts-mag/mag-util";
magEffectiveNote,
magTransMap,
magTransProperty,
magTransUsername,
} from "@/scripts-mag/mag-util";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
export function getNoteMenu(props: { export function getNoteMenu(props: {
note: packed.PackNoteMaybeFull | misskey.entities.Note; note: packed.PackNoteMaybeFull;
menuButton: Ref<HTMLElement | undefined>; menuButton: HTMLElement | null;
translation: Ref<any>; translation: Ref<any>;
translating: Ref<boolean>; translating: Ref<boolean>;
isDeleted: Ref<boolean>; isDeleted: Ref<boolean>;
currentClipPage?: Ref<misskey.entities.Clip>; currentClipPage?: misskey.entities.Clip;
}) { }) {
const appearNote = magEffectiveNote(props.note); const appearNote = magEffectiveNote(props.note) as packed.PackNoteMaybeFull;
function del(): void { function del(): void {
os.confirm({ os.confirm({
@ -36,7 +31,7 @@ export function getNoteMenu(props: {
os.api("notes/delete", { os.api("notes/delete", {
noteId: appearNote.id, noteId: appearNote.id,
}); }).then();
}); });
} }
@ -49,23 +44,23 @@ export function getNoteMenu(props: {
os.api("notes/delete", { os.api("notes/delete", {
noteId: appearNote.id, noteId: appearNote.id,
}); }).then();
os.post({ os.post({
initialNote: appearNote, initialNote: appearNote,
renote: magTransProperty(appearNote, "renoted_note", "renote"), renote: appearNote.renoted_note,
reply: magTransProperty(appearNote, "parent_note", "reply"), reply: appearNote.parent_note,
}); }).then();
}); });
} }
function edit(): void { function edit(): void {
os.post({ os.post({
initialNote: appearNote, initialNote: appearNote,
renote: magTransProperty(appearNote, "renoted_note", "renote"), renote: appearNote.renoted_note,
reply: magTransProperty(appearNote, "parent_note", "reply"), reply: appearNote.parent_note,
editId: appearNote.id, editId: appearNote.id,
}); }).then();
} }
function toggleFavorite(favorite: boolean): void { function toggleFavorite(favorite: boolean): void {
@ -74,7 +69,7 @@ export function getNoteMenu(props: {
{ {
noteId: appearNote.id, noteId: appearNote.id,
} }
); ).then();
} }
function toggleWatch(watch: boolean): void { function toggleWatch(watch: boolean): void {
@ -83,7 +78,7 @@ export function getNoteMenu(props: {
{ {
noteId: appearNote.id, noteId: appearNote.id,
} }
); ).then();
} }
function toggleThreadMute(mute: boolean): void { function toggleThreadMute(mute: boolean): void {
@ -92,22 +87,22 @@ export function getNoteMenu(props: {
{ {
noteId: appearNote.id, noteId: appearNote.id,
} }
); ).then();
} }
function copyContent(): void { function copyContent(): void {
copyToClipboard(appearNote.text); copyToClipboard(appearNote.text);
os.success(); os.success().then();
} }
function copyLink(): void { function copyLink(): void {
copyToClipboard(`${url}/notes/${appearNote.id}`); copyToClipboard(`${url}/notes/${appearNote.id}`);
os.success(); os.success().then();
} }
function copyOriginal(): void { function copyOriginal(): void {
copyToClipboard(appearNote.url ?? appearNote.uri); copyToClipboard(appearNote.url ?? appearNote.uri);
os.success(); os.success().then();
} }
function togglePin(pin: boolean): void { function togglePin(pin: boolean): void {
@ -122,7 +117,7 @@ export function getNoteMenu(props: {
os.alert({ os.alert({
type: "error", type: "error",
text: i18n.ts.pinLimitExceeded, text: i18n.ts.pinLimitExceeded,
}); }).then();
} }
}); });
} }
@ -162,14 +157,15 @@ export function getNoteMenu(props: {
result result
); );
os.apiWithDialog("clips/add-note", { if (clip)
clipId: clip.id, os.apiWithDialog("clips/add-note", {
noteId: appearNote.id, clipId: clip.id,
}); noteId: appearNote.id,
}).then();
}, },
}, },
null, null,
...clips.map((clip) => ({ ...clips?.map((clip) => ({
text: clip.name, text: clip.name,
action: () => { action: () => {
os.promiseDialog( os.promiseDialog(
@ -196,9 +192,9 @@ export function getNoteMenu(props: {
os.apiWithDialog("clips/remove-note", { os.apiWithDialog("clips/remove-note", {
clipId: clip.id, clipId: clip.id,
noteId: appearNote.id, noteId: appearNote.id,
}); }).then();
if ( if (
props.currentClipPage?.value.id === props.currentClipPage?.id ===
clip.id clip.id
) )
props.isDeleted.value = true; props.isDeleted.value = true;
@ -207,34 +203,36 @@ export function getNoteMenu(props: {
os.alert({ os.alert({
type: "error", type: "error",
text: err.message + "\n" + err.id, text: err.message + "\n" + err.id,
}); }).then();
} }
} }
); );
}, },
})), })),
], ],
props.menuButton.value, props.menuButton ?? undefined,
{} {}
).then(focus); ).then(focus);
} }
async function unclip(): Promise<void> { async function unclip(): Promise<void> {
os.apiWithDialog("clips/remove-note", { os.apiWithDialog("clips/remove-note", {
clipId: props.currentClipPage.value.id, clipId: props.currentClipPage?.id,
noteId: appearNote.id, noteId: appearNote.id,
}); }).then();
props.isDeleted.value = true; props.isDeleted.value = true;
} }
function share(): void { function share(): void {
navigator.share({ navigator
title: i18n.t("noteOf", { .share({
user: magTransUsername(appearNote.user), title: i18n.t("noteOf", {
}), user: magTransUsername(appearNote.user),
text: appearNote.text ?? undefined, }),
url: `${url}/notes/${appearNote.id}`, text: appearNote.text ?? undefined,
}); url: `${url}/notes/${appearNote.id}`,
})
.then();
} }
async function translate(): Promise<void> { async function translate(): Promise<void> {
@ -254,12 +252,11 @@ export function getNoteMenu(props: {
noteId: appearNote.id, noteId: appearNote.id,
}); });
const isAppearAuthor = const isAppearAuthor = appearNote.user.id === $i.id;
magTransMap(appearNote, "user", "userId", (u) => u.id) === $i.id;
const isModerator = $i.isAdmin || $i.isModerator; const isModerator = $i.isAdmin || $i.isModerator;
menu = [ menu = [
...(props.currentClipPage?.value.userId === $i.id ...(props.currentClipPage?.value?.userId === $i.id
? [ ? [
{ {
icon: "ph-minus-circle ph-bold ph-lg", icon: "ph-minus-circle ph-bold ph-lg",
@ -412,7 +409,7 @@ export function getNoteMenu(props: {
}, },
{}, {},
"closed" "closed"
); ).then();
}, },
} }
: undefined, : undefined,

View File

@ -1,4 +1,3 @@
import * as Acct from "calckey-js/built/acct";
import { defineAsyncComponent } from "vue"; import { defineAsyncComponent } from "vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import copyToClipboard from "@/scripts/copy-to-clipboard"; import copyToClipboard from "@/scripts/copy-to-clipboard";
@ -8,8 +7,14 @@ import { userActions } from "@/store";
import { $i, iAmModerator } from "@/account"; import { $i, iAmModerator } from "@/account";
import { mainRouter } from "@/router"; import { mainRouter } from "@/router";
import { Router } from "@/nirax"; import { Router } from "@/nirax";
import * as Misskey from "calckey-js";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
export function getUserMenu(user, router: Router = mainRouter) { export function getUserMenu(
user: packed.PackUserMaybeAll | Misskey.entities.UserDetailed,
router: Router = mainRouter
) {
const meId = $i ? $i.id : null; const meId = $i ? $i.id : null;
async function pushList() { async function pushList() {
@ -60,11 +65,12 @@ export function getUserMenu(user, router: Router = mainRouter) {
} }
async function toggleMute() { async function toggleMute() {
if (user.isMuted) { if (magTransProperty(user, "mute", "isMuted")) {
os.apiWithDialog("mute/delete", { os.apiWithDialog("mute/delete", {
userId: user.id, userId: user.id,
}).then(() => { }).then(() => {
user.isMuted = false; if ("isMuted" in user) user.isMuted = false;
else if ("mute" in user) user.mute = false;
}); });
} else { } else {
const { canceled, result: period } = await os.select({ const { canceled, result: period } = await os.select({
@ -112,82 +118,121 @@ export function getUserMenu(user, router: Router = mainRouter) {
userId: user.id, userId: user.id,
expiresAt, expiresAt,
}).then(() => { }).then(() => {
user.isMuted = true; if ("isMuted" in user) user.isMuted = true;
else if ("mute" in user) user.mute = true;
}); });
} }
} }
async function toggleRenoteMute(): Promise<void> { async function toggleRenoteMute(): Promise<void> {
os.apiWithDialog( os.apiWithDialog(
user.isRenoteMuted ? "renote-mute/delete" : "renote-mute/create", magTransProperty(user, "mute_renotes", "isRenoteMuted")
? "renote-mute/delete"
: "renote-mute/create",
{ {
userId: user.id, userId: user.id,
} }
).then(() => { ).then(() => {
user.isRenoteMuted = !user.isRenoteMuted; if ("isRenoteMuted" in user)
user.isRenoteMuted = !user.isRenoteMuted;
else if ("mute_renotes" in user)
user.mute_renotes = !user.mute_renotes;
}); });
} }
async function toggleBlock(): Promise<void> { async function toggleBlock(): Promise<void> {
if ( if (
!(await getConfirmed( !(await getConfirmed(
user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm magTransProperty(user, "you_block", "isBlocking")
? i18n.ts.unblockConfirm
: i18n.ts.blockConfirm
)) ))
) )
return; return;
await os.apiWithDialog( await os.apiWithDialog(
user.isBlocking ? "blocking/delete" : "blocking/create", magTransProperty(user, "you_block", "isBlocking")
? "blocking/delete"
: "blocking/create",
{ {
userId: user.id, userId: user.id,
} }
); );
user.isBlocking = !user.isBlocking;
await os.api(user.isBlocking ? "mute/create" : "mute/delete", { if ("isBlocking" in user) user.isBlocking = !user.isBlocking;
userId: user.id, else if ("you_block" in user) user.you_block = !user.you_block;
});
user.isMuted = user.isBlocking; await os.api(
if (user.isBlocking) { magTransProperty(user, "you_block", "isBlocking")
? "mute/create"
: "mute/delete",
{
userId: user.id,
}
);
if ("isMuted" in user) user.isMuted = user.isBlocking;
else if ("mute" in user) user.mute = !user.mute;
if (magTransProperty(user, "isBlocking", "you_block")) {
await os.api("following/delete", { await os.api("following/delete", {
userId: user.id, userId: user.id,
}); });
user.isFollowing = false;
if ("isFollowing" in user) user.isFollowing = false;
else if ("you_follow" in user) user.you_follow = false;
} }
} }
async function toggleSilence() { async function toggleSilence() {
if ( if (
!(await getConfirmed( !(await getConfirmed(
i18n.t(user.isSilenced ? "unsilenceConfirm" : "silenceConfirm") i18n.t(
magTransProperty(user, "is_silenced", "isSilenced")
? "unsilenceConfirm"
: "silenceConfirm"
)
)) ))
) )
return; return;
os.apiWithDialog( os.apiWithDialog(
user.isSilenced ? "admin/unsilence-user" : "admin/silence-user", magTransProperty(user, "isSilenced", "is_silenced")
? "admin/unsilence-user"
: "admin/silence-user",
{ {
userId: user.id, userId: user.id,
} }
).then(() => { ).then(() => {
user.isSilenced = !user.isSilenced; if ("isSilenced" in user) user.isSilenced = !user.isSilenced;
else if ("is_silenced" in user)
user.is_silenced = !user.is_silenced;
}); });
} }
async function toggleSuspend() { async function toggleSuspend() {
if ( if (
!(await getConfirmed( !(await getConfirmed(
i18n.t(user.isSuspended ? "unsuspendConfirm" : "suspendConfirm") i18n.t(
magTransProperty(user, "is_suspended", "isSuspended")
? "unsuspendConfirm"
: "suspendConfirm"
)
)) ))
) )
return; return;
os.apiWithDialog( os.apiWithDialog(
user.isSuspended ? "admin/unsuspend-user" : "admin/suspend-user", magTransProperty(user, "is_suspended", "isSuspended")
? "admin/unsuspend-user"
: "admin/suspend-user",
{ {
userId: user.id, userId: user.id,
} }
).then(() => { ).then(() => {
user.isSuspended = !user.isSuspended; if ("isSuspended" in user) user.isSuspended = !user.isSuspended;
else if ("is_suspended" in user)
user.is_suspended = !user.is_suspended;
}); });
} }
@ -220,7 +265,9 @@ export function getUserMenu(user, router: Router = mainRouter) {
os.apiWithDialog("following/invalidate", { os.apiWithDialog("following/invalidate", {
userId: user.id, userId: user.id,
}).then(() => { }).then(() => {
user.isFollowed = !user.isFollowed; if ("isFollowed" in user) user.isFollowed = !user.isFollowed;
else if ("follows_you" in user)
user.follows_you = !user.follows_you;
}); });
} }
@ -266,10 +313,10 @@ export function getUserMenu(user, router: Router = mainRouter) {
: undefined, : undefined,
null, null,
{ {
icon: user.isRenoteMuted icon: magTransProperty(user, "mute_renotes", "isRenoteMuted")
? "ph-eye ph-bold ph-lg" ? "ph-eye ph-bold ph-lg"
: "ph-eye-slash ph-bold ph-lg", : "ph-eye-slash ph-bold ph-lg",
text: user.isRenoteMuted text: magTransProperty(user, "mute_renotes", "isRenoteMuted")
? i18n.ts.renoteUnmute ? i18n.ts.renoteUnmute
: i18n.ts.renoteMute, : i18n.ts.renoteMute,
action: toggleRenoteMute, action: toggleRenoteMute,
@ -279,21 +326,26 @@ export function getUserMenu(user, router: Router = mainRouter) {
if ($i && meId !== user.id) { if ($i && meId !== user.id) {
menu = menu.concat([ menu = menu.concat([
{ {
icon: user.isMuted icon: magTransProperty(user, "mute", "isMuted")
? "ph-eye ph-bold ph-lg" ? "ph-eye ph-bold ph-lg"
: "ph-eye-slash ph-bold ph-lg", : "ph-eye-slash ph-bold ph-lg",
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, text: magTransProperty(user, "mute", "isMuted")
hidden: user.isBlocking === true, ? i18n.ts.unmute
: i18n.ts.mute,
hidden:
magTransProperty(user, "you_block", "isBlocking") === true,
action: toggleMute, action: toggleMute,
}, },
{ {
icon: "ph-prohibit ph-bold ph-lg", icon: "ph-prohibit ph-bold ph-lg",
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, text: magTransProperty(user, "you_block", "isBlocking")
? i18n.ts.unblock
: i18n.ts.block,
action: toggleBlock, action: toggleBlock,
}, },
]); ]);
if (user.isFollowed) { if (magTransProperty(user, "follows_you", "isFollowed")) {
menu = menu.concat([ menu = menu.concat([
{ {
icon: "ph-link-break ph-bold ph-lg", icon: "ph-link-break ph-bold ph-lg",
@ -317,12 +369,14 @@ export function getUserMenu(user, router: Router = mainRouter) {
null, null,
{ {
icon: "ph-microphone-slash ph-bold ph-lg", icon: "ph-microphone-slash ph-bold ph-lg",
text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, text: magTransProperty(user, "is_silenced", "isSilenced")
? i18n.ts.unsilence
: i18n.ts.silence,
action: toggleSilence, action: toggleSilence,
}, },
{ {
icon: "ph-snowflake ph-bold ph-lg", icon: "ph-snowflake ph-bold ph-lg",
text: user.isSuspended text: magTransProperty(user, "is_suspended", "isSuspended")
? i18n.ts.unsuspend ? i18n.ts.unsuspend
: i18n.ts.suspend, : i18n.ts.suspend,
action: toggleSuspend, action: toggleSuspend,

View File

@ -19,7 +19,7 @@ export type PageMetadata = {
subtitle?: string; subtitle?: string;
icon?: string | null; icon?: string | null;
avatar?: packed.PackUserBase | misskey.entities.User | null; avatar?: packed.PackUserBase | misskey.entities.User | null;
userName?: misskey.entities.User | null; userName?: packed.PackUserBase | misskey.entities.User | null;
bg?: string; bg?: string;
}; };

View File

@ -38,7 +38,7 @@ class ReactionPicker {
} }
public show( public show(
src: HTMLElement, src: HTMLElement | null,
onChosen: ReactionPicker["onChosen"], onChosen: ReactionPicker["onChosen"],
onClosed: ReactionPicker["onClosed"] onClosed: ReactionPicker["onClosed"]
) { ) {

View File

@ -1,18 +1,13 @@
import { onUnmounted, Ref, toRaw } from "vue"; import { onUnmounted, Ref, toRaw } from "vue";
import * as misskey from "calckey-js";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
import * as os from "@/os"; import * as os from "@/os";
import { endpoints, packed } from "magnetar-common"; import { endpoints, packed } from "magnetar-common";
import { import { magConvertReaction, magReactionIndex } from "@/scripts-mag/mag-util";
magConvertReaction,
magReactionIndex,
noteIsMag,
} from "@/scripts-mag/mag-util";
export function useNoteCapture(props: { export function useNoteCapture(props: {
rootEl: Ref<HTMLElement>; rootEl: Ref<HTMLElement>;
note: Ref<packed.PackNoteMaybeFull | misskey.entities.Note>; note: Ref<packed.PackNoteMaybeFull>;
isDeletedRef: Ref<boolean>; isDeletedRef: Ref<boolean>;
}) { }) {
const note = props.note; const note = props.note;
@ -23,180 +18,98 @@ export function useNoteCapture(props: {
if (id !== note.value.id) return; if (id !== note.value.id) return;
if (noteIsMag(note.value)) { switch (type) {
switch (type) { case "reacted": {
case "reacted": { const reaction = body.reaction as string;
const reaction = body.reaction as string;
const reactionType = magConvertReaction( const reactionType = magConvertReaction(
reaction,
body?.emoji?.url
);
const foundReaction = magReactionIndex(
note.value.reactions,
reactionType
);
const selfReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) {
const [reaction, cnt, selfReactPrev] = toRaw(
note.value.reactions[foundReaction]
);
note.value.reactions[foundReaction] = [
reaction, reaction,
body?.emoji?.url cnt + 1,
); selfReactPrev || selfReact,
const foundReaction = magReactionIndex( ];
note.value.reactions, } else {
reactionType note.value.reactions.push([reactionType, 1, selfReact]);
);
const selfReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) {
const [reaction, cnt, selfReactPrev] = toRaw(
note.value.reactions[foundReaction]
);
note.value.reactions[foundReaction] = [
reaction,
cnt + 1,
selfReactPrev || selfReact,
];
} else {
note.value.reactions.push([reactionType, 1, selfReact]);
}
break;
}
case "unreacted": {
const reaction = body.reaction;
const reactionType = magConvertReaction(reaction);
const foundReaction = magReactionIndex(
note.value.reactions,
reactionType
);
const selfUnReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) {
const [name, cnt, selfReact] =
note.value.reactions[foundReaction];
note.value.reactions[foundReaction] = [
name,
Math.max(0, cnt - 1),
!selfUnReact && (selfReact ?? false),
];
}
break;
}
case "pollVoted": {
const choice = body.choice;
if (note.value.poll) {
const options = [...note.value.poll.options];
options[choice] = {
...options[choice],
votes_count: options[choice].votes_count + 1,
voted: ($i && body.userId === $i.id) || null,
};
note.value.poll.options = options;
}
break;
}
case "deleted": {
props.isDeletedRef.value = true;
break;
}
case "updated": {
const editedNote = await os.magApi(
endpoints.GetNoteById,
{
attachments: true,
context: true,
},
{ id }
);
const keys = new Set<string>();
Object.keys(editedNote)
.concat(Object.keys(note.value))
.forEach((key) => keys.add(key));
keys.forEach((key) => {
note.value[key] = editedNote[key];
});
break;
} }
break;
} }
} else {
switch (type) {
case "reacted": {
const reaction = body.reaction;
if (body.emoji) { case "unreacted": {
const emojis = note.value.emojis || []; const reaction = body.reaction;
if (!emojis.includes(body.emoji)) {
note.value.emojis = [...emojis, body.emoji];
}
}
// TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる const reactionType = magConvertReaction(reaction);
const currentCount = note.value.reactions?.[reaction] || 0; const foundReaction = magReactionIndex(
note.value.reactions,
reactionType
);
note.value.reactions[reaction] = currentCount + 1; const selfUnReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) {
const [name, cnt, selfReact] =
note.value.reactions[foundReaction];
if ($i && body.userId === $i.id) { note.value.reactions[foundReaction] = [
note.value.myReaction = reaction; name,
} Math.max(0, cnt - 1),
break; !selfUnReact && (selfReact ?? false),
];
} }
case "unreacted": { break;
const reaction = body.reaction; }
// TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる case "pollVoted": {
const currentCount = note.value.reactions?.[reaction] || 0; const choice = body.choice;
note.value.reactions[reaction] = Math.max( if (note.value.poll) {
0, const options = [...note.value.poll.options];
currentCount - 1 options[choice] = {
); ...options[choice],
votes_count: options[choice].votes_count + 1,
if ($i && body.userId === $i.id) { voted: ($i && body.userId === $i.id) || null,
note.value.myReaction = undefined; };
} note.value.poll.options = options;
break;
} }
case "pollVoted": { break;
const choice = body.choice; }
if (note.value.poll) { case "deleted": {
const choices = [...note.value.poll.choices]; props.isDeletedRef.value = true;
choices[choice] = { break;
...choices[choice], }
votes: choices[choice].votes + 1,
...($i && body.userId === $i.id
? {
isVoted: true,
}
: {}),
};
note.value.poll.choices = choices;
}
break; case "updated": {
} const editedNote = await os.magApi(
endpoints.GetNoteById,
{
attachments: true,
context: true,
},
{ id }
);
case "deleted": { const keys = new Set<string>();
props.isDeletedRef.value = true; Object.keys(editedNote)
break; .concat(Object.keys(note.value))
} .forEach((key) => keys.add(key));
keys.forEach((key) => {
case "updated": { note.value[key] = editedNote[key];
const editedNote = await os.api("notes/show", { });
noteId: id, break;
});
const keys = new Set<string>();
Object.keys(editedNote)
.concat(Object.keys(note.value))
.forEach((key) => keys.add(key));
keys.forEach((key) => {
note.value[key] = editedNote[key];
});
break;
}
} }
} }
} }

View File

@ -41,3 +41,4 @@ export { DriveFolderBase } from "./types/DriveFolderBase";
export { DriveFolderParentExt } from "./types/DriveFolderParentExt"; export { DriveFolderParentExt } from "./types/DriveFolderParentExt";
export { ImageMeta } from "./types/ImageMeta"; export { ImageMeta } from "./types/ImageMeta";
export { InstanceTicker } from "./types/InstanceTicker"; export { InstanceTicker } from "./types/InstanceTicker";
export { MovedTo } from "./types/MovedTo";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface MovedTo { moved_to_uri: string, username: string, host: string, }

View File

@ -1,5 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MmXml } from "./MmXml"; import type { MmXml } from "./MmXml";
import type { MovedTo } from "./MovedTo";
import type { ProfileField } from "./ProfileField"; import type { ProfileField } from "./ProfileField";
export interface UserProfileExt { is_locked: boolean, is_silenced: boolean, is_suspended: boolean, description: string | null, description_mm: MmXml | null, location: string | null, birthday: string | null, fields: Array<ProfileField>, follower_count: number | null, following_count: number | null, note_count: number | null, url: string | null, moved_to_uri: string | null, also_known_as: string | null, banner_url: string | null, banner_blurhash: string | null, has_public_reactions: boolean, } export interface UserProfileExt { is_locked: boolean, is_silenced: boolean, is_suspended: boolean, description: string | null, description_mm: MmXml | null, location: string | null, birthday: string | null, fields: Array<ProfileField>, follower_count: number | null, following_count: number | null, note_count: number | null, url: string | null, moved_to: MovedTo | null, also_known_as: string | null, banner_url: string | null, banner_blurhash: string | null, has_public_reactions: boolean, }

View File

@ -56,6 +56,14 @@ pub struct UserBase {
pack!(PackUserBase, Required<Id> as id & Required<UserBase> as user); pack!(PackUserBase, Required<Id> as id & Required<UserBase> as user);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct MovedTo {
pub moved_to_uri: String,
pub username: String,
pub host: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
pub struct UserProfileExt { pub struct UserProfileExt {
@ -75,7 +83,7 @@ pub struct UserProfileExt {
pub url: Option<String>, pub url: Option<String>,
pub moved_to_uri: Option<String>, pub moved_to: Option<MovedTo>,
pub also_known_as: Option<String>, pub also_known_as: Option<String>,
pub banner_url: Option<String>, pub banner_url: Option<String>,

View File

@ -5,7 +5,7 @@ use magnetar_sdk::types::emoji::{EmojiContext, PackEmojiBase};
use magnetar_sdk::types::instance::InstanceTicker; use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::note::PackNoteMaybeFull; use magnetar_sdk::types::note::PackNoteMaybeFull;
use magnetar_sdk::types::user::{ use magnetar_sdk::types::user::{
AvatarDecoration, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform, AvatarDecoration, MovedTo, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform,
UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx,
UserRelationExt, UserSecretsExt, UserRelationExt, UserSecretsExt,
}; };
@ -80,6 +80,7 @@ pub struct UserProfileExtSource<'a> {
pub banner: Option<&'a PackDriveFileBase>, pub banner: Option<&'a PackDriveFileBase>,
pub description_mm: Option<&'a MmXml>, pub description_mm: Option<&'a MmXml>,
pub relation: Option<&'a UserRelationExt>, pub relation: Option<&'a UserRelationExt>,
pub moved_to: Option<&'a MovedTo>,
} }
impl PackType<UserProfileExtSource<'_>> for UserProfileExt { impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
@ -92,6 +93,7 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
banner, banner,
description_mm, description_mm,
relation, relation,
moved_to,
}: UserProfileExtSource, }: UserProfileExtSource,
) -> Self { ) -> Self {
let follow_visibility = match profile.ff_visibility { let follow_visibility = match profile.ff_visibility {
@ -118,7 +120,7 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
following_count: follow_visibility.then_some(user.following_count as usize), following_count: follow_visibility.then_some(user.following_count as usize),
note_count: Some(user.notes_count as usize), note_count: Some(user.notes_count as usize),
url: profile.url.clone(), url: profile.url.clone(),
moved_to_uri: user.moved_to_uri.clone(), moved_to: moved_to.cloned(),
also_known_as: user.also_known_as.clone(), also_known_as: user.also_known_as.clone(),
banner_url: banner.and_then(|b| b.file.0.url.to_owned()), banner_url: banner.and_then(|b| b.file.0.url.to_owned()),
banner_blurhash: banner.and_then(|b| b.file.0.blurhash.clone()), banner_blurhash: banner.and_then(|b| b.file.0.blurhash.clone()),

View File

@ -13,8 +13,8 @@ use magnetar_sdk::types::drive::PackDriveFileBase;
use magnetar_sdk::types::emoji::EmojiContext; use magnetar_sdk::types::emoji::EmojiContext;
use magnetar_sdk::types::instance::InstanceTicker; use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::user::{ use magnetar_sdk::types::user::{
PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll, ProfileField, MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll,
SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt,
UserProfilePinsEx, UserRelationExt, UserSecretsExt, UserProfilePinsEx, UserRelationExt, UserSecretsExt,
}; };
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
@ -153,6 +153,27 @@ impl UserModel {
_ => None, _ => None,
}; };
let moved = match &user.moved_to_uri {
Some(uri) => {
let moved = ctx.service.db.get_user_by_uri(uri).await?;
moved.and_then(|m| {
let Some(uri) = m.uri else {
return None;
};
Some(MovedTo {
moved_to_uri: uri,
username: m.username,
host: m
.host
.unwrap_or_else(|| ctx.service.config.networking.host.to_string()),
})
})
}
None => None,
};
let description_mm = self.tokenize_description(&profile); let description_mm = self.tokenize_description(&profile);
let fields = Vec::<ProfileFieldRaw>::deserialize(&profile.fields)?; let fields = Vec::<ProfileFieldRaw>::deserialize(&profile.fields)?;
@ -192,6 +213,7 @@ impl UserModel {
.and_then(|desc_mm| mmm::to_xml_string(&desc_mm).map(MmXml).ok()) .and_then(|desc_mm| mmm::to_xml_string(&desc_mm).map(MmXml).ok())
.as_ref(), .as_ref(),
relation, relation,
moved_to: moved.as_ref(),
}, },
)) ))
} }