-
- {{ i18n.ts._timelineDescription[src] }}
-
-
-
-
-
-
+
+
+
+ {{ i18n.ts._timelineDescription[src] }}
+
+
+
+
+
+
-
+
@@ -38,6 +39,7 @@ import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { scroll } from '@/scripts/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -69,7 +71,9 @@ const withRenotes = ref(true);
const withReplies = ref($i ? defaultStore.state.tlWithReplies : false);
const onlyFiles = ref(false);
-watch(src, () => queue.value = 0);
+watch(src, () => {
+ queue.value = 0;
+});
watch(withReplies, (x) => {
if ($i) defaultStore.set('tlWithReplies', x);
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index 95869e7b8c..603f1bef33 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -8,19 +8,21 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -36,6 +38,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XHome = defineAsyncComponent(() => import('./home.vue'));
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
@@ -57,6 +60,7 @@ const props = withDefaults(defineProps<{
});
const tab = ref(props.page);
+
const user = ref
(null);
const error = ref(null);
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts
index 2a9a42ace5..b0c36cb927 100644
--- a/packages/frontend/src/scripts/autocomplete.ts
+++ b/packages/frontend/src/scripts/autocomplete.ts
@@ -8,13 +8,13 @@ import getCaretCoordinates from 'textarea-caret';
import { toASCII } from 'punycode/';
import { popup } from '@/os.js';
-export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
+export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
export class Autocomplete {
private suggestion: {
x: Ref;
y: Ref;
- q: Ref;
+ q: Ref;
close: () => void;
} | null;
private textarea: HTMLInputElement | HTMLTextAreaElement;
@@ -49,7 +49,7 @@ export class Autocomplete {
this.textarea = textarea;
this.textRef = textRef;
this.opening = false;
- this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
+ this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam'];
this.attach();
}
@@ -80,6 +80,7 @@ export class Autocomplete {
const hashtagIndex = text.lastIndexOf('#');
const emojiIndex = text.lastIndexOf(':');
const mfmTagIndex = text.lastIndexOf('$');
+ const mfmParamIndex = text.lastIndexOf('.');
const max = Math.max(
mentionIndex,
@@ -94,7 +95,8 @@ export class Autocomplete {
const isMention = mentionIndex !== -1;
const isHashtag = hashtagIndex !== -1;
- const isMfmTag = mfmTagIndex !== -1;
+ const isMfmParam = mfmParamIndex !== -1 && text.split(/\$\[[a-zA-Z]+/).pop()?.includes('.');
+ const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
let opened = false;
@@ -134,6 +136,17 @@ export class Autocomplete {
}
}
+ if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) {
+ const mfmParam = text.substring(mfmParamIndex + 1);
+ if (!mfmParam.includes(' ')) {
+ this.open('mfmParam', {
+ tag: text.substring(mfmTagIndex + 2, mfmParamIndex),
+ params: mfmParam.split(','),
+ });
+ opened = true;
+ }
+ }
+
if (!opened) {
this.close();
}
@@ -142,7 +155,7 @@ export class Autocomplete {
/**
* サジェストを提示します。
*/
- private async open(type: string, q: string | null) {
+ private async open(type: string, q: any) {
if (type !== this.currentType) {
this.close();
}
@@ -280,6 +293,22 @@ export class Autocomplete {
const pos = trimmedBefore.length + (value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});
+ } else if (type === 'mfmParam') {
+ const source = this.text;
+
+ const before = source.substring(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf('.'));
+ const after = source.substring(caret);
+
+ // 挿入
+ this.text = `${trimmedBefore}.${value}${after}`;
+
+ // キャレットを戻す
+ nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + (value.length + 1);
+ this.textarea.setSelectionRange(pos, pos);
+ });
}
}
}
diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts
index 8e5f17f38a..3366f3eac3 100644
--- a/packages/frontend/src/scripts/i18n.ts
+++ b/packages/frontend/src/scripts/i18n.ts
@@ -2,33 +2,114 @@
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
-export class I18n> {
- public ts: T;
+type FlattenKeys = keyof {
+ [K in keyof T as T[K] extends ILocale
+ ? FlattenKeys extends infer C extends string
+ ? `${K & string}.${C}`
+ : never
+ : T[K] extends TPrediction
+ ? K
+ : never]: T[K];
+};
- constructor(locale: T) {
- this.ts = locale;
+type ParametersOf>> = T extends ILocale
+ ? TKey extends `${infer K}.${infer C}`
+ // @ts-expect-error -- C は明らかに FlattenKeys> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
+ ? ParametersOf
+ : TKey extends keyof T
+ ? T[TKey] extends ParameterizedString
+ ? P
+ : never
+ : never
+ : never;
+type Ts = {
+ readonly [K in keyof T as T[K] extends ParameterizedString ? never : K]: T[K] extends ILocale ? Ts : string;
+};
+
+export class I18n {
+ constructor(private locale: T) {
//#region BIND
this.t = this.t.bind(this);
//#endregion
}
- // string にしているのは、ドット区切りでのパス指定を許可するため
- // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
- public t(key: string, args?: Record): string {
- try {
- let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
+ public get ts(): Ts {
+ if (_DEV_) {
+ class Handler implements ProxyHandler {
+ get(target: TTarget, p: string | symbol): unknown {
+ const value = target[p as keyof TTarget];
- if (args) {
- for (const [k, v] of Object.entries(args)) {
- str = str.replace(`{${k}}`, v.toString());
+ if (typeof value === 'object') {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 実際には null がくることはないので。
+ return new Proxy(value!, new Handler());
+ }
+
+ if (typeof value === 'string') {
+ const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter);
+
+ if (parameters.length) {
+ console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
+ }
+
+ return value;
+ }
+
+ console.error(`Unexpected locale key: ${String(p)}`);
+
+ return p;
}
}
- return str;
- } catch (err) {
- console.warn(`missing localization '${key}'`);
- return key;
+
+ return new Proxy(this.locale, new Handler()) as Ts;
}
+
+ return this.locale as Ts;
+ }
+
+ /**
+ * @deprecated なるべくこのメソッド使うよりも locale 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
+ */
+ public t>(key: TKey): string;
+ public t>>(key: TKey, args: { readonly [_ in ParametersOf]: string | number }): string;
+ public t(key: string, args?: { readonly [_: string]: string | number }) {
+ let str: string | ParameterizedString | ILocale = this.locale;
+
+ for (const k of key.split('.')) {
+ str = str[k];
+
+ if (_DEV_) {
+ if (typeof str === 'undefined') {
+ console.error(`Unexpected locale key: ${key}`);
+ return key;
+ }
+ }
+ }
+
+ if (args) {
+ if (_DEV_) {
+ const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
+
+ if (missing.length) {
+ console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`);
+ }
+ }
+
+ for (const [k, v] of Object.entries(args)) {
+ const search = `{${k}}`;
+
+ if (_DEV_) {
+ if (!(str as string).includes(search)) {
+ console.error(`Unexpected locale parameter: ${k} at ${key}`);
+ }
+ }
+
+ str = (str as string).replace(search, v.toString());
+ }
+ }
+
+ return str;
}
}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index e3a85377d8..21b796caa1 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -427,6 +427,10 @@ export const defaultStore = markRaw(new Storage('base', {
sfxVolume: 1,
},
},
+ enableHorizontalSwipe: {
+ where: 'device',
+ default: true,
+ },
sound_masterVolume: {
where: 'device',