From 155e458806fecf86967f595d8161681ba7cb5024 Mon Sep 17 00:00:00 2001 From: Natty Date: Tue, 30 Apr 2024 13:52:13 +0200 Subject: [PATCH] Frontend: Code and post form cleanup --- .../calckey-js/src/streaming.types.ts | 2 - fe_calckey/frontend/client/package.json | 4 +- .../components/{MkLink.vue => MagLink.vue} | 8 +- .../src/components/MagMatrixMention.vue | 4 +- .../components/{MkMoved.vue => MagMoved.vue} | 0 .../client/src/components/MagNote.vue | 35 +- .../client/src/components/MagNoteHeader.vue | 20 +- .../{MkVisibility.vue => MagVisibility.vue} | 0 .../client/src/components/MkNumber.vue | 4 +- .../client/src/components/MkPostForm.vue | 123 ++++--- .../client/src/components/MkTimeline.vue | 16 +- .../src/components/global/MagAvatar.vue | 10 +- .../frontend/client/src/components/mfm.vue | 108 +++--- fe_calckey/frontend/client/src/os.ts | 341 +++++++++--------- .../client/src/pages/about-magnetar.vue | 10 +- .../client/src/pages/instance-info.vue | 159 ++++---- .../client/src/pages/settings/general.vue | 198 +++++----- .../frontend/client/src/pages/user/home.vue | 26 +- .../client/src/scripts/autocomplete.ts | 43 +-- fe_calckey/frontend/pnpm-lock.yaml | 210 +++++------ 20 files changed, 691 insertions(+), 630 deletions(-) rename fe_calckey/frontend/client/src/components/{MkLink.vue => MagLink.vue} (89%) rename fe_calckey/frontend/client/src/components/{MkMoved.vue => MagMoved.vue} (100%) rename fe_calckey/frontend/client/src/components/{MkVisibility.vue => MagVisibility.vue} (100%) diff --git a/fe_calckey/frontend/calckey-js/src/streaming.types.ts b/fe_calckey/frontend/calckey-js/src/streaming.types.ts index 078f855..885fdd0 100644 --- a/fe_calckey/frontend/calckey-js/src/streaming.types.ts +++ b/fe_calckey/frontend/calckey-js/src/streaming.types.ts @@ -5,7 +5,6 @@ import type { MeDetailed, Note, Notification, - PageEvent, User, } from "./entities"; @@ -23,7 +22,6 @@ export type Channels = { followed: (payload: User) => void; // 他人が自分をフォローしたとき unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき meUpdated: (payload: MeDetailed) => void; - pageEvent: (payload: PageEvent) => void; urlUploadFinished: (payload: { marker: string; file: DriveFile }) => void; readAllNotifications: () => void; unreadNotification: (payload: Notification) => void; diff --git a/fe_calckey/frontend/client/package.json b/fe_calckey/frontend/client/package.json index 395060b..10efe95 100644 --- a/fe_calckey/frontend/client/package.json +++ b/fe_calckey/frontend/client/package.json @@ -64,7 +64,7 @@ "prismjs": "1.29.0", "punycode": "2.1.1", "rndstr": "1.0.0", - "rollup": "^4.17.1", + "rollup": "^4.17.2", "s-age": "1.1.2", "sass": "1.62.1", "seedrandom": "3.0.5", @@ -88,7 +88,7 @@ "vite": "^5.2.10", "vite-plugin-compression": "^0.5.1", "vue": "^3.4.26", - "vue-component-type-helpers": "^2.0.14", + "vue-component-type-helpers": "^2.0.15", "vue-isyourpasswordsafe": "^2.0.0", "vue3-otp-input": "^0.4.4", "vuedraggable": "4.1.0" diff --git a/fe_calckey/frontend/client/src/components/MkLink.vue b/fe_calckey/frontend/client/src/components/MagLink.vue similarity index 89% rename from fe_calckey/frontend/client/src/components/MkLink.vue rename to fe_calckey/frontend/client/src/components/MagLink.vue index 5872500..d523c1e 100644 --- a/fe_calckey/frontend/client/src/components/MkLink.vue +++ b/fe_calckey/frontend/client/src/components/MagLink.vue @@ -3,7 +3,7 @@ :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" - :[attr]="self ? url.substr(local.length) : url" + :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target" :title="url" @@ -28,7 +28,7 @@ const props = withDefaults( url: string; rel?: null | string; }>(), - {}, + {} ); const self = props.url.startsWith(local); @@ -40,7 +40,7 @@ const el = ref(); useTooltip(el, (showing) => { os.popup( defineAsyncComponent( - () => import("@/components/MkUrlPreviewPopup.vue"), + () => import("@/components/MkUrlPreviewPopup.vue") ), { showing, @@ -48,7 +48,7 @@ useTooltip(el, (showing) => { source: el.value, }, {}, - "closed", + "closed" ); }); diff --git a/fe_calckey/frontend/client/src/components/MagMatrixMention.vue b/fe_calckey/frontend/client/src/components/MagMatrixMention.vue index 7bcb105..cf4f78d 100644 --- a/fe_calckey/frontend/client/src/components/MagMatrixMention.vue +++ b/fe_calckey/frontend/client/src/components/MagMatrixMention.vue @@ -10,7 +10,7 @@ diff --git a/fe_calckey/frontend/client/src/components/MkMoved.vue b/fe_calckey/frontend/client/src/components/MagMoved.vue similarity index 100% rename from fe_calckey/frontend/client/src/components/MkMoved.vue rename to fe_calckey/frontend/client/src/components/MagMoved.vue diff --git a/fe_calckey/frontend/client/src/components/MagNote.vue b/fe_calckey/frontend/client/src/components/MagNote.vue index 53b1899..404d64c 100644 --- a/fe_calckey/frontend/client/src/components/MagNote.vue +++ b/fe_calckey/frontend/client/src/components/MagNote.vue @@ -228,7 +228,7 @@ import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XStarButton from "@/components/MkStarButton.vue"; import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue"; import XQuoteButton from "@/components/MagQuoteButton.vue"; -import MkVisibility from "@/components/MkVisibility.vue"; +import MkVisibility from "@/components/MagVisibility.vue"; import copyToClipboard from "@/scripts/copy-to-clipboard"; import { url } from "@/config"; import { pleaseLogin } from "@/scripts/please-login"; @@ -611,15 +611,18 @@ defineExpose({ 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; @@ -634,6 +637,7 @@ defineExpose({ > div > i { margin-left: -0.5px; } + > .info { display: flex; align-items: center; @@ -683,6 +687,7 @@ defineExpose({ color: inherit; display: inline-flex; align-items: center; + > .dropdownIcon { margin-right: 4px; } @@ -693,6 +698,7 @@ defineExpose({ &.collapsedReply { .line { opacity: 0.25; + &::after { content: ""; position: absolute; @@ -705,10 +711,12 @@ defineExpose({ height: calc(50% + 5px); } } + .info { color: var(--fgTransparentWeak); transition: color 0.2s; } + .avatar { width: 1.2em; height: 1.2em; @@ -717,14 +725,17 @@ defineExpose({ margin-right: 0.4em; background: var(--panelHighlight); } + .username { font-weight: 700; flex-shrink: 0; max-width: 30%; + &::after { content: ": "; } } + &:hover, &:focus-within { .info { @@ -754,6 +765,7 @@ defineExpose({ display: flex; position: relative; z-index: 2; + > .avatar { flex-shrink: 0; display: block; @@ -764,30 +776,36 @@ defineExpose({ 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); @@ -795,6 +813,7 @@ defineExpose({ } } } + > .info { display: flex; justify-content: space-between; @@ -804,6 +823,7 @@ defineExpose({ opacity: 0.7; font-size: 0.9em; } + > .footer { position: relative; z-index: 2; @@ -811,6 +831,7 @@ defineExpose({ 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; @@ -823,6 +844,7 @@ defineExpose({ pointer-events: all; height: auto; transition: opacity 0.2s; + &::before { content: ""; position: absolute; @@ -832,17 +854,21 @@ defineExpose({ 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); } @@ -873,25 +899,32 @@ defineExpose({ 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)); diff --git a/fe_calckey/frontend/client/src/components/MagNoteHeader.vue b/fe_calckey/frontend/client/src/components/MagNoteHeader.vue index fc69ed9..dcad883 100644 --- a/fe_calckey/frontend/client/src/components/MagNoteHeader.vue +++ b/fe_calckey/frontend/client/src/components/MagNoteHeader.vue @@ -12,7 +12,9 @@ bot -
+
+ +
@@ -23,10 +25,10 @@ v-tooltip.noDelay=" i18n.t('edited', { date: new Date( - note.updated_at, + note.updated_at ).toLocaleDateString(), time: new Date( - note.updated_at, + note.updated_at ).toLocaleTimeString(), }) " @@ -48,7 +50,7 @@ diff --git a/fe_calckey/frontend/client/src/components/MkPostForm.vue b/fe_calckey/frontend/client/src/components/MkPostForm.vue index b9144f3..308897c 100644 --- a/fe_calckey/frontend/client/src/components/MkPostForm.vue +++ b/fe_calckey/frontend/client/src/components/MkPostForm.vue @@ -89,8 +89,8 @@ reply ? 'ph-arrow-u-up-left ph-bold ph-lg' : renote - ? 'ph-quotes ph-bold ph-lg' - : 'ph-paper-plane-tilt ph-bold ph-lg' + ? 'ph-quotes ph-bold ph-lg' + : 'ph-paper-plane-tilt ph-bold ph-lg' " > @@ -101,8 +101,8 @@
- {{ i18n.ts.quoteAttached - }}
@@ -127,8 +127,8 @@ >{{ i18n.ts.notSpecifiedMentionWarning }} - + + import { + computed, defineAsyncComponent, inject, nextTick, + onBeforeUnmount, onMounted, - watch, ref, - computed, + watch, } from "vue"; import * as mfm from "mfm-js"; import * as misskey from "calckey-js"; @@ -285,7 +286,7 @@ const props = withDefaults( mention?: misskey.entities.User; specified?: misskey.entities.User; initialText?: string; - initialVisibility?: typeof misskey.noteVisibilities; + initialVisibility?: (typeof misskey.noteVisibilities)[number]; initialFiles?: ( | packed.PackDriveFileBase | misskey.entities.DriveFile @@ -303,7 +304,7 @@ const props = withDefaults( initialVisibleUsers: () => [], autofocus: true, showMfmCheatSheet: true, - }, + } ); const emit = defineEmits<{ @@ -332,20 +333,20 @@ let cw = ref(null); let localOnly = ref( props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly - : defaultStore.state.defaultNoteLocalOnly, + : defaultStore.state.defaultNoteLocalOnly ); let visibility = ref( props.initialVisibility ?? ((defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state - .defaultNoteVisibility) as (typeof misskey.noteVisibilities)[number]), + .defaultNoteVisibility) as (typeof misskey.noteVisibilities)[number]) ); let visibleUsers = ref([]); if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(pushVisibleUser); } -let autocomplete = ref(null); +let autocomplete = ref(null); let draghover = ref(false); let quoteId = ref(null); let hasNotSpecifiedMentions = ref(false); @@ -392,10 +393,10 @@ const submitText = computed((): string => { return props.editId ? i18n.ts.edit : props.renote - ? i18n.ts.quote - : props.reply - ? i18n.ts.reply - : i18n.ts.note; + ? i18n.ts.quote + : props.reply + ? i18n.ts.reply + : i18n.ts.note; }); const textLength = computed((): number => { @@ -419,9 +420,11 @@ const canPost = computed((): boolean => { }); const withHashtags = computed( - defaultStore.makeGetterSetter("postFormWithHashtags"), + defaultStore.makeGetterSetter("postFormWithHashtags") +); +const hashtags = computed( + defaultStore.makeGetterSetter("postFormHashtags") ); -const hashtags = computed(defaultStore.makeGetterSetter("postFormHashtags")); watch(text, () => { checkMissingMention(); @@ -434,7 +437,7 @@ watch( }, { deep: true, - }, + } ); if (props.mention) { @@ -464,8 +467,8 @@ if (props.reply && props.reply.text != null) { const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : otherHost == null || otherHost === host - ? `@${x.username}` - : `@${x.username}@${toASCII(otherHost)}`; + ? `@${x.username}` + : `@${x.username}@${toASCII(otherHost)}`; // 自分は除外 if ($i.username === x.username && (x.host == null || x.host === host)) @@ -482,7 +485,7 @@ if (props.reply && props.reply.text != null) { if ( props.reply && ["home", "followers", "specified"].includes( - magLegacyVisibility(props.reply.visibility), + magLegacyVisibility(props.reply.visibility) ) ) { if ( @@ -492,7 +495,7 @@ if ( visibility.value = "followers"; } else if ( ["home", "followers"].includes( - magLegacyVisibility(props.reply.visibility), + magLegacyVisibility(props.reply.visibility) ) && visibility.value === "specified" ) { @@ -505,7 +508,7 @@ if ( if (ids) { os.api("users/show", { userIds: ids.filter( - (uid) => uid !== $i.id && uid !== props.reply!.user.id, + (uid) => uid !== $i.id && uid !== props.reply!.user.id ), }).then((users) => { users.forEach(pushVisibleUser); @@ -516,7 +519,7 @@ if ( os.api("users/show", { userId: props.reply.user.id }).then( (user) => { pushVisibleUser(user); - }, + } ); } } @@ -550,7 +553,7 @@ function checkMissingMention() { for (const x of extractMentions(ast)) { if ( !visibleUsers.value.some( - (u) => u.username === x.username && u.host === x.host, + (u) => u.username === x.username && u.host === x.host ) ) { hasNotSpecifiedMentions.value = true; @@ -567,13 +570,13 @@ function addMissingMention() { for (const x of extractMentions(ast)) { if ( !visibleUsers.value.some( - (u) => u.username === x.username && u.host === x.host, + (u) => u.username === x.username && u.host === x.host ) ) { os.api("users/show", { username: x.username, host: x.host }).then( (user) => { visibleUsers.value.push(user); - }, + } ); } } @@ -601,7 +604,7 @@ function focus() { textareaEl.value.focus(); textareaEl.value.setSelectionRange( textareaEl.value.value.length, - textareaEl.value.value.length, + textareaEl.value.value.length ); } } @@ -612,7 +615,7 @@ function chooseFileFrom(ev) { for (const file of files_) { files.value.push(file); } - }, + } ); } @@ -644,7 +647,7 @@ function upload(file: File, name?: string) { function setVisibility() { os.popup( defineAsyncComponent( - () => import("@/components/MkVisibilityPicker.vue"), + () => import("@/components/MkVisibilityPicker.vue") ), { currentVisibility: visibility.value, @@ -665,14 +668,14 @@ function setVisibility() { } }, }, - "closed", + "closed" ); } function pushVisibleUser(user) { if ( !visibleUsers.value.some( - (u) => u.username === user.username && u.host === user.host, + (u) => u.username === user.username && u.host === user.host ) ) { visibleUsers.value.push(user); @@ -697,13 +700,9 @@ function clear() { } function onKeydown(ev: KeyboardEvent) { - if ( - (ev.which === 10 || ev.which === 13) && - (ev.ctrlKey || ev.metaKey) && - canPost.value - ) + if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); - if (ev.which === 27) emit("esc"); + if (ev.key === "Escape") emit("esc"); } function onCompositionUpdate(ev: CompositionEvent) { @@ -715,16 +714,24 @@ function onCompositionEnd(ev: CompositionEvent) { } async function onPaste(ev: ClipboardEvent) { + if (!ev.clipboardData) { + return; + } + for (const { item, i } of Array.from(ev.clipboardData.items).map( - (item, i) => ({ item, i }), + (item, i) => ({ item, i }) )) { if (item.kind === "file") { const file = item.getAsFile(); + if (!file) { + continue; + } + const lio = file.name.lastIndexOf("."); const ext = lio >= 0 ? file.name.slice(lio) : ""; const formatted = `${formatTimeString( new Date(file.lastModified), - defaultStore.state.pastedFileName, + defaultStore.state.pastedFileName ).replace(/{{number}}/g, `${i + 1}`)}${ext}`; upload(file, formatted); } @@ -744,9 +751,9 @@ async function onPaste(ev: ClipboardEvent) { return; } - quoteId.value = paste - .substring(url.length) - .match(/^\/notes\/(.+?)\/?$/)[1]; + quoteId.value = + paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? + null; }); } } @@ -844,8 +851,8 @@ async function post() { renoteId: props.renote ? props.renote.id : quoteId.value - ? quoteId.value - : undefined, + ? quoteId.value + : undefined, poll: poll.value, cw: useCw.value ? cw.value || "" : undefined, localOnly: localOnly.value, @@ -872,7 +879,7 @@ async function post() { if (postAccount.value) { const storedAccounts = await getAccounts(); token = storedAccounts.find( - (x) => x.id === postAccount.value.id, + (x) => x.id === postAccount.value.id )?.token; } @@ -889,11 +896,11 @@ async function post() { .filter((x) => x.type === "hashtag") .map((x) => x.props.hashtag); const history = JSON.parse( - localStorage.getItem("hashtags") || "[]", + localStorage.getItem("hashtags") || "[]" ) as string[]; localStorage.setItem( "hashtags", - JSON.stringify(unique(hashtags_.concat(history))), + JSON.stringify(unique(hashtags_.concat(history))) ); } posting.value = false; @@ -943,7 +950,7 @@ function openAccountMenu(ev: MouseEvent) { } }, }, - ev, + ev ); } @@ -957,9 +964,11 @@ onMounted(() => { } // TODO: detach when unmount - new Autocomplete(textareaEl.value, text); - new Autocomplete(cwInputEl.value, cw); - new Autocomplete(hashtagsInputEl.value, hashtags); + autocomplete.value = [ + new Autocomplete(textareaEl.value!, text), + new Autocomplete(cwInputEl.value!, cw), + new Autocomplete(hashtagsInputEl.value!, hashtags), + ]; nextTick(() => { // 書きかけの投稿を復元 @@ -974,7 +983,7 @@ onMounted(() => { visibility.value = draft.data.visibility; localOnly.value = draft.data.localOnly; files.value = (draft.data.files || []).filter( - (draftFile) => draftFile, + (draftFile) => draftFile ); if (draft.data.poll) { poll.value = draft.data.poll; @@ -1007,6 +1016,10 @@ onMounted(() => { nextTick(() => watchForDraft()); }); }); + +onBeforeUnmount(() => { + autocomplete.value?.forEach((a) => a.detach()); +});