mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-26 07:23:09 +02:00
Compare commits
24 commits
1fa347390a
...
af5ebdfced
Author | SHA1 | Date | |
---|---|---|---|
|
af5ebdfced | ||
|
52bf808d89 | ||
|
155896a851 | ||
|
313ce82192 | ||
|
56d7f58626 | ||
|
e89d760240 | ||
|
c81b61eb2e | ||
|
500ea793b3 | ||
|
93e711d8a9 | ||
|
653ca7e708 | ||
|
d6cb68b091 | ||
|
6829ecb509 | ||
|
4bf3974abd | ||
|
74245df382 | ||
|
edb39a089d | ||
|
16eccad492 | ||
|
2f54a53062 | ||
|
0df069494e | ||
|
ddfc3b8a6a | ||
|
c5ac2ae163 | ||
|
03351cec0c | ||
|
dabf1867fd | ||
|
bafef1f8b4 | ||
|
2c4ba4723f |
11 changed files with 123 additions and 23 deletions
|
@ -73,6 +73,7 @@
|
||||||
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
|
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
|
||||||
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
|
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
|
||||||
- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正
|
- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正
|
||||||
|
- Fix: MkCodeEditorで行がずれていってしまう問題の修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||||
|
@ -87,6 +88,7 @@
|
||||||
- Fix: properly handle cc followers
|
- Fix: properly handle cc followers
|
||||||
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
|
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
|
||||||
- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
|
- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
|
||||||
|
- Enhance: 連合向けのノート配信を軽量化 #13192
|
||||||
|
|
||||||
### Service Worker
|
### Service Worker
|
||||||
- Enhance: オフライン表示のデザインを改善・多言語対応
|
- Enhance: オフライン表示のデザインを改善・多言語対応
|
||||||
|
|
|
@ -419,6 +419,10 @@ export class MfmService {
|
||||||
},
|
},
|
||||||
|
|
||||||
text: (node) => {
|
text: (node) => {
|
||||||
|
if (!node.props.text.match(/[\r\n]/)) {
|
||||||
|
return doc.createTextNode(node.props.text);
|
||||||
|
}
|
||||||
|
|
||||||
const el = doc.createElement('span');
|
const el = doc.createElement('span');
|
||||||
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,21 @@ export class ApMfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getNoteHtml(note: MiNote): string | null {
|
public getNoteHtml(note: MiNote, apAppend?: string) {
|
||||||
if (!note.text) return '';
|
let noMisskeyContent = false;
|
||||||
return this.mfmService.toHtml(mfm.parse(note.text), note.mentionedRemoteUsers ? JSON.parse(note.mentionedRemoteUsers) : []);
|
const srcMfm = (note.text ?? '') + (apAppend ?? '');
|
||||||
|
|
||||||
|
const parsed = mfm.parse(srcMfm);
|
||||||
|
|
||||||
|
if (!apAppend && parsed?.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
|
||||||
|
noMisskeyContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers));
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
noMisskeyContent,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -409,17 +409,15 @@ export class ApRendererService {
|
||||||
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
let apText = text;
|
let apAppend = '';
|
||||||
|
|
||||||
if (quote) {
|
if (quote) {
|
||||||
apText += `\n\nRE: ${quote}`;
|
apAppend += `\n\nRE: ${quote}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||||
|
|
||||||
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
|
||||||
text: apText,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const emojis = await this.getEmojis(note.emojis);
|
const emojis = await this.getEmojis(note.emojis);
|
||||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||||
|
@ -432,9 +430,6 @@ export class ApRendererService {
|
||||||
|
|
||||||
const asPoll = poll ? {
|
const asPoll = poll ? {
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
|
||||||
text: text,
|
|
||||||
})),
|
|
||||||
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
|
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
|
||||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
|
@ -452,11 +447,13 @@ export class ApRendererService {
|
||||||
attributedTo,
|
attributedTo,
|
||||||
summary: summary ?? undefined,
|
summary: summary ?? undefined,
|
||||||
content: content ?? undefined,
|
content: content ?? undefined,
|
||||||
|
...(noMisskeyContent ? {} : {
|
||||||
_misskey_content: text,
|
_misskey_content: text,
|
||||||
source: {
|
source: {
|
||||||
content: text,
|
content: text,
|
||||||
mediaType: 'text/x.misskeymarkdown',
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
_misskey_quote: quote,
|
_misskey_quote: quote,
|
||||||
quoteUrl: quote,
|
quoteUrl: quote,
|
||||||
quoteUri: quote,
|
quoteUri: quote,
|
||||||
|
|
44
packages/backend/test/unit/ApMfmService.ts
Normal file
44
packages/backend/test/unit/ApMfmService.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
|
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
|
||||||
|
describe('ApMfmService', () => {
|
||||||
|
let apMfmService: ApMfmService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const app = await Test.createTestingModule({
|
||||||
|
imports: [GlobalModule, CoreModule],
|
||||||
|
}).compile();
|
||||||
|
apMfmService = app.get<ApMfmService>(ApMfmService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNoteHtml', () => {
|
||||||
|
test('Do not provide _misskey_content for simple text', () => {
|
||||||
|
const note: MiNote = {
|
||||||
|
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
||||||
|
mentionedRemoteUsers: '[]',
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
|
||||||
|
assert.equal(content, '<p>テキスト <a href="http://misskey.local/tags/タグ" rel="tag">#タグ</a> <a href="http://misskey.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Provide _misskey_content for MFM', () => {
|
||||||
|
const note: MiNote = {
|
||||||
|
text: '$[tada foo]',
|
||||||
|
mentionedRemoteUsers: '[]',
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||||
|
|
||||||
|
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
|
||||||
|
assert.equal(content, '<p><i>foo</i></p>', 'content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -33,6 +33,12 @@ describe('MfmService', () => {
|
||||||
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
||||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Do not generate unnecessary span', () => {
|
||||||
|
const input = 'foo $[tada bar]';
|
||||||
|
const output = '<p>foo <i>bar</i></p>';
|
||||||
|
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fromHtml', () => {
|
describe('fromHtml', () => {
|
||||||
|
|
|
@ -93,6 +93,7 @@ watch(() => props.lang, (to) => {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
border: 1px solid var(--divider);
|
border: 1px solid var(--divider);
|
||||||
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
|
|
||||||
color: var(--shiki-fallback);
|
color: var(--shiki-fallback);
|
||||||
background-color: var(--shiki-fallback-bg);
|
background-color: var(--shiki-fallback-bg);
|
||||||
|
|
|
@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
|
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
|
||||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
|
||||||
|
|
||||||
const rootEl = shallowRef<HTMLDivElement>();
|
const rootEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
@ -49,16 +49,16 @@ const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontal
|
||||||
// ▼ しきい値 ▼ //
|
// ▼ しきい値 ▼ //
|
||||||
|
|
||||||
// スワイプと判定される最小の距離
|
// スワイプと判定される最小の距離
|
||||||
const MIN_SWIPE_DISTANCE = 50;
|
const MIN_SWIPE_DISTANCE = 20;
|
||||||
|
|
||||||
// スワイプ時の動作を発火する最小の距離
|
// スワイプ時の動作を発火する最小の距離
|
||||||
const SWIPE_DISTANCE_THRESHOLD = 125;
|
const SWIPE_DISTANCE_THRESHOLD = 70;
|
||||||
|
|
||||||
// スワイプを中断するY方向の移動距離
|
// スワイプを中断するY方向の移動距離
|
||||||
const SWIPE_ABORT_Y_THRESHOLD = 75;
|
const SWIPE_ABORT_Y_THRESHOLD = 75;
|
||||||
|
|
||||||
// スワイプできる最大の距離
|
// スワイプできる最大の距離
|
||||||
const MAX_SWIPE_DISTANCE = 150;
|
const MAX_SWIPE_DISTANCE = 120;
|
||||||
|
|
||||||
// ▲ しきい値 ▲ //
|
// ▲ しきい値 ▲ //
|
||||||
|
|
||||||
|
@ -68,7 +68,6 @@ let startScreenY: number | null = null;
|
||||||
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
|
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
|
||||||
|
|
||||||
const pullDistance = ref(0);
|
const pullDistance = ref(0);
|
||||||
const isSwiping = ref(false);
|
|
||||||
const isSwipingForClass = ref(false);
|
const isSwipingForClass = ref(false);
|
||||||
let swipeAborted = false;
|
let swipeAborted = false;
|
||||||
|
|
||||||
|
@ -77,6 +76,8 @@ function touchStart(event: TouchEvent) {
|
||||||
|
|
||||||
if (event.touches.length !== 1) return;
|
if (event.touches.length !== 1) return;
|
||||||
|
|
||||||
|
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||||
|
|
||||||
startScreenX = event.touches[0].screenX;
|
startScreenX = event.touches[0].screenX;
|
||||||
startScreenY = event.touches[0].screenY;
|
startScreenY = event.touches[0].screenY;
|
||||||
}
|
}
|
||||||
|
@ -90,6 +91,8 @@ function touchMove(event: TouchEvent) {
|
||||||
|
|
||||||
if (swipeAborted) return;
|
if (swipeAborted) return;
|
||||||
|
|
||||||
|
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||||
|
|
||||||
let distanceX = event.touches[0].screenX - startScreenX;
|
let distanceX = event.touches[0].screenX - startScreenX;
|
||||||
let distanceY = event.touches[0].screenY - startScreenY;
|
let distanceY = event.touches[0].screenY - startScreenY;
|
||||||
|
|
||||||
|
@ -139,6 +142,8 @@ function touchEnd(event: TouchEvent) {
|
||||||
|
|
||||||
if (!isSwiping.value) return;
|
if (!isSwiping.value) return;
|
||||||
|
|
||||||
|
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
|
||||||
|
|
||||||
const distance = event.changedTouches[0].screenX - startScreenX;
|
const distance = event.changedTouches[0].screenX - startScreenX;
|
||||||
|
|
||||||
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
|
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
|
||||||
|
@ -162,6 +167,24 @@ function touchEnd(event: TouchEvent) {
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 横スワイプに関与する可能性のある要素を調べる */
|
||||||
|
function hasSomethingToDoWithXSwipe(el: HTMLElement) {
|
||||||
|
if (['INPUT', 'TEXTAREA'].includes(el.tagName)) return true;
|
||||||
|
if (el.isContentEditable) return true;
|
||||||
|
if (el.scrollWidth > el.clientWidth) return true;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
if (['absolute', 'fixed', 'sticky'].includes(style.position)) return true;
|
||||||
|
if (['scroll', 'auto'].includes(style.overflowX)) return true;
|
||||||
|
if (style.touchAction === 'pan-x') return true;
|
||||||
|
|
||||||
|
if (el.parentElement && el.parentElement !== rootEl.value) {
|
||||||
|
return hasSomethingToDoWithXSwipe(el.parentElement);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
|
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
|
||||||
|
|
||||||
watch(tabModel, (newTab, oldTab) => {
|
watch(tabModel, (newTab, oldTab) => {
|
||||||
|
@ -182,6 +205,7 @@ watch(tabModel, (newTab, oldTab) => {
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.transitionRoot {
|
.transitionRoot {
|
||||||
|
touch-action: pan-y pinch-zoom;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100%;
|
grid-template-columns: 100%;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
|
@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||||
|
import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
|
||||||
|
|
||||||
const SCROLL_STOP = 10;
|
const SCROLL_STOP = 10;
|
||||||
const MAX_PULL_DISTANCE = Infinity;
|
const MAX_PULL_DISTANCE = Infinity;
|
||||||
|
@ -129,7 +130,7 @@ function moveEnd() {
|
||||||
function moving(event: TouchEvent | PointerEvent) {
|
function moving(event: TouchEvent | PointerEvent) {
|
||||||
if (!isPullStart.value || isRefreshing.value || disabled) return;
|
if (!isPullStart.value || isRefreshing.value || disabled) return;
|
||||||
|
|
||||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) {
|
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
|
||||||
pullDistance.value = 0;
|
pullDistance.value = 0;
|
||||||
isPullEnd.value = false;
|
isPullEnd.value = false;
|
||||||
moveEnd();
|
moveEnd();
|
||||||
|
@ -148,6 +149,10 @@ function moving(event: TouchEvent | PointerEvent) {
|
||||||
if (event.cancelable) event.preventDefault();
|
if (event.cancelable) event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pullDistance.value > SCROLL_STOP) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
|
|
||||||
const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
|
const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
|
||||||
|
@ -16,3 +17,6 @@ if (isTouchSupported && !isTouchUsing) {
|
||||||
isTouchUsing = true;
|
isTouchUsing = true;
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** (MkHorizontalSwipe) 横スワイプ中か? */
|
||||||
|
export const isHorizontalSwipeSwiping = ref(false);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2024.2.0-beta.8",
|
"version": "2024.2.0-beta.10",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"types": "./built/dts/index.d.ts",
|
"types": "./built/dts/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
Loading…
Reference in a new issue