Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ノートを畳む条件を仮想行数、旧式、実際の表示の大きさから選べるようにする #13961

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正

### Client
-
- ノートを畳む条件を「高精度の算出方法(MFMの引数なども考慮する)」、「旧式の算出方法(従来通り)」、「実際の表示の大きさ」から選べるように

### Server
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
Expand Down
5 changes: 5 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,11 @@ regenerate: "再生成"
fontSize: "フォントサイズ"
mediaListWithOneImageAppearance: "画像が1枚のみのメディアリストの高さ"
limitTo: "{x}を上限に"
collapsingNoteSize: "ノートを畳む大きさ"
collapsingNoteCondition: "ノートを畳む条件"
detailedCalculation: "高精度の算出方法"
legacyCalculation: "旧式の算出方法"
seeRenderedSize: "実際の表示サイズに基づく"
noFollowRequests: "フォロー申請はありません"
openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード"
Expand Down
102 changes: 65 additions & 37 deletions packages/frontend/src/components/MkNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,42 +56,44 @@
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;"/>
</p>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm
v-if="appearNote.text"
:parsedNodes="parsed"
:text="appearNote.text"
:author="appearNote.user"
:nyaize="'respect'"
:emojiUrls="appearNote.emojis"
:enableEmojiMenu="true"
:enableEmojiMenuReaction="true"
/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" :style="{ 'max-height': collapsed ? `${collapseSize}em` : undefined }">
<div ref="collapsibleArea">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm
v-if="appearNote.text"
:parsedNodes="parsed"
:text="appearNote.text"
:author="appearNote.user"
:nyaize="'respect'"
:emojiUrls="appearNote.emojis"
:enableEmojiMenu="true"
:enableEmojiMenuReaction="true"
/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
</div>
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
<div v-if="isEnabledUrlPreview">
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
</div>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>
<button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true">
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
</button>
</div>
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
<div v-if="isEnabledUrlPreview">
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
</div>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>
<button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true">
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
</button>
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div>
Expand Down Expand Up @@ -196,7 +198,7 @@
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { shouldCollapsed } from '@/scripts/collapsed.js';
import { shouldCollapseLegacy, shouldCollapse } from '@/scripts/collapsed.js';
import { isEnabledUrlPreview } from '@/instance.js';

const props = withDefaults(defineProps<{
Expand All @@ -221,6 +223,17 @@

const note = ref(deepClone(props.note));

// used later
const collapsingNoteCondition = defaultStore.state.collapsingNoteCondition;
if (collapsingNoteCondition === 'seeRenderedSize') {
onMounted(() => {
const current = collapsibleArea.value.clientHeight;
const limit = collapseSize * parseFloat(getComputedStyle(collapsibleArea.value).fontSize);
isLong.value = current > limit;
collapsed.value &&= isLong.value;
})

Check failure on line 234 in packages/frontend/src/components/MkNote.vue

View workflow job for this annotation

GitHub Actions / lint (frontend)

Missing semicolon
FineArchs marked this conversation as resolved.
Show resolved Hide resolved
}

// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
Expand Down Expand Up @@ -260,8 +273,6 @@
const showContent = ref(false);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
Expand All @@ -276,6 +287,24 @@
),
);

// oversized note collapsing
const collapsibleArea = ref(null);
const collapseSize = defaultStore.state.collapsingNoteSize;
const isLong = ref(true);
switch (collapsingNoteCondition) {
case 'detailedCalculation':
isLong.value = shouldCollapse(appearNote.value, collapseSize, parsed.value, urls.value ?? []);
break;
case 'seeRenderedSize':
break;
// fail safe
case 'legacyCalculation':
default:
isLong.value = shouldCollapseLegacy(appearNote.value, urls.value ?? []);
break;
}
const collapsed = ref(appearNote.value.cw == null && isLong.value);

