enhance(frontend): ノートをたたむ基準を仮想行数から算定するように

This commit is contained in:
kakkokari-gtyih 2024-04-28 20:56:31 +09:00
parent e2ff5f58b2
commit e9e6801cef
3 changed files with 202 additions and 16 deletions

View File

@ -260,7 +260,7 @@ const isMyRenote = $i && ($i.id === note.value.userId);
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 isLong = shouldCollapsed(appearNote.value, parsed.value, urls.value ?? []);
const collapsed = ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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"/>
<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">
@ -30,8 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
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';
@ -41,7 +42,10 @@ const props = defineProps<{
note: Misskey.entities.Note;
}>();
const isLong = shouldCollapsed(props.note, []);
const ast = computed(() => props.note.text ? mfm.parse(props.note.text) : []);
// eslint-disable-next-line vue/no-setup-props-destructure
const isLong = shouldCollapsed(props.note, ast.value);
const collapsed = ref(isLong);
</script>

View File

@ -3,19 +3,201 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { safeParseFloat } from './safe-parse.js';
export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
const collapsed = note.cw == null && note.text != null && (
(note.text.includes('$[x2')) ||
(note.text.includes('$[x3')) ||
(note.text.includes('$[x4')) ||
(note.text.includes('$[scale')) ||
(note.text.split('\n').length > 9) ||
(note.text.length > 500) ||
(note.files.length >= 5) ||
(urls.length >= 4)
);
export function shouldCollapsed(note: Misskey.entities.Note, ast?: mfm.MfmNode[] | null, urls?: string[]): boolean {
if (note.cw != null) return false;
if (note.text == null) return false;
if (ast == null) return false;
if (note.files && note.files.length >= 5) return true;
if (urls && urls.length >= 4) return true;
return collapsed;
// しきい値X方向の文字数は半角換算
const limitX = 55;
const limitY = 13.5;
let forceCollapsed = false;
// まずは、文字数を考慮せずに高さを計算
function getHeightForEachLine(nodes: mfm.MfmNode[], depth = 1): [number, number][] {
// [文字カウント, 高さ]
const lineHeights: [number, number][] = [];
// インライン要素の高さを追加
function addHeightsInline(lines: [number, number][]) {
// linesのはじめの要素と、lineHeightsの最後の要素を比較。それ以外はそのまま追加
if (lines.length === 0) return;
if (lineHeights.length === 0) {
lineHeights.push(...lines);
} else {
// 入力側の最初の行
const [firstLineCharCount, firstLineHeight] = lines.shift()!;
// 記憶側の最後の行
const [lastLineCharCount, lastLineHeight] = lineHeights.pop()!;
if (lastLineCharCount <= 0 && firstLineCharCount > 0) {
lineHeights.push([firstLineCharCount, firstLineHeight], ...lines);
} else {
lineHeights.push([firstLineCharCount + lastLineCharCount, Math.max(firstLineHeight, lastLineHeight)], ...lines);
}
}
}
// 半角は1、全角は2として文字数をカウント
function getCharCount(str: string): number {
return str.split('').reduce((count, char) => count + Math.min(new Blob([char]).size, 2), 0);
}
// 糖衣 文字列の高さ
function createTextHeight(text: string): [number, number][] {
return text.split('\n').map(l => [getCharCount(l), 1]);
}
// 糖衣 文字の大きさ変換
function transformSize(lineHeight: [number, number], size: number): [number, number] {
return [lineHeight[0] * size, lineHeight[1] * size];
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
switch (node.type) {
case 'text': {
addHeightsInline(createTextHeight(node.props.text));
break;
}
case 'url': {
addHeightsInline(createTextHeight(node.props.url));
break;
}
case 'mention': {
addHeightsInline(createTextHeight(node.props.acct));
break;
}
case 'hashtag': {
addHeightsInline(createTextHeight('#' + node.props.hashtag));
break;
}
case 'inlineCode': {
addHeightsInline(createTextHeight(node.props.code));
break;
}
case 'mathInline': {
addHeightsInline(createTextHeight(node.props.formula));
break;
}
case 'small': {
addHeightsInline(getHeightForEachLine(node.children).map(h => [h[0], h[1] * 0.8]));
break;
}
case 'blockCode': {
// TODO: コードブロックは折り返ししないのでlimitXを考慮しないようにしたい
lineHeights.push(...createTextHeight(node.props.code), [0, 0]);
break;
}
case 'mathBlock': {
lineHeights.push(...createTextHeight(node.props.formula), [0, 0]);
break;
}
case 'search': {
lineHeights.push([1, 2], [0, 0]);
break;
}
case 'plain': {
addHeightsInline(getHeightForEachLine(node.children, depth + 1));
break;
}
case 'fn': {
switch (node.props.name) {
case 'tada': {
addHeightsInline(getHeightForEachLine(node.children, depth + 1).map(l => transformSize(l, 1.5)));
break;
}
case 'x2': {
addHeightsInline(getHeightForEachLine(node.children, depth + 1).map(l => transformSize(l, 2)));
break;
}
case 'x3': {
addHeightsInline(getHeightForEachLine(node.children, depth + 1).map(l => transformSize(l, 3)));
break;
}
case 'x4': {
addHeightsInline(getHeightForEachLine(node.children, depth + 1).map(l => transformSize(l, 4)));
break;
}
case 'scale': {
if ((safeParseFloat(node.props.args.x) ?? 1) > 1 || (safeParseFloat(node.props.args.y) ?? 1) > 1) {
forceCollapsed = true;
}
addHeightsInline(getHeightForEachLine(node.children, depth + 1));
break;
}
default: {
addHeightsInline(getHeightForEachLine(node.children, depth + 1));
break;
}
}
break;
}
case 'center':
case 'quote': {
lineHeights.push(...getHeightForEachLine(node.children, depth + 1), [0, 0]);
break;
}
case 'unicodeEmoji':
case 'emojiCode': {
addHeightsInline([[2, 1]]);
break;
}
case 'bold':
case 'italic':
case 'strike':
case 'link': {
addHeightsInline(getHeightForEachLine(node.children, depth + 1));
break;
}
}
}
return lineHeights.filter(h => h[1] > 0);
}
function getHeight(nodes: mfm.MfmNode[]): number {
const heights = getHeightForEachLine(nodes);
// 横幅のリミットからはみ出た分、高さを追加
const vHeight = heights.reduce((a, b) => {
return a + b[1] + Math.max(Math.ceil(b[0] / limitX) - 1, 0) * b[1];
}, 0);
return vHeight;
}
const virtualHeight = getHeight(ast);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return forceCollapsed || virtualHeight > limitY;
}