From e9e6801cef62afdc2eea466e8f864fdfb0259b4c Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 28 Apr 2024 20:56:31 +0900 Subject: [PATCH] =?UTF-8?q?enhance(frontend):=20=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=82=92=E3=81=9F=E3=81=9F=E3=82=80=E5=9F=BA=E6=BA=96?= =?UTF-8?q?=E3=82=92=E4=BB=AE=E6=83=B3=E8=A1=8C=E6=95=B0=E3=81=8B=E3=82=89?= =?UTF-8?q?=E7=AE=97=E5=AE=9A=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/components/MkNote.vue | 2 +- .../src/components/MkSubNoteContent.vue | 10 +- packages/frontend/src/scripts/collapsed.ts | 206 +++++++++++++++++- 3 files changed, 202 insertions(+), 16 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 22b1691a86..f8764f3dea 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -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)); diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 9a07826f1a..3b47c831c2 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only ({{ i18n.ts.private }}) ({{ i18n.ts.deletedNote }}) - + RN: ...
@@ -30,8 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts index 237bd37c7a..e26b9863d1 100644 --- a/packages/frontend/src/scripts/collapsed.ts +++ b/packages/frontend/src/scripts/collapsed.ts @@ -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; }