From 6798effbabe52e1afb9c83767f971679306c3428 Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Wed, 5 Apr 2023 14:30:03 +0900 Subject: [PATCH] =?UTF-8?q?enhance(client):=20=E6=8A=95=E7=A8=BF=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=BC=E3=83=A0=E3=82=92=E3=81=A1=E3=82=87=E3=81=A3?= =?UTF-8?q?=E3=81=A8=E3=81=84=E3=81=84=E6=84=9F=E3=81=98=E3=81=AB=20(#1044?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * .formラッパーを削除 * fix type of MkPostFormAttaches * :rocket: * :art: * :art: * :art: * :art: * specifiedの時は連合なしをdisabledに * :v: * set select default * gap: 2px (max-width: 500px) / 4px * wip * :v: * :art: * fix maxTextLength * 今後表示しない * :art: * cache channel * :art: * 連合なしにする * use i18n.ts.neverShow * :v: * refactor * fix indent * tweak --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- locales/ja-JP.yml | 4 +- packages/frontend/src/components/MkDialog.vue | 3 +- packages/frontend/src/components/MkNote.vue | 2 +- .../src/components/MkNoteDetailed.vue | 4 +- .../frontend/src/components/MkNoteHeader.vue | 2 +- .../frontend/src/components/MkNotePreview.vue | 5 +- .../frontend/src/components/MkPostForm.vue | 399 +++++++++++------- .../src/components/MkPostFormAttaches.vue | 6 +- .../src/components/MkVisibilityPicker.vue | 76 ++-- packages/frontend/src/local-storage.ts | 1 + packages/frontend/src/os.ts | 2 + packages/frontend/src/pages/channel.vue | 4 +- packages/frontend/src/store.ts | 2 +- .../frontend/src/ui/deck/channel-column.vue | 15 +- packages/frontend/test/note.test.ts | 21 +- packages/frontend/test/url-preview.test.ts | 3 +- 16 files changed, 338 insertions(+), 211 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a4f1d802cc..66b591760c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -961,7 +961,9 @@ copyErrorInfo: "エラー情報をコピー" joinThisServer: "このサーバーに登録する" exploreOtherServers: "他のサーバーを探す" letsLookAtTimeline: "タイムラインを見てみる" -disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。" +disableFederationConfirm: "連合なしにしますか?" +disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。" +disableFederationOk: "連合なしにする" invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。" emailNotSupported: "このサーバーではメール配信はサポートされていません" postToTheChannel: "チャンネルに投稿" diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 863ea702cd..7649eb54ea 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -36,7 +36,7 @@ <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton> </div> <div v-if="actions" :class="$style.buttons"> - <MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton> + <MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" :danger="action.danger" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton> </div> </div> </MkModal> @@ -84,6 +84,7 @@ const props = withDefaults(defineProps<{ actions?: { text: string; primary?: boolean, + danger?: boolean, callback: (...args: any[]) => void; }[]; showOkButton?: boolean; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 700bbde6f0..36ec778a14 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -31,7 +31,7 @@ <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> </div> </div> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 67bdfd2258..b9ab366850 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -30,7 +30,7 @@ <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> <article class="article" @contextmenu.stop="onContextmenu"> @@ -48,7 +48,7 @@ <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span> + <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> <div class="username"><MkAcct :user="appearNote.user"/></div> diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 15d7ea2e14..e468650430 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -17,7 +17,7 @@ <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> </div> </header> diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 16196834b7..6b55c27869 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -3,7 +3,7 @@ <MkAvatar :class="$style.avatar" :user="$i" link preview/> <div :class="$style.main"> <div :class="$style.header"> - <MkUserName :user="$i"/> + <MkUserName :user="$i" :nowrap="true"/> </div> <div> <div :class="$style.content"> @@ -50,6 +50,9 @@ const props = defineProps<{ .header { margin-bottom: 2px; font-weight: bold; + width: 100%; + overflow: clip; + text-overflow: ellipsis; } @container (min-width: 350px) { diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 2f1b74baad..247292a1b2 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -7,20 +7,35 @@ @drop.stop="onDrop" > <header :class="$style.header"> - <button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button> - <button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu"> - <MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/> - </button> - <div :class="$style.headerRight"> - <span :class="[$style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</span> - <span v-if="localOnly" :class="$style.localOnly"><i class="ti ti-world-off"></i></span> - <button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button" :class="$style.visibility" :disabled="channel != null" @click="setVisibility"> - <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> - <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> - <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> - <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> + <div :class="$style.headerLeft"> + <button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button> + <button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu"> + <MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/> + </button> + </div> + <div :class="$style.headerRight"> + <template v-if="!(channel != null && fixed)"> + <button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> + <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> + <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> + <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> + <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> + <span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span> + </button> + <button v-else :class="['_button', $style.headerRightItem, $style.visibility]" disabled> + <span><i class="ti ti-device-tv"></i></span> + <span :class="$style.headerRightButtonText">{{ channel.name }}</span> + </button> + </template> + <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" :class="['_button', $style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> + <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> + <span v-else><i class="ti ti-rocket-off"></i></span> + </button> + <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" :class="['_button', $style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance }]" @click="toggleReactionAcceptance"> + <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> + <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> + <span v-else><i class="ti ti-icons"></i></span> </button> - <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.previewButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> <button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <div :class="$style.submitInner"> <template v-if="posted"></template> @@ -31,50 +46,49 @@ </button> </div> </header> - <div :class="[$style.form]"> - <MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/> - <MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/> - <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div> - <div v-if="visibility === 'specified'" :class="$style.toSpecified"> - <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> - <div :class="$style.visibleUsers"> - <span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser"> - <MkAcct :user="u"/> - <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button> - </span> - <button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> - </div> + <MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/> + <MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/> + <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div> + <div v-if="visibility === 'specified'" :class="$style.toSpecified"> + <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> + <div :class="$style.visibleUsers"> + <span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser"> + <MkAcct :user="u"/> + <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button> + </span> + <button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> </div> - <MkInfo v-if="localOnly && channel == null" warn :class="$style.disableFederationWarn">{{ i18n.ts.disableFederationWarn }}</MkInfo> - <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> - <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> - <textarea ref="textareaEl" v-model="text" :class="[$style.text, { [$style.withCw]: useCw }]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> - <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> - <XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/> - <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> - <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/> - <div v-if="showingOptions" style="padding: 0 16px;"> - <MkSelect v-model="reactionAcceptance" small> - <template #label>{{ i18n.ts.reactionAcceptance }}</template> - <option :value="null">{{ i18n.ts.all }}</option> - <option value="likeOnly">{{ i18n.ts.likeOnly }}</option> - <option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option> - </MkSelect> - </div> - <button v-tooltip="i18n.ts.emoji" class="_button" :class="$style.emojiButton" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> - <footer :class="$style.footer"> + </div> + <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> + <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> + <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> + <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> + </div> + <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> + <XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/> + <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> + <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/> + <div v-if="showingOptions" style="padding: 8px 16px;"> + </div> + <footer :class="$style.footer"> + <div :class="$style.footerLeft"> <button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button> <button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button> <button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button> <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button> <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button> <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> - <button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button> - </footer> - <datalist id="hashtags"> - <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> - </datalist> - </div> + <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> + </div> + <div :class="$style.footerRight"> + <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> + <!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>--> + </div> + </footer> + <datalist id="hashtags"> + <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> + </datalist> </div> </template> @@ -85,7 +99,6 @@ import * as misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { toASCII } from 'punycode/'; import * as Acct from 'misskey-js/built/acct'; -import MkSelect from './MkSelect.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; @@ -113,7 +126,7 @@ const modal = inject('modal'); const props = withDefaults(defineProps<{ reply?: misskey.entities.Note; renote?: misskey.entities.Note; - channel?: any; // TODO + channel?: misskey.entities.Channel; // TODO mention?: misskey.entities.User; specified?: misskey.entities.User; initialText?: string; @@ -401,13 +414,14 @@ function upload(file: File, name?: string) { function setVisibility() { if (props.channel) { - // TODO: information dialog + visibility = 'public'; + localOnly = true; // TODO: チャンネルが連合するようになった折には消す return; } os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { currentVisibility: visibility, - currentLocalOnly: localOnly, + localOnly: localOnly, src: visibilityButton, }, { changeVisibility: v => { @@ -416,15 +430,65 @@ function setVisibility() { defaultStore.set('visibility', visibility); } }, - changeLocalOnly: v => { - localOnly = v; - if (defaultStore.state.rememberNoteVisibility) { - defaultStore.set('localOnly', localOnly); - } - }, }, 'closed'); } +async function toggleLocalOnly() { + if (props.channel) { + visibility = 'public'; + localOnly = true; // TODO: チャンネルが連合するようになった折には消す + return; + } + + const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo'); + + if (!localOnly && neverShowInfo !== 'true') { + const confirm = await os.actions({ + type: 'question', + title: i18n.ts.disableFederationConfirm, + text: i18n.ts.disableFederationConfirmWarn, + actions: [ + { + value: 'yes' as const, + text: i18n.ts.disableFederationOk, + primary: true, + }, + { + value: 'neverShow' as const, + text: `${i18n.ts.disableFederationOk} (${i18n.ts.neverShow})`, + danger: true, + }, + { + value: 'no' as const, + text: i18n.ts.cancel, + }, + ], + }); + if (confirm.canceled) return; + if (confirm.result === 'no') return; + + if (confirm.result === 'neverShow') { + miLocalStorage.setItem('neverShowLocalOnlyInfo', 'true'); + } + } + + localOnly = !localOnly; +} + +async function toggleReactionAcceptance() { + const select = await os.select({ + title: i18n.ts.reactionAcceptance, + items: [ + { value: null, text: i18n.ts.all }, + { value: 'likeOnly' as const, text: i18n.ts.likeOnly }, + { value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote }, + ], + default: reactionAcceptance, + }); + if (select.canceled) return; + reactionAcceptance = select.result; +} + function pushVisibleUser(user) { if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) { visibleUsers.push(user); @@ -818,6 +882,7 @@ defineExpose({ <style lang="scss" module> .root { position: relative; + container-type: inline-size; &.modal { width: 100%; @@ -825,21 +890,29 @@ defineExpose({ } } +//#region header .header { z-index: 1000; - height: 66px; + min-height: 50px; + display: flex; + flex-wrap: nowrap; + gap: 4px; +} + +.headerLeft { + display: grid; + grid-template-columns: repeat(2, minmax(36px, 50px)); + grid-template-rows: minmax(40px, 100%); } .cancel { padding: 0; font-size: 1em; - width: 64px; - line-height: 66px; + height: 100%; } .account { height: 100%; - aspect-ratio: 1/1; display: inline-flex; vertical-align: bottom; } @@ -847,55 +920,23 @@ defineExpose({ .avatar { width: 28px; height: 28px; - margin: auto; + margin: auto 0; } .headerRight { - position: absolute; - top: 0; - right: 0; -} - -.textCount { - opacity: 0.7; - line-height: 66px; -} - -.visibility { - height: 34px; - width: 34px; - margin: 0 0 0 8px; - - & + .localOnly { - margin-left: 0 !important; - } -} - -.localOnly { - margin: 0 0 0 12px; - opacity: 0.7; -} - -.previewButton { - display: inline-block; - padding: 0; - margin: 0 8px 0 0; - font-size: 16px; - width: 34px; - height: 34px; - border-radius: 6px; - - &:hover { - background: var(--X5); - } - - &.previewButtonActive { - color: var(--accent); - } + display: flex; + min-height: 48px; + font-size: 0.9em; + flex-wrap: nowrap; + align-items: center; + margin-left: auto; + gap: 4px; + overflow: clip; + padding-left: 4px; } .submit { - margin: 16px 16px 16px 0; + margin: 12px 12px 12px 6px; vertical-align: bottom; &:disabled { @@ -924,16 +965,47 @@ defineExpose({ line-height: 34px; font-weight: bold; border-radius: 4px; - font-size: 0.9em; min-width: 90px; box-sizing: border-box; color: var(--fgOnAccent); background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); } -.form { +.headerRightItem { + margin: 0; + padding: 8px; + border-radius: 6px; + + &:hover { + background: var(--X5); + } + + &:disabled { + background: none; + } + + &.danger { + color: #ff2a2a; + } } +.headerRightButtonText { + padding-left: 6px; +} + +.visibility { + overflow: clip; + text-overflow: ellipsis; + white-space: nowrap; + + &:enabled { + > .headerRightButtonText { + opacity: 0.8; + } + } +} +//#endregion + .preview { padding: 16px 20px 0 20px; } @@ -967,10 +1039,6 @@ defineExpose({ background: var(--X4); } -.disableFederationWarn { - margin: 0 20px 16px 20px; -} - .hasNotSpecifiedMentions { margin: 0 20px 16px 20px; } @@ -1012,18 +1080,61 @@ defineExpose({ border-top: solid 0.5px var(--divider); } -.text { - max-width: 100%; - min-width: 100%; - min-height: 90px; +.textOuter { + width: 100%; + position: relative; &.withCw { padding-top: 8px; } } +.text { + max-width: 100%; + min-width: 100%; + width: 100%; + min-height: 90px; + height: 100%; +} + +.textCount { + position: absolute; + top: 0; + right: 2px; + padding: 4px 6px; + font-size: .9em; + color: var(--warn); + border-radius: 6px; + min-width: 1.6em; + text-align: center; + + &.textOver { + color: #ff2a2a; + } +} + .footer { + display: flex; padding: 0 16px 16px 16px; + font-size: 1em; +} + +.footerLeft { + flex: 1; + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); + grid-auto-rows: 46px; +} + +.footerRight { + flex: 0.5; + margin-left: auto; + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); + grid-auto-rows: 46px; + direction: rtl; } .footerButton { @@ -1031,8 +1142,8 @@ defineExpose({ padding: 0; margin: 0; font-size: 1em; - width: 46px; - height: 46px; + width: auto; + height: 100%; border-radius: 6px; &:hover { @@ -1044,42 +1155,34 @@ defineExpose({ } } -.emojiButton { - position: absolute; - top: 55px; - right: 13px; - display: inline-block; - padding: 0; - margin: 0; - font-size: 1em; - width: 32px; - height: 32px; +.previewButtonActive { + color: var(--accent); } @container (max-width: 500px) { - .header { - height: 50px; + .headerRight { + font-size: .9em; + } - > .cancel { - width: 50px; - line-height: 50px; - } + .headerRightButtonText { + display: none; + } - > .headerRight { - > .textCount { - line-height: 50px; - } + .visibility { + overflow: initial; + } - > .submit { - margin: 8px; - } - } + .submit { + margin: 8px 8px 8px 4px; } .toSpecified { padding: 6px 16px; } + .preview { + padding: 16px 14px 0 14px; + } .cw, .hashtags, .text { @@ -1095,11 +1198,13 @@ defineExpose({ } } -@container (max-width: 310px) { - .footerButton { +@container (max-width: 330px) { + .headerRight { + gap: 0; + } + + .footer { font-size: 14px; - width: 44px; - height: 44px; } } </style> diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 5fb820f03f..760c6e5d08 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -24,19 +24,19 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d const props = defineProps<{ modelValue: any[]; - detachMediaFn: () => void; + detachMediaFn?: (id: string) => void; }>(); const emit = defineEmits<{ (ev: 'update:modelValue', value: any[]): void; - (ev: 'detach'): void; + (ev: 'detach', id: string): void; (ev: 'changeSensitive'): void; (ev: 'changeName'): void; }>(); let menuShowing = false; -function detachMedia(id) { +function detachMedia(id: string) { if (props.detachMediaFn) { props.detachMediaFn(id); } else { diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index 703c75c7d0..c181d84bc0 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -1,6 +1,9 @@ <template> -<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> - <div class="_popup" :class="$style.root"> +<MkModal ref="modal" v-slot="{ type }" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> + <div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }"> + <div :class="[$style.label, $style.item]"> + {{ i18n.ts.visibility }} + </div> <button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')"> <div :class="$style.icon"><i class="ti ti-world"></i></div> <div :class="$style.body"> @@ -29,21 +32,12 @@ <span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span> </div> </button> - <div :class="$style.divider"></div> - <button key="localOnly" class="_button" :class="[$style.item, $style.localOnly, { [$style.active]: localOnly }]" data-index="5" @click="localOnly = !localOnly"> - <div :class="$style.icon"><i class="ti ti-world-off"></i></div> - <div :class="$style.body"> - <span :class="$style.itemTitle">{{ i18n.ts._visibility.disableFederation }}</span> - <span :class="$style.itemDescription">{{ i18n.ts._visibility.disableFederationDescription }}</span> - </div> - <div :class="$style.toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div> - </button> </div> </MkModal> </template> <script lang="ts" setup> -import { nextTick, watch } from 'vue'; +import { nextTick } from 'vue'; import * as misskey from 'misskey-js'; import MkModal from '@/components/MkModal.vue'; import { i18n } from '@/i18n'; @@ -52,42 +46,58 @@ const modal = $shallowRef<InstanceType<typeof MkModal>>(); const props = withDefaults(defineProps<{ currentVisibility: typeof misskey.noteVisibilities[number]; - currentLocalOnly: boolean; + localOnly: boolean; src?: HTMLElement; }>(), { }); const emit = defineEmits<{ (ev: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void; - (ev: 'changeLocalOnly', v: boolean): void; (ev: 'closed'): void; }>(); let v = $ref(props.currentVisibility); -let localOnly = $ref(props.currentLocalOnly); - -watch($$(localOnly), () => { - emit('changeLocalOnly', localOnly); -}); function choose(visibility: typeof misskey.noteVisibilities[number]): void { v = visibility; emit('changeVisibility', visibility); nextTick(() => { - modal.close(); + if (modal) modal.close(); }); } </script> <style lang="scss" module> .root { - width: 240px; + min-width: 240px; padding: 8px 0; + + &.asDrawer { + padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; + width: 100%; + border-radius: 24px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + + .label { + pointer-events: none; + font-size: 12px; + padding-bottom: 4px; + opacity: 0.7; + } + + .item { + font-size: 14px; + padding: 10px 24px; + } + } } -.divider { - margin: 8px 0; - border-top: solid 0.5px var(--divider); +.label { + pointer-events: none; + font-size: 10px; + padding-bottom: 4px; + opacity: 0.7; } .item { @@ -107,13 +117,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void { } &.active { - color: var(--fgOnAccent); - background: var(--accent); - } - - &.localOnly.active { color: var(--accent); - background: inherit; } } @@ -144,16 +148,4 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void { .itemDescription { opacity: 0.6; } - -.toggle { - display: flex; - justify-content: center; - align-items: center; - margin-left: 10px; - width: 16px; - top: 0; - bottom: 0; - margin-top: auto; - margin-bottom: auto; -} </style> diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 38462c8a65..9a288f264c 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -6,6 +6,7 @@ type Keys = 'accounts' | 'latestDonationInfoShownAt' | 'neverShowDonationInfo' | + 'neverShowLocalOnlyInfo' | 'lastUsed' | 'lang' | 'drafts' | diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index f0af9f081b..962f9cdd98 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -215,6 +215,7 @@ export function actions<T extends { value: string; text: string; primary?: boolean, + danger?: boolean, }[]>(props: { type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; title?: string | null; @@ -229,6 +230,7 @@ export function actions<T extends { actions: props.actions.map(a => ({ text: a.text, primary: a.primary, + danger: a.danger, callback: () => { resolve({ canceled: false, result: a.value }); }, diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 47ca8003ad..437c1fae31 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -98,9 +98,7 @@ function edit() { function openPostForm() { os.post({ - channel: { - id: channel.id, - }, + channel, }); } diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 0be91bbcb4..e5558829d4 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -88,7 +88,7 @@ export const defaultStore = markRaw(new Storage('base', { }, reactionAcceptance: { where: 'account', - default: null, + default: null as 'likeOnly' | 'likeOnlyForRemote' | null, }, mutedWords: { where: 'account', diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index b81d6729e6..ff0cba33ac 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -14,13 +14,13 @@ </template> <script lang="ts" setup> -import { } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store'; import MkTimeline from '@/components/MkTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; +import * as misskey from 'misskey-js'; const props = defineProps<{ column: Column; @@ -33,6 +33,7 @@ const emit = defineEmits<{ }>(); let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); +let channel = $shallowRef<misskey.entities.Channel>(); if (props.column.channelId == null) { setChannel(); @@ -56,11 +57,15 @@ async function setChannel() { }); } -function post() { +async function post() { + if (!channel || channel.id !== props.column.channelId) { + channel = await os.api('channels/show', { + channelId: props.column.channelId, + }); + } + os.post({ - channel: { - id: props.column.channelId, - }, + channel, }); } diff --git a/packages/frontend/test/note.test.ts b/packages/frontend/test/note.test.ts index f7c47ec100..bdb1a8281a 100644 --- a/packages/frontend/test/note.test.ts +++ b/packages/frontend/test/note.test.ts @@ -2,14 +2,31 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import './init'; import type { DriveFile } from 'misskey-js/built/entities'; +import { components } from '@/components'; import { directives } from '@/directives'; import MkMediaImage from '@/components/MkMediaImage.vue'; describe('MkMediaImage', () => { const renderMediaImage = (image: Partial<DriveFile>): RenderResult => { return render(MkMediaImage, { - props: { image }, - global: { directives }, + props: { + image: { + id: 'xxxxxxxx', + createdAt: (new Date()).toJSON(), + isSensitive: false, + name: 'example.png', + thumbnailUrl: null, + url: '', + type: 'application/octet-stream', + size: 1, + md5: '15eca7fba0480996e2245f5185bf39f2', + blurhash: null, + comment: null, + properties: {}, + ...image, + } as DriveFile, + }, + global: { directives, components }, }); }; diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts index 205982a40a..4cb37e6584 100644 --- a/packages/frontend/test/url-preview.test.ts +++ b/packages/frontend/test/url-preview.test.ts @@ -2,6 +2,7 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import './init'; import type { summaly } from 'summaly'; +import { components } from '@/components'; import { directives } from '@/directives'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; @@ -27,7 +28,7 @@ describe('MkMediaImage', () => { const result = render(MkUrlPreview, { props: { url: summary.url }, - global: { directives }, + global: { directives, components }, }); await new Promise<void>(resolve => {