/* Overload FunctionにLintが対応していないのでコメントアウト
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
Expand Down Expand Up @@ -808,7 +837,6 @@

.contentCollapsed {
position: relative;
max-height: 9em;
overflow: clip;
}

Expand Down
70 changes: 50 additions & 20 deletions packages/frontend/src/components/MkSubNoteContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@
-->

<template>
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
<div :class="[$style.root, { [$style.collapsed]: collapsed }]" :style="{ 'max-height': collapsed ? `${collapseSize}em` : undefined }">
<div ref="collapsibleArea">
<div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :parsedNodes="ast" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<details v-if="note.files && note.files.length > 0">
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
<MkMediaList :mediaList="note.files"/>
</details>
<details v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary>
<MkPoll :noteId="note.id" :poll="note.poll"/>
</details>
</div>
<details v-if="note.files && note.files.length > 0">
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
<MkMediaList :mediaList="note.files"/>
</details>
<details v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary>
<MkPoll :noteId="note.id" :poll="note.poll"/>
</details>
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
</button>
Expand All @@ -30,20 +32,49 @@
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
FineArchs marked this conversation as resolved.
Show resolved Hide resolved
import * as Misskey from 'misskey-js';
import * as mfm from 'mfm-js';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import { i18n } from '@/i18n.js';
import { shouldCollapsed } from '@/scripts/collapsed.js';
import { defaultStore } from '@/store.js';
import { shouldCollapseLegacy, shouldCollapse } from '@/scripts/collapsed.js';

const props = defineProps<{
note: Misskey.entities.Note;
}>();

const isLong = shouldCollapsed(props.note, []);
const ast = computed(() => props.note.text ? mfm.parse(props.note.text) : []);

const collapsed = ref(isLong);
// oversized note collapsing
const collapsingNoteCondition = defaultStore.state.collapsingNoteCondition;
const collapseSize = defaultStore.state.collapsingNoteSize;
const collapsibleArea = ref(null);
if (collapsingNoteCondition === 'seeRenderedSize') {
onMounted(() => {

Check failure on line 55 in packages/frontend/src/components/MkSubNoteContent.vue

View workflow job for this annotation

GitHub Actions / lint (frontend)

'onMounted' is not defined
const current = collapsibleArea.value.clientHeight;
const limit = collapseSize * parseFloat(getComputedStyle(collapsibleArea.value).fontSize);
isLong.value = current > limit;
collapsed.value &&= isLong.value;
})

Check failure on line 60 in packages/frontend/src/components/MkSubNoteContent.vue

View workflow job for this annotation

GitHub Actions / lint (frontend)

Missing semicolon
FineArchs marked this conversation as resolved.
Show resolved Hide resolved
}
const isLong = ref(true);
switch (collapsingNoteCondition) {
case 'detailedCalculation':
// eslint-disable-next-line vue/no-setup-props-destructure
isLong.value = shouldCollapse(props.note, collapseSize, ast.value);
break;
case 'seeRenderedSize':
break;
// fail safe
case 'legacyCalculation':
default:
// eslint-disable-next-line vue/no-setup-props-destructure
isLong.value = shouldCollapseLegacy(props.note, []);
break;
}
const collapsed = ref(isLong.value);
</script>

<style lang="scss" module>
Expand All @@ -52,7 +83,6 @@

&.collapsed {
position: relative;
max-height: 9em;
overflow: clip;

> .fade {
Expand Down
14 changes: 14 additions & 0 deletions packages/frontend/src/pages/settings/general.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@
<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
</MkRadios>
<MkRadios v-model="collapsingNoteCondition">
<template #label>{{ i18n.ts.collapsingNoteCondition }}</template>
<option value="detailedCalculation">{{ i18n.ts.detailedCalculation }}</option>
<option value="legacyCalculation">{{ i18n.ts.legacyCalculation }}</option>
<option value="seeRenderedSize">{{ i18n.ts.seeRenderedSize }}</option>
</MkRadios>
<MkRadios v-model="collapsingNoteSize" v-if="collapsingNoteCondition !== 'legacyCalculation'">

Check failure on line 101 in packages/frontend/src/pages/settings/general.vue

View workflow job for this annotation

GitHub Actions / lint (frontend)

Attribute "v-if" should go before "v-model"
FineArchs marked this conversation as resolved.
Show resolved Hide resolved
<template #label>{{ i18n.ts.collapsingNoteSize }}</template>
<option :value="18">{{ i18n.ts.large }}</option>
<option :value="13.5">{{ i18n.ts.medium }}</option>
<option :value="9">{{ i18n.ts.small }}</option>
</MkRadios>
</div>
</FormSection>

Expand Down Expand Up @@ -306,6 +318,8 @@
const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
const showAvatarDecorations = computed(defaultStore.makeGetterSetter('showAvatarDecorations'));
const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
const collapsingNoteSize = computed(defaultStore.makeGetterSetter('collapsingNoteSize'));
const collapsingNoteCondition = computed(defaultStore.makeGetterSetter('collapsingNoteCondition'));
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/pages/settings/preferences-backups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'aiChanMode',
'devMode',
'mediaListWithOneImageAppearance',
'collapsingNoteSize',
'collapsingNoteCondition',
'notificationPosition',
'notificationStackAxis',
'enableCondensedLineForAcct',
Expand Down
Loading
Loading