summaryrefslogtreecommitdiff
path: root/src/client/scripts
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-10-17 20:12:00 +0900
committerGitHub <noreply@github.com>2020-10-17 20:12:00 +0900
commit7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a (patch)
tree2263a06acec7fa21882366bae26d1a983ce21135 /src/client/scripts
parentCW の input でも投稿ショートカットが動作するように (#6690) (diff)
downloadsharkey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.tar.gz
sharkey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.tar.bz2
sharkey-7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a.zip
Migrate to Vue3 (#6587)
* Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
Diffstat (limited to 'src/client/scripts')
-rw-r--r--src/client/scripts/aiscript/api.ts60
-rw-r--r--src/client/scripts/autocomplete.ts251
-rw-r--r--src/client/scripts/extract-avg-color-from-blurhash.ts9
-rw-r--r--src/client/scripts/focus.ts12
-rw-r--r--src/client/scripts/gen-search-query.ts4
-rw-r--r--src/client/scripts/get-static-image-url.ts2
-rw-r--r--src/client/scripts/get-user-menu.ts194
-rw-r--r--src/client/scripts/hotkey.ts116
-rw-r--r--src/client/scripts/hpml/evaluator.ts9
-rw-r--r--src/client/scripts/loading.ts16
-rw-r--r--src/client/scripts/paging.ts74
-rw-r--r--src/client/scripts/please-login.ts14
-rw-r--r--src/client/scripts/popout.ts22
-rw-r--r--src/client/scripts/search.ts38
-rw-r--r--src/client/scripts/select-drive-file.ts13
-rw-r--r--src/client/scripts/select-drive-folder.ts13
-rw-r--r--src/client/scripts/select-file.ts99
-rw-r--r--src/client/scripts/set-i18n-contexts.ts6
-rw-r--r--src/client/scripts/stream.ts14
-rw-r--r--src/client/scripts/theme-editor.ts17
-rw-r--r--src/client/scripts/theme.ts2
21 files changed, 643 insertions, 342 deletions
diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts
index 7e3a668871..f5618bd14c 100644
--- a/src/client/scripts/aiscript/api.ts
+++ b/src/client/scripts/aiscript/api.ts
@@ -1,22 +1,22 @@
import { utils, values } from '@syuilo/aiscript';
-import { jsToVal } from '@syuilo/aiscript/built/interpreter/util';
+import { store } from '@/store';
+import * as os from '@/os';
-// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず
-export function createAiScriptEnv(vm, opts) {
+export function createAiScriptEnv(opts) {
let apiRequests = 0;
return {
- USER_ID: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.id) : values.NULL,
- USER_NAME: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.name) : values.NULL,
- USER_USERNAME: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.username) : values.NULL,
+ USER_ID: store.getters.isSignedIn ? values.STR(store.state.i.id) : values.NULL,
+ USER_NAME: store.getters.isSignedIn ? values.STR(store.state.i.name) : values.NULL,
+ USER_USERNAME: store.getters.isSignedIn ? values.STR(store.state.i.username) : values.NULL,
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
- await vm.$root.dialog({
+ await os.dialog({
type: type ? type.value : 'info',
title: title.value,
text: text.value,
});
}),
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
- const confirm = await vm.$root.dialog({
+ const confirm = await os.dialog({
type: type ? type.value : 'question',
showCancelButton: true,
title: title.value,
@@ -28,7 +28,7 @@ export function createAiScriptEnv(vm, opts) {
if (token) utils.assertString(token);
apiRequests++;
if (apiRequests > 16) return values.NULL;
- const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
+ const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
return utils.jsToVal(res);
}),
'Mk:save': values.FN_NATIVE(([key, value]) => {
@@ -42,45 +42,3 @@ export function createAiScriptEnv(vm, opts) {
}),
};
}
-
-// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず
-export function createPluginEnv(vm, opts) {
- const config = new Map();
- for (const [k, v] of Object.entries(opts.plugin.config || {})) {
- config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
- }
-
- return {
- ...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }),
- //#region Deprecated
- 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
- vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
- }),
- 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
- vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler });
- }),
- 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
- vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
- }),
- //#endregion
- 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
- vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
- }),
- 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
- vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler });
- }),
- 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
- vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
- }),
- 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
- vm.$store.commit('registerNoteViewInterruptor', { pluginId: opts.plugin.id, handler });
- }),
- 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
- vm.$store.commit('registerNotePostInterruptor', { pluginId: opts.plugin.id, handler });
- }),
- 'Plugin:open_url': values.FN_NATIVE(([url]) => {
- window.open(url.value, '_blank');
- }),
- 'Plugin:config': values.OBJ(config),
- };
-}
diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts
new file mode 100644
index 0000000000..444f416156
--- /dev/null
+++ b/src/client/scripts/autocomplete.ts
@@ -0,0 +1,251 @@
+import { Ref, ref } from 'vue';
+import * as getCaretCoordinates from 'textarea-caret';
+import { toASCII } from 'punycode';
+import { popup } from '@/os';
+
+export class Autocomplete {
+ private suggestion: {
+ x: Ref<number>;
+ y: Ref<number>;
+ q: Ref<string>;
+ close: Function;
+ };
+ private textarea: any;
+ private vm: any;
+ private currentType: string;
+ private opts: {
+ model: string;
+ };
+ private opening: boolean;
+
+ private get text(): string {
+ return this.vm[this.opts.model];
+ }
+
+ private set text(text: string) {
+ this.vm[this.opts.model] = text;
+ }
+
+ /**
+ * 対象のテキストエリアを与えてインスタンスを初期化します。
+ */
+ constructor(textarea, vm, opts) {
+ //#region BIND
+ this.onInput = this.onInput.bind(this);
+ this.complete = this.complete.bind(this);
+ this.close = this.close.bind(this);
+ //#endregion
+
+ this.suggestion = null;
+ this.textarea = textarea;
+ this.vm = vm;
+ this.opts = opts;
+ this.opening = false;
+
+ this.attach();
+ }
+
+ /**
+ * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
+ */
+ public attach() {
+ this.textarea.addEventListener('input', this.onInput);
+ }
+
+ /**
+ * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
+ */
+ public detach() {
+ this.textarea.removeEventListener('input', this.onInput);
+ this.close();
+ }
+
+ /**
+ * テキスト入力時
+ */
+ private onInput() {
+ const caretPos = this.textarea.selectionStart;
+ const text = this.text.substr(0, caretPos).split('\n').pop();
+
+ const mentionIndex = text.lastIndexOf('@');
+ const hashtagIndex = text.lastIndexOf('#');
+ const emojiIndex = text.lastIndexOf(':');
+
+ const max = Math.max(
+ mentionIndex,
+ hashtagIndex,
+ emojiIndex);
+
+ if (max == -1) {
+ this.close();
+ return;
+ }
+
+ const isMention = mentionIndex != -1;
+ const isHashtag = hashtagIndex != -1;
+ const isEmoji = emojiIndex != -1;
+
+ let opened = false;
+
+ if (isMention) {
+ const username = text.substr(mentionIndex + 1);
+ if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
+ this.open('user', username);
+ opened = true;
+ } else if (username === '') {
+ this.open('user', null);
+ opened = true;
+ }
+ }
+
+ if (isHashtag && !opened) {
+ const hashtag = text.substr(hashtagIndex + 1);
+ if (!hashtag.includes(' ')) {
+ this.open('hashtag', hashtag);
+ opened = true;
+ }
+ }
+
+ if (isEmoji && !opened) {
+ const emoji = text.substr(emojiIndex + 1);
+ if (!emoji.includes(' ')) {
+ this.open('emoji', emoji);
+ opened = true;
+ }
+ }
+
+ if (!opened) {
+ this.close();
+ }
+ }
+
+ /**
+ * サジェストを提示します。
+ */
+ private async open(type: string, q: string) {
+ if (type != this.currentType) {
+ this.close();
+ }
+ if (this.opening) return;
+ this.opening = true;
+ this.currentType = type;
+
+ //#region サジェストを表示すべき位置を計算
+ const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
+
+ const rect = this.textarea.getBoundingClientRect();
+
+ const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
+ const y = rect.top + caretPosition.top - this.textarea.scrollTop;
+ //#endregion
+
+ if (this.suggestion) {
+ this.suggestion.x.value = x;
+ this.suggestion.y.value = y;
+ this.suggestion.q.value = q;
+
+ this.opening = false;
+ } else {
+ const MkAutocomplete = await import('@/components/autocomplete.vue');
+
+ const _x = ref(x);
+ const _y = ref(y);
+ const _q = ref(q);
+
+ const { dispose } = popup(MkAutocomplete, {
+ textarea: this.textarea,
+ close: this.close,
+ type: type,
+ q: _q,
+ x: _x,
+ y: _y,
+ }, {
+ done: (res) => {
+ this.complete(res);
+ }
+ });
+
+ this.suggestion = {
+ q: _q,
+ x: _x,
+ y: _y,
+ close: () => dispose(),
+ };
+
+ this.opening = false;
+ }
+ }
+
+ /**
+ * サジェストを閉じます。
+ */
+ private close() {
+ if (this.suggestion == null) return;
+
+ this.suggestion.close();
+ this.suggestion = null;
+
+ this.textarea.focus();
+ }
+
+ /**
+ * オートコンプリートする
+ */
+ private complete({ type, value }) {
+ this.close();
+
+ const caret = this.textarea.selectionStart;
+
+ if (type == 'user') {
+ const source = this.text;
+
+ const before = source.substr(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
+ const after = source.substr(caret);
+
+ const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
+
+ // 挿入
+ this.text = `${trimmedBefore}@${acct} ${after}`;
+
+ // キャレットを戻す
+ this.vm.$nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + (acct.length + 2);
+ this.textarea.setSelectionRange(pos, pos);
+ });
+ } else if (type == 'hashtag') {
+ const source = this.text;
+
+ const before = source.substr(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
+ const after = source.substr(caret);
+
+ // 挿入
+ this.text = `${trimmedBefore}#${value} ${after}`;
+
+ // キャレットを戻す
+ this.vm.$nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + (value.length + 2);
+ this.textarea.setSelectionRange(pos, pos);
+ });
+ } else if (type == 'emoji') {
+ const source = this.text;
+
+ const before = source.substr(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
+ const after = source.substr(caret);
+
+ // 挿入
+ this.text = trimmedBefore + value + after;
+
+ // キャレットを戻す
+ this.vm.$nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + value.length;
+ this.textarea.setSelectionRange(pos, pos);
+ });
+ }
+ }
+}
diff --git a/src/client/scripts/extract-avg-color-from-blurhash.ts b/src/client/scripts/extract-avg-color-from-blurhash.ts
new file mode 100644
index 0000000000..123ab7a06d
--- /dev/null
+++ b/src/client/scripts/extract-avg-color-from-blurhash.ts
@@ -0,0 +1,9 @@
+export function extractAvgColorFromBlurhash(hash: string) {
+ return typeof hash == 'string'
+ ? '#' + [...hash.slice(2, 6)]
+ .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
+ .reduce((a, c) => a * 83 + c, 0)
+ .toString(16)
+ .padStart(6, '0')
+ : undefined;
+}
diff --git a/src/client/scripts/focus.ts b/src/client/scripts/focus.ts
index a2a8516d36..0894877820 100644
--- a/src/client/scripts/focus.ts
+++ b/src/client/scripts/focus.ts
@@ -1,21 +1,25 @@
-export function focusPrev(el: Element | null, self = false) {
+export function focusPrev(el: Element | null, self = false, scroll = true) {
if (el == null) return;
if (!self) el = el.previousElementSibling;
if (el) {
if (el.hasAttribute('tabindex')) {
- (el as HTMLElement).focus();
+ (el as HTMLElement).focus({
+ preventScroll: !scroll
+ });
} else {
focusPrev(el.previousElementSibling, true);
}
}
}
-export function focusNext(el: Element | null, self = false) {
+export function focusNext(el: Element | null, self = false, scroll = true) {
if (el == null) return;
if (!self) el = el.nextElementSibling;
if (el) {
if (el.hasAttribute('tabindex')) {
- (el as HTMLElement).focus();
+ (el as HTMLElement).focus({
+ preventScroll: !scroll
+ });
} else {
focusPrev(el.nextElementSibling, true);
}
diff --git a/src/client/scripts/gen-search-query.ts b/src/client/scripts/gen-search-query.ts
index 2520da75df..670d915104 100644
--- a/src/client/scripts/gen-search-query.ts
+++ b/src/client/scripts/gen-search-query.ts
@@ -1,5 +1,5 @@
import parseAcct from '../../misc/acct/parse';
-import { host as localHost } from '../config';
+import { host as localHost } from '@/config';
export async function genSearchQuery(v: any, q: string) {
let host: string;
@@ -13,7 +13,7 @@ export async function genSearchQuery(v: any, q: string) {
host = at;
}
} else {
- const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null);
+ const user = await v.os.api('users/show', parseAcct(at)).catch(x => null);
if (user) {
userId = user.id;
} else {
diff --git a/src/client/scripts/get-static-image-url.ts b/src/client/scripts/get-static-image-url.ts
index eff76af256..e932eb6da5 100644
--- a/src/client/scripts/get-static-image-url.ts
+++ b/src/client/scripts/get-static-image-url.ts
@@ -1,4 +1,4 @@
-import { url as instanceUrl } from '../config';
+import { url as instanceUrl } from '@/config';
import * as url from '../../prelude/url';
export function getStaticImageUrl(baseUrl: string): string {
diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts
new file mode 100644
index 0000000000..63c3ae43b6
--- /dev/null
+++ b/src/client/scripts/get-user-menu.ts
@@ -0,0 +1,194 @@
+import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug } from '@fortawesome/free-solid-svg-icons';
+import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
+import { i18n } from '@/i18n';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { host } from '@/config';
+import getAcct from '../../misc/acct/render';
+import * as os from '@/os';
+import { store, userActions } from '@/store';
+import { router } from '@/router';
+import { defineAsyncComponent } from 'vue';
+import { popout } from './popout';
+
+export function getUserMenu(user) {
+ async function pushList() {
+ const t = i18n.global.t('selectList'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
+ const lists = await os.api('users/lists/list');
+ if (lists.length === 0) {
+ os.dialog({
+ type: 'error',
+ text: i18n.global.t('youHaveNoLists')
+ });
+ return;
+ }
+ const { canceled, result: listId } = await os.dialog({
+ type: null,
+ title: t,
+ select: {
+ items: lists.map(list => ({
+ value: list.id, text: list.name
+ }))
+ },
+ showCancelButton: true
+ });
+ if (canceled) return;
+ os.apiWithDialog('users/lists/push', {
+ listId: listId,
+ userId: user.id
+ });
+ }
+
+ async function inviteGroup() {
+ const groups = await os.api('users/groups/owned');
+ if (groups.length === 0) {
+ os.dialog({
+ type: 'error',
+ text: i18n.global.t('youHaveNoGroups')
+ });
+ return;
+ }
+ const { canceled, result: groupId } = await os.dialog({
+ type: null,
+ title: i18n.global.t('group'),
+ select: {
+ items: groups.map(group => ({
+ value: group.id, text: group.name
+ }))
+ },
+ showCancelButton: true
+ });
+ if (canceled) return;
+ os.apiWithDialog('users/groups/invite', {
+ groupId: groupId,
+ userId: user.id
+ });
+ }
+
+ async function toggleMute() {
+ os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', {
+ userId: user.id
+ }).then(() => {
+ user.isMuted = !user.isMuted;
+ });
+ }
+
+ async function toggleBlock() {
+ if (!await getConfirmed(user.isBlocking ? i18n.global.t('unblockConfirm') : i18n.global.t('blockConfirm'))) return;
+
+ os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
+ userId: user.id
+ }).then(() => {
+ user.isBlocking = !user.isBlocking;
+ });
+ }
+
+ async function toggleSilence() {
+ if (!await getConfirmed(i18n.global.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return;
+
+ os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
+ userId: user.id
+ }).then(() => {
+ user.isSilenced = !user.isSilenced;
+ });
+ }
+
+ async function toggleSuspend() {
+ if (!await getConfirmed(i18n.global.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return;
+
+ os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
+ userId: user.id
+ }).then(() => {
+ user.isSuspended = !user.isSuspended;
+ });
+ }
+
+ async function getConfirmed(text: string): Promise<boolean> {
+ const confirm = await os.dialog({
+ type: 'warning',
+ showCancelButton: true,
+ title: 'confirm',
+ text,
+ });
+
+ return !confirm.canceled;
+ }
+
+ let menu = [{
+ icon: faAt,
+ text: i18n.global.t('copyUsername'),
+ action: () => {
+ copyToClipboard(`@${user.username}@${user.host || host}`);
+ }
+ }, {
+ icon: faEnvelope,
+ text: i18n.global.t('sendMessage'),
+ action: () => {
+ os.post({ specified: user });
+ }
+ }, store.state.i.id != user.id ? {
+ icon: faComments,
+ text: i18n.global.t('startMessaging'),
+ action: () => {
+ const acct = getAcct(user);
+ switch (store.state.device.chatOpenBehavior) {
+ case 'window': { os.pageWindow('/my/messaging/' + acct, defineAsyncComponent(() => import('@/pages/messaging/messaging-room.vue')), { userAcct: acct }); break; }
+ case 'popout': { popout('/my/messaging'); break; }
+ default: { router.push('/my/messaging'); break; }
+ }
+ }
+ } : undefined, null, {
+ icon: faListUl,
+ text: i18n.global.t('addToList'),
+ action: pushList
+ }, store.state.i.id != user.id ? {
+ icon: faUsers,
+ text: i18n.global.t('inviteToGroup'),
+ action: inviteGroup
+ } : undefined] as any;
+
+ if (store.getters.isSignedIn && store.state.i.id != user.id) {
+ menu = menu.concat([null, {
+ icon: user.isMuted ? faEye : faEyeSlash,
+ text: user.isMuted ? i18n.global.t('unmute') : i18n.global.t('mute'),
+ action: toggleMute
+ }, {
+ icon: faBan,
+ text: user.isBlocking ? i18n.global.t('unblock') : i18n.global.t('block'),
+ action: toggleBlock
+ }]);
+
+ if (store.getters.isSignedIn && (store.state.i.isAdmin || store.state.i.isModerator)) {
+ menu = menu.concat([null, {
+ icon: faMicrophoneSlash,
+ text: user.isSilenced ? i18n.global.t('unsilence') : i18n.global.t('silence'),
+ action: toggleSilence
+ }, {
+ icon: faSnowflake,
+ text: user.isSuspended ? i18n.global.t('unsuspend') : i18n.global.t('suspend'),
+ action: toggleSuspend
+ }]);
+ }
+ }
+
+ if (store.getters.isSignedIn && store.state.i.id === user.id) {
+ menu = menu.concat([null, {
+ icon: faPencilAlt,
+ text: i18n.global.t('editProfile'),
+ action: () => {
+ router.push('/settings/profile');
+ }
+ }]);
+ }
+
+ if (userActions.length > 0) {
+ menu = menu.concat([null, ...userActions.map(action => ({
+ icon: faPlug,
+ text: action.title,
+ action: () => {
+ action.handler(user);
+ }
+ }))]);
+ }
+
+ return menu;
+}
diff --git a/src/client/scripts/hotkey.ts b/src/client/scripts/hotkey.ts
deleted file mode 100644
index 5f73aa58b9..0000000000
--- a/src/client/scripts/hotkey.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import keyCode from './keycode';
-import { concat } from '../../prelude/array';
-
-type pattern = {
- which: string[];
- ctrl?: boolean;
- shift?: boolean;
- alt?: boolean;
-};
-
-type action = {
- patterns: pattern[];
-
- callback: Function;
-
- allowRepeat: boolean;
-};
-
-const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
- const result = {
- patterns: [],
- callback: callback,
- allowRepeat: true
- } as action;
-
- if (patterns.match(/^\(.*\)$/) !== null) {
- result.allowRepeat = false;
- patterns = patterns.slice(1, -1);
- }
-
- result.patterns = patterns.split('|').map(part => {
- const pattern = {
- which: [],
- ctrl: false,
- alt: false,
- shift: false
- } as pattern;
-
- const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
- for (const key of keys) {
- switch (key) {
- case 'ctrl': pattern.ctrl = true; break;
- case 'alt': pattern.alt = true; break;
- case 'shift': pattern.shift = true; break;
- default: pattern.which = keyCode(key).map(k => k.toLowerCase());
- }
- }
-
- return pattern;
- });
-
- return result;
-});
-
-const ignoreElemens = ['input', 'textarea'];
-
-function match(e: KeyboardEvent, patterns: action['patterns']): boolean {
- const key = e.code.toLowerCase();
- return patterns.some(pattern => pattern.which.includes(key) &&
- pattern.ctrl === e.ctrlKey &&
- pattern.shift === e.shiftKey &&
- pattern.alt === e.altKey &&
- !e.metaKey
- );
-}
-
-export default {
- install(Vue) {
- Vue.directive('hotkey', {
- bind(el, binding) {
- el._hotkey_global = binding.modifiers.global === true;
-
- const actions = getKeyMap(binding.value);
-
- // flatten
- const reservedKeys = concat(actions.map(a => a.patterns));
-
- el._misskey_reservedKeys = reservedKeys;
-
- el._keyHandler = (e: KeyboardEvent) => {
- const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : [];
- if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
- if (document.activeElement && document.activeElement.attributes['contenteditable']) return;
-
- for (const action of actions) {
- const matched = match(e, action.patterns);
-
- if (matched) {
- if (!action.allowRepeat && e.repeat) return;
- if (el._hotkey_global && match(e, targetReservedKeys)) return;
-
- e.preventDefault();
- e.stopPropagation();
- action.callback(e);
- break;
- }
- }
- };
-
- if (el._hotkey_global) {
- document.addEventListener('keydown', el._keyHandler);
- } else {
- el.addEventListener('keydown', el._keyHandler);
- }
- },
-
- unbind(el) {
- if (el._hotkey_global) {
- document.removeEventListener('keydown', el._keyHandler);
- } else {
- el.removeEventListener('keydown', el._keyHandler);
- }
- }
- });
- }
-};
diff --git a/src/client/scripts/hpml/evaluator.ts b/src/client/scripts/hpml/evaluator.ts
index a056884368..01a122c0e4 100644
--- a/src/client/scripts/hpml/evaluator.ts
+++ b/src/client/scripts/hpml/evaluator.ts
@@ -1,11 +1,12 @@
import autobind from 'autobind-decorator';
import * as seedrandom from 'seedrandom';
import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.';
-import { version } from '../../config';
+import { version } from '@/config';
import { AiScript, utils, values } from '@syuilo/aiscript';
import { createAiScriptEnv } from '../aiscript/api';
import { collectPageVars } from '../collect-page-vars';
import { initLib } from './lib';
+import * as os from '@/os';
type Fn = {
slots: string[];
@@ -30,19 +31,19 @@ export class Hpml {
enableAiScript: boolean;
};
- constructor(vm: any, page: Hpml['page'], opts: Hpml['opts']) {
+ constructor(page: Hpml['page'], opts: Hpml['opts']) {
this.page = page;
this.variables = this.page.variables;
this.pageVars = collectPageVars(this.page.content);
this.opts = opts;
if (this.opts.enableAiScript) {
- this.aiscript = new AiScript({ ...createAiScriptEnv(vm, {
+ this.aiscript = new AiScript({ ...createAiScriptEnv({
storageKey: 'pages:' + this.page.id
}), ...initLib(this)}, {
in: (q) => {
return new Promise(ok => {
- vm.$root.dialog({
+ os.dialog({
title: q,
input: {}
}).then(({ canceled, result: a }) => {
diff --git a/src/client/scripts/loading.ts b/src/client/scripts/loading.ts
index 70a3a4c85e..4b0a560e34 100644
--- a/src/client/scripts/loading.ts
+++ b/src/client/scripts/loading.ts
@@ -1,21 +1,11 @@
-import * as NProgress from 'nprogress';
-NProgress.configure({
- trickleSpeed: 500,
- showSpinner: false
-});
-
-const root = document.getElementsByTagName('html')[0];
-
export default {
start: () => {
- root.classList.add('progress');
- NProgress.start();
+ // TODO
},
done: () => {
- root.classList.remove('progress');
- NProgress.done();
+ // TODO
},
set: val => {
- NProgress.set(val);
+ // TODO
}
};
diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts
index 538615afa1..3d9668f108 100644
--- a/src/client/scripts/paging.ts
+++ b/src/client/scripts/paging.ts
@@ -1,8 +1,12 @@
+import { markRaw } from 'vue';
+import * as os from '@/os';
import { onScrollTop, isTopVisible } from './scroll';
const SECOND_FETCH_LIMIT = 30;
export default (opts) => ({
+ emits: ['queue'],
+
data() {
return {
items: [],
@@ -14,13 +18,6 @@ export default (opts) => ({
more: false,
backed: false, // 遡り中か否か
isBackTop: false,
- ilObserver: new IntersectionObserver(
- (entries) => entries.some((entry) => entry.isIntersecting)
- && !this.moreFetching
- && !this.fetching
- && this.fetchMore()
- ),
- loadMoreElement: null as Element,
};
},
@@ -35,41 +32,33 @@ export default (opts) => ({
},
watch: {
- pagination() {
- this.init();
+ pagination: {
+ handler() {
+ this.init();
+ },
+ deep: true
},
- queue() {
- this.$emit('queue', this.queue.length);
+ queue: {
+ handler(a, b) {
+ if (a.length === 0 && b.length === 0) return;
+ this.$emit('queue', this.queue.length);
+ },
+ deep: true
}
},
created() {
opts.displayLimit = opts.displayLimit || 30;
this.init();
-
- this.$on('hook:activated', () => {
- this.isBackTop = false;
- });
-
- this.$on('hook:deactivated', () => {
- this.isBackTop = window.scrollY === 0;
- });
},
- mounted() {
- this.$nextTick(() => {
- if (this.$refs.loadMore) {
- this.loadMoreElement = this.$refs.loadMore instanceof Element ? this.$refs.loadMore : this.$refs.loadMore.$el;
- if (this.$store.state.device.enableInfiniteScroll) this.ilObserver.observe(this.loadMoreElement);
- this.loadMoreElement.addEventListener('click', this.fetchMore);
- }
- });
+ activated() {
+ this.isBackTop = false;
},
- beforeDestroy() {
- this.ilObserver.disconnect();
- if (this.$refs.loadMore) this.loadMoreElement.removeEventListener('click', this.fetchMore);
+ deactivated() {
+ this.isBackTop = window.scrollY === 0;
},
methods: {
@@ -78,19 +67,30 @@ export default (opts) => ({
this.init();
},
+ replaceItem(finder, data) {
+ const i = this.items.findIndex(finder);
+ this.items[i] = data;
+ },
+
+ removeItem(finder) {
+ const i = this.items.findIndex(finder);
+ this.items.splice(i, 1);
+ },
+
async init() {
this.queue = [];
this.fetching = true;
if (opts.before) opts.before(this);
let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
if (params && params.then) params = await params;
+ if (params === null) return;
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
- await this.$root.api(endpoint, {
+ await os.api(endpoint, {
...params,
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
}).then(items => {
for (const item of items) {
- Object.freeze(item);
+ markRaw(item);
}
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
items.pop();
@@ -111,13 +111,13 @@ export default (opts) => ({
},
async fetchMore() {
- if (!this.more || this.moreFetching || this.items.length === 0) return;
+ if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
this.moreFetching = true;
this.backed = true;
let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
if (params && params.then) params = await params;
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
- await this.$root.api(endpoint, {
+ await os.api(endpoint, {
...params,
limit: SECOND_FETCH_LIMIT + 1,
...(this.pagination.offsetMode ? {
@@ -129,7 +129,7 @@ export default (opts) => ({
}),
}).then(items => {
for (const item of items) {
- Object.freeze(item);
+ markRaw(item);
}
if (items.length > SECOND_FETCH_LIMIT) {
items.pop();
@@ -172,9 +172,5 @@ export default (opts) => ({
append(item) {
this.items.push(item);
},
-
- remove(find) {
- this.items = this.items.filter(x => !find(x));
- },
}
});
diff --git a/src/client/scripts/please-login.ts b/src/client/scripts/please-login.ts
index ebd7dd82ab..a221665295 100644
--- a/src/client/scripts/please-login.ts
+++ b/src/client/scripts/please-login.ts
@@ -1,10 +1,14 @@
-export default ($root: any) => {
- if ($root.$store.getters.isSignedIn) return;
+import { i18n } from '@/i18n';
+import { dialog } from '@/os';
+import { store } from '@/store';
- $root.dialog({
- title: $root.$t('signinRequired'),
+export function pleaseLogin() {
+ if (store.getters.isSignedIn) return;
+
+ dialog({
+ title: i18n.global.t('signinRequired'),
text: null
});
throw new Error('signin required');
-};
+}
diff --git a/src/client/scripts/popout.ts b/src/client/scripts/popout.ts
new file mode 100644
index 0000000000..f3611390c6
--- /dev/null
+++ b/src/client/scripts/popout.ts
@@ -0,0 +1,22 @@
+import * as config from '@/config';
+
+export function popout(path: string, w?: HTMLElement) {
+ let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
+ url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける
+ if (w) {
+ const position = w.getBoundingClientRect();
+ const width = parseInt(getComputedStyle(w, '').width, 10);
+ const height = parseInt(getComputedStyle(w, '').height, 10);
+ const x = window.screenX + position.left;
+ const y = window.screenY + position.top;
+ window.open(url, url,
+ `width=${width}, height=${height}, top=${y}, left=${x}`);
+ } else {
+ const width = 400;
+ const height = 450;
+ const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2);
+ const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2);
+ window.open(url, url,
+ `width=${width}, height=${height}, top=${x}, left=${y}`);
+ }
+}
diff --git a/src/client/scripts/search.ts b/src/client/scripts/search.ts
index 16057dfd34..45cc691fe4 100644
--- a/src/client/scripts/search.ts
+++ b/src/client/scripts/search.ts
@@ -1,15 +1,29 @@
import { faHistory } from '@fortawesome/free-solid-svg-icons';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { router } from '@/router';
+
+export async function search(q?: string | null | undefined) {
+ if (q == null) {
+ const { canceled, result: query } = await os.dialog({
+ title: i18n.global.t('search'),
+ input: true
+ });
+
+ if (canceled || query == null || query === '') return;
+
+ q = query;
+ }
-export async function search(v: any, q: string) {
q = q.trim();
if (q.startsWith('@') && !q.includes(' ')) {
- v.$router.push(`/${q}`);
+ router.push(`/${q}`);
return;
}
if (q.startsWith('#')) {
- v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
+ router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
return;
}
@@ -26,7 +40,7 @@ export async function search(v: any, q: string) {
}
v.$root.$emit('warp', date);
- v.$root.dialog({
+ os.dialog({
icon: faHistory,
iconOnly: true, autoClose: true
});
@@ -34,31 +48,31 @@ export async function search(v: any, q: string) {
}
if (q.startsWith('https://')) {
- const dialog = v.$root.dialog({
+ const dialog = os.dialog({
type: 'waiting',
- text: v.$t('fetchingAsApObject') + '...',
+ text: i18n.global.t('fetchingAsApObject') + '...',
showOkButton: false,
showCancelButton: false,
cancelableByBgClick: false
});
try {
- const res = await v.$root.api('ap/show', {
+ const res = await os.api('ap/show', {
uri: q
});
- dialog.close();
+ dialog.cancel();
if (res.type === 'User') {
- v.$router.push(`/@${res.object.username}@${res.object.host}`);
+ router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
- v.$router.push(`/notes/${res.object.id}`);
+ router.push(`/notes/${res.object.id}`);
}
} catch (e) {
- dialog.close();
+ dialog.cancel();
// TODO: Show error
}
return;
}
- v.$router.push(`/search?q=${encodeURIComponent(q)}`);
+ router.push(`/search?q=${encodeURIComponent(q)}`);
}
diff --git a/src/client/scripts/select-drive-file.ts b/src/client/scripts/select-drive-file.ts
deleted file mode 100644
index 3a4ac70007..0000000000
--- a/src/client/scripts/select-drive-file.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export function selectDriveFile($root: any, multiple) {
- return new Promise((res, rej) => {
- import('../components/drive-window.vue').then(m => m.default).then(dialog => {
- const w = $root.new(dialog, {
- type: 'file',
- multiple
- });
- w.$once('selected', files => {
- res(multiple ? files : files[0]);
- });
- });
- });
-}
diff --git a/src/client/scripts/select-drive-folder.ts b/src/client/scripts/select-drive-folder.ts
deleted file mode 100644
index 313d552e3a..0000000000
--- a/src/client/scripts/select-drive-folder.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export function selectDriveFolder($root: any, multiple) {
- return new Promise((res, rej) => {
- import('../components/drive-window.vue').then(m => m.default).then(dialog => {
- const w = $root.new(dialog, {
- type: 'folder',
- multiple
- });
- w.$once('selected', folders => {
- res(multiple ? folders : (folders.length === 0 ? null : folders[0]));
- });
- });
- });
-}
diff --git a/src/client/scripts/select-file.ts b/src/client/scripts/select-file.ts
index 462bdae9c0..80f9d25a2e 100644
--- a/src/client/scripts/select-file.ts
+++ b/src/client/scripts/select-file.ts
@@ -1,45 +1,23 @@
-import { faUpload, faCloud } from '@fortawesome/free-solid-svg-icons';
-import { selectDriveFile } from './select-drive-file';
-import { apiUrl } from '../config';
+import { faUpload, faCloud, faLink } from '@fortawesome/free-solid-svg-icons';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
-export function selectFile(component: any, src: any, label: string | null, multiple = false) {
+export function selectFile(src: any, label: string | null, multiple = false) {
return new Promise((res, rej) => {
const chooseFileFromPc = () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.onchange = () => {
- const dialog = component.$root.dialog({
- type: 'waiting',
- text: component.$t('uploading') + '...',
- showOkButton: false,
- showCancelButton: false,
- cancelableByBgClick: false
- });
-
- const promises = Array.from(input.files).map(file => new Promise((ok, err) => {
- const data = new FormData();
- data.append('file', file);
- data.append('i', component.$store.state.i.token);
-
- fetch(apiUrl + '/drive/files/create', {
- method: 'POST',
- body: data
- })
- .then(response => response.json())
- .then(ok)
- .catch(err);
- }));
+ const promises = Array.from(input.files).map(file => os.upload(file));
Promise.all(promises).then(driveFiles => {
res(multiple ? driveFiles : driveFiles[0]);
}).catch(e => {
- component.$root.dialog({
+ os.dialog({
type: 'error',
text: e
});
- }).finally(() => {
- dialog.close();
});
// 一応廃棄
@@ -54,34 +32,57 @@ export function selectFile(component: any, src: any, label: string | null, multi
};
const chooseFileFromDrive = () => {
- selectDriveFile(component.$root, multiple).then(files => {
+ os.selectDriveFile(multiple).then(files => {
res(files);
});
};
- // TODO
const chooseFileFromUrl = () => {
+ os.dialog({
+ title: i18n.global.t('uploadFromUrl'),
+ input: {
+ placeholder: i18n.global.t('uploadFromUrlDescription')
+ }
+ }).then(({ canceled, result: url }) => {
+ if (canceled) return;
+
+ const marker = Math.random().toString(); // TODO: UUIDとか使う
+
+ const connection = os.stream.useSharedConnection('main');
+ connection.on('urlUploadFinished', data => {
+ if (data.marker === marker) {
+ res(multiple ? [data.file] : data.file);
+ connection.dispose();
+ }
+ });
+
+ os.api('drive/files/upload_from_url', {
+ url: url,
+ marker
+ });
+ os.dialog({
+ title: i18n.global.t('uploadFromUrlRequested'),
+ text: i18n.global.t('uploadFromUrlMayTakeTime')
+ });
+ });
};
- component.$root.menu({
- items: [label ? {
- text: label,
- type: 'label'
- } : undefined, {
- text: component.$t('upload'),
- icon: faUpload,
- action: chooseFileFromPc
- }, {
- text: component.$t('fromDrive'),
- icon: faCloud,
- action: chooseFileFromDrive
- }, /*{
- text: component.$t('fromUrl'),
- icon: faLink,
- action: chooseFileFromUrl
- }*/],
- source: src
- });
+ os.modalMenu([label ? {
+ text: label,
+ type: 'label'
+ } : undefined, {
+ text: i18n.global.t('upload'),
+ icon: faUpload,
+ action: chooseFileFromPc
+ }, {
+ text: i18n.global.t('fromDrive'),
+ icon: faCloud,
+ action: chooseFileFromDrive
+ }, {
+ text: i18n.global.t('fromUrl'),
+ icon: faLink,
+ action: chooseFileFromUrl
+ }], src);
});
}
diff --git a/src/client/scripts/set-i18n-contexts.ts b/src/client/scripts/set-i18n-contexts.ts
index 872153e0bd..6014957361 100644
--- a/src/client/scripts/set-i18n-contexts.ts
+++ b/src/client/scripts/set-i18n-contexts.ts
@@ -1,8 +1,7 @@
-import VueI18n from 'vue-i18n';
import { clientDb, clear, bulkSet } from '../db';
import { deepEntries, delimitEntry } from 'deep-entries';
-export function setI18nContexts(lang: string, version: string, i18n: VueI18n, cleardb = false) {
+export function setI18nContexts(lang: string, version: string, cleardb = false) {
return Promise.all([
cleardb ? clear(clientDb.i18n) : Promise.resolve(),
fetch(`/assets/locales/${lang}.${version}.json`)
@@ -11,7 +10,6 @@ export function setI18nContexts(lang: string, version: string, i18n: VueI18n, cl
.then(locale => {
const flatLocaleEntries = deepEntries(locale, delimitEntry) as [string, string][];
bulkSet(flatLocaleEntries, clientDb.i18n);
- i18n.locale = lang;
- i18n.setLocaleMessage(lang, Object.fromEntries(flatLocaleEntries));
+ return Object.fromEntries(flatLocaleEntries);
});
}
diff --git a/src/client/scripts/stream.ts b/src/client/scripts/stream.ts
index defb22af8e..789bf94320 100644
--- a/src/client/scripts/stream.ts
+++ b/src/client/scripts/stream.ts
@@ -1,8 +1,7 @@
import autobind from 'autobind-decorator';
import { EventEmitter } from 'eventemitter3';
import ReconnectingWebsocket from 'reconnecting-websocket';
-import { wsUrl } from '../config';
-import MiOS from '../mios';
+import { wsUrl } from '@/config';
import { query as urlQuery } from '../../prelude/url';
/**
@@ -10,18 +9,13 @@ import { query as urlQuery } from '../../prelude/url';
*/
export default class Stream extends EventEmitter {
private stream: ReconnectingWebsocket;
- public state: 'initializing' | 'reconnecting' | 'connected';
+ public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = [];
private nonSharedConnections: NonSharedConnection[] = [];
- constructor(os: MiOS) {
- super();
-
- this.state = 'initializing';
-
- const user = os.store.state.i;
-
+ @autobind
+ public init(user): void {
const query = urlQuery({
i: user?.token,
_t: Date.now(),
diff --git a/src/client/scripts/theme-editor.ts b/src/client/scripts/theme-editor.ts
index e0c3bc25bc..3d69d2836a 100644
--- a/src/client/scripts/theme-editor.ts
+++ b/src/client/scripts/theme-editor.ts
@@ -5,11 +5,12 @@ import { themeProps, Theme } from './theme';
export type Default = null;
export type Color = string;
export type FuncName = 'alpha' | 'darken' | 'lighten';
-export type Func = { type: 'func', name: FuncName, arg: number, value: string };
-export type RefProp = { type: 'refProp', key: string };
-export type RefConst = { type: 'refConst', key: string };
+export type Func = { type: 'func'; name: FuncName; arg: number; value: string; };
+export type RefProp = { type: 'refProp'; key: string; };
+export type RefConst = { type: 'refConst'; key: string; };
+export type Css = { type: 'css'; value: string; };
-export type ThemeValue = Color | Func | RefProp | RefConst | Default;
+export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default;
export type ThemeViewModel = [ string, ThemeValue ][];
@@ -31,17 +32,23 @@ export const fromThemeString = (str?: string) : ThemeValue => {
type: 'refConst',
key: str.slice(1),
};
+ } else if (str.startsWith('"')) {
+ return {
+ type: 'css',
+ value: str.substr(1).trim(),
+ };
} else {
return str;
}
};
-export const toThemeString = (value: Color | Func | RefProp | RefConst) => {
+export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => {
if (typeof value === 'string') return value;
switch (value.type) {
case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
case 'refProp': return `@${value.key}`;
case 'refConst': return `$${value.key}`;
+ case 'css': return `" ${value.value}`;
}
};
diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts
index 30eaf77e01..476a41ace5 100644
--- a/src/client/scripts/theme.ts
+++ b/src/client/scripts/theme.ts
@@ -101,7 +101,7 @@ function compile(theme: Theme): Record<string, string> {
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
- props[k] = genValue(getColor(v));
+ props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
}
return props;