From 90f8fe7e538bb7e52d2558152a0390e693f39b11 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Thu, 29 Mar 2018 01:20:40 +0900 Subject: Introduce processor --- src/server/web/app/common/define-widget.ts | 79 +++ src/server/web/app/common/mios.ts | 578 +++++++++++++++++++++ .../web/app/common/scripts/check-for-update.ts | 33 ++ .../web/app/common/scripts/compose-notification.ts | 67 +++ src/server/web/app/common/scripts/contains.ts | 8 + .../web/app/common/scripts/copy-to-clipboard.ts | 13 + .../web/app/common/scripts/date-stringify.ts | 13 + src/server/web/app/common/scripts/fuck-ad-block.ts | 21 + src/server/web/app/common/scripts/gcd.ts | 2 + src/server/web/app/common/scripts/get-kao.ts | 5 + src/server/web/app/common/scripts/get-median.ts | 11 + src/server/web/app/common/scripts/loading.ts | 21 + .../web/app/common/scripts/parse-search-query.ts | 53 ++ .../web/app/common/scripts/streaming/channel.ts | 13 + .../web/app/common/scripts/streaming/drive.ts | 34 ++ .../web/app/common/scripts/streaming/home.ts | 57 ++ .../common/scripts/streaming/messaging-index.ts | 34 ++ .../web/app/common/scripts/streaming/messaging.ts | 20 + .../app/common/scripts/streaming/othello-game.ts | 11 + .../web/app/common/scripts/streaming/othello.ts | 31 ++ .../web/app/common/scripts/streaming/requests.ts | 30 ++ .../web/app/common/scripts/streaming/server.ts | 30 ++ .../app/common/scripts/streaming/stream-manager.ts | 108 ++++ .../web/app/common/scripts/streaming/stream.ts | 137 +++++ .../app/common/views/components/autocomplete.vue | 306 +++++++++++ .../components/connect-failed.troubleshooter.vue | 137 +++++ .../app/common/views/components/connect-failed.vue | 106 ++++ .../web/app/common/views/components/ellipsis.vue | 26 + .../app/common/views/components/file-type-icon.vue | 17 + .../web/app/common/views/components/forkit.vue | 42 ++ .../web/app/common/views/components/index.ts | 51 ++ .../web/app/common/views/components/media-list.vue | 57 ++ .../views/components/messaging-room.form.vue | 305 +++++++++++ .../views/components/messaging-room.message.vue | 263 ++++++++++ .../app/common/views/components/messaging-room.vue | 377 ++++++++++++++ .../web/app/common/views/components/messaging.vue | 463 +++++++++++++++++ src/server/web/app/common/views/components/nav.vue | 41 ++ .../app/common/views/components/othello.game.vue | 324 ++++++++++++ .../common/views/components/othello.gameroom.vue | 42 ++ .../app/common/views/components/othello.room.vue | 297 +++++++++++ .../web/app/common/views/components/othello.vue | 311 +++++++++++ .../app/common/views/components/poll-editor.vue | 142 +++++ .../web/app/common/views/components/poll.vue | 124 +++++ .../web/app/common/views/components/post-html.ts | 137 +++++ .../web/app/common/views/components/post-menu.vue | 141 +++++ .../app/common/views/components/reaction-icon.vue | 28 + .../common/views/components/reaction-picker.vue | 191 +++++++ .../common/views/components/reactions-viewer.vue | 49 ++ .../web/app/common/views/components/signin.vue | 142 +++++ .../web/app/common/views/components/signup.vue | 287 ++++++++++ .../common/views/components/special-message.vue | 42 ++ .../common/views/components/stream-indicator.vue | 86 +++ .../web/app/common/views/components/switch.vue | 190 +++++++ .../web/app/common/views/components/time.vue | 76 +++ .../web/app/common/views/components/timer.vue | 49 ++ .../common/views/components/twitter-setting.vue | 66 +++ .../web/app/common/views/components/uploader.vue | 212 ++++++++ .../app/common/views/components/url-preview.vue | 142 +++++ src/server/web/app/common/views/components/url.vue | 66 +++ .../common/views/components/welcome-timeline.vue | 118 +++++ .../app/common/views/directives/autocomplete.ts | 194 +++++++ .../web/app/common/views/directives/index.ts | 5 + src/server/web/app/common/views/filters/bytes.ts | 8 + src/server/web/app/common/views/filters/index.ts | 2 + src/server/web/app/common/views/filters/number.ts | 5 + .../web/app/common/views/widgets/access-log.vue | 90 ++++ .../web/app/common/views/widgets/broadcast.vue | 161 ++++++ .../web/app/common/views/widgets/calendar.vue | 201 +++++++ .../web/app/common/views/widgets/donation.vue | 58 +++ src/server/web/app/common/views/widgets/index.ts | 25 + src/server/web/app/common/views/widgets/nav.vue | 31 ++ .../web/app/common/views/widgets/photo-stream.vue | 104 ++++ src/server/web/app/common/views/widgets/rss.vue | 93 ++++ .../app/common/views/widgets/server.cpu-memory.vue | 127 +++++ .../web/app/common/views/widgets/server.cpu.vue | 68 +++ .../web/app/common/views/widgets/server.disk.vue | 76 +++ .../web/app/common/views/widgets/server.info.vue | 25 + .../web/app/common/views/widgets/server.memory.vue | 76 +++ .../web/app/common/views/widgets/server.pie.vue | 61 +++ .../app/common/views/widgets/server.uptimes.vue | 46 ++ src/server/web/app/common/views/widgets/server.vue | 93 ++++ .../web/app/common/views/widgets/slideshow.vue | 159 ++++++ src/server/web/app/common/views/widgets/tips.vue | 108 ++++ .../web/app/common/views/widgets/version.vue | 28 + 84 files changed, 8706 insertions(+) create mode 100644 src/server/web/app/common/define-widget.ts create mode 100644 src/server/web/app/common/mios.ts create mode 100644 src/server/web/app/common/scripts/check-for-update.ts create mode 100644 src/server/web/app/common/scripts/compose-notification.ts create mode 100644 src/server/web/app/common/scripts/contains.ts create mode 100644 src/server/web/app/common/scripts/copy-to-clipboard.ts create mode 100644 src/server/web/app/common/scripts/date-stringify.ts create mode 100644 src/server/web/app/common/scripts/fuck-ad-block.ts create mode 100644 src/server/web/app/common/scripts/gcd.ts create mode 100644 src/server/web/app/common/scripts/get-kao.ts create mode 100644 src/server/web/app/common/scripts/get-median.ts create mode 100644 src/server/web/app/common/scripts/loading.ts create mode 100644 src/server/web/app/common/scripts/parse-search-query.ts create mode 100644 src/server/web/app/common/scripts/streaming/channel.ts create mode 100644 src/server/web/app/common/scripts/streaming/drive.ts create mode 100644 src/server/web/app/common/scripts/streaming/home.ts create mode 100644 src/server/web/app/common/scripts/streaming/messaging-index.ts create mode 100644 src/server/web/app/common/scripts/streaming/messaging.ts create mode 100644 src/server/web/app/common/scripts/streaming/othello-game.ts create mode 100644 src/server/web/app/common/scripts/streaming/othello.ts create mode 100644 src/server/web/app/common/scripts/streaming/requests.ts create mode 100644 src/server/web/app/common/scripts/streaming/server.ts create mode 100644 src/server/web/app/common/scripts/streaming/stream-manager.ts create mode 100644 src/server/web/app/common/scripts/streaming/stream.ts create mode 100644 src/server/web/app/common/views/components/autocomplete.vue create mode 100644 src/server/web/app/common/views/components/connect-failed.troubleshooter.vue create mode 100644 src/server/web/app/common/views/components/connect-failed.vue create mode 100644 src/server/web/app/common/views/components/ellipsis.vue create mode 100644 src/server/web/app/common/views/components/file-type-icon.vue create mode 100644 src/server/web/app/common/views/components/forkit.vue create mode 100644 src/server/web/app/common/views/components/index.ts create mode 100644 src/server/web/app/common/views/components/media-list.vue create mode 100644 src/server/web/app/common/views/components/messaging-room.form.vue create mode 100644 src/server/web/app/common/views/components/messaging-room.message.vue create mode 100644 src/server/web/app/common/views/components/messaging-room.vue create mode 100644 src/server/web/app/common/views/components/messaging.vue create mode 100644 src/server/web/app/common/views/components/nav.vue create mode 100644 src/server/web/app/common/views/components/othello.game.vue create mode 100644 src/server/web/app/common/views/components/othello.gameroom.vue create mode 100644 src/server/web/app/common/views/components/othello.room.vue create mode 100644 src/server/web/app/common/views/components/othello.vue create mode 100644 src/server/web/app/common/views/components/poll-editor.vue create mode 100644 src/server/web/app/common/views/components/poll.vue create mode 100644 src/server/web/app/common/views/components/post-html.ts create mode 100644 src/server/web/app/common/views/components/post-menu.vue create mode 100644 src/server/web/app/common/views/components/reaction-icon.vue create mode 100644 src/server/web/app/common/views/components/reaction-picker.vue create mode 100644 src/server/web/app/common/views/components/reactions-viewer.vue create mode 100644 src/server/web/app/common/views/components/signin.vue create mode 100644 src/server/web/app/common/views/components/signup.vue create mode 100644 src/server/web/app/common/views/components/special-message.vue create mode 100644 src/server/web/app/common/views/components/stream-indicator.vue create mode 100644 src/server/web/app/common/views/components/switch.vue create mode 100644 src/server/web/app/common/views/components/time.vue create mode 100644 src/server/web/app/common/views/components/timer.vue create mode 100644 src/server/web/app/common/views/components/twitter-setting.vue create mode 100644 src/server/web/app/common/views/components/uploader.vue create mode 100644 src/server/web/app/common/views/components/url-preview.vue create mode 100644 src/server/web/app/common/views/components/url.vue create mode 100644 src/server/web/app/common/views/components/welcome-timeline.vue create mode 100644 src/server/web/app/common/views/directives/autocomplete.ts create mode 100644 src/server/web/app/common/views/directives/index.ts create mode 100644 src/server/web/app/common/views/filters/bytes.ts create mode 100644 src/server/web/app/common/views/filters/index.ts create mode 100644 src/server/web/app/common/views/filters/number.ts create mode 100644 src/server/web/app/common/views/widgets/access-log.vue create mode 100644 src/server/web/app/common/views/widgets/broadcast.vue create mode 100644 src/server/web/app/common/views/widgets/calendar.vue create mode 100644 src/server/web/app/common/views/widgets/donation.vue create mode 100644 src/server/web/app/common/views/widgets/index.ts create mode 100644 src/server/web/app/common/views/widgets/nav.vue create mode 100644 src/server/web/app/common/views/widgets/photo-stream.vue create mode 100644 src/server/web/app/common/views/widgets/rss.vue create mode 100644 src/server/web/app/common/views/widgets/server.cpu-memory.vue create mode 100644 src/server/web/app/common/views/widgets/server.cpu.vue create mode 100644 src/server/web/app/common/views/widgets/server.disk.vue create mode 100644 src/server/web/app/common/views/widgets/server.info.vue create mode 100644 src/server/web/app/common/views/widgets/server.memory.vue create mode 100644 src/server/web/app/common/views/widgets/server.pie.vue create mode 100644 src/server/web/app/common/views/widgets/server.uptimes.vue create mode 100644 src/server/web/app/common/views/widgets/server.vue create mode 100644 src/server/web/app/common/views/widgets/slideshow.vue create mode 100644 src/server/web/app/common/views/widgets/tips.vue create mode 100644 src/server/web/app/common/views/widgets/version.vue (limited to 'src/server/web/app/common') diff --git a/src/server/web/app/common/define-widget.ts b/src/server/web/app/common/define-widget.ts new file mode 100644 index 0000000000..d8d29873a4 --- /dev/null +++ b/src/server/web/app/common/define-widget.ts @@ -0,0 +1,79 @@ +import Vue from 'vue'; + +export default function(data: { + name: string; + props?: () => T; +}) { + return Vue.extend({ + props: { + widget: { + type: Object + }, + isMobile: { + type: Boolean, + default: false + }, + isCustomizeMode: { + type: Boolean, + default: false + } + }, + computed: { + id(): string { + return this.widget.id; + } + }, + data() { + return { + props: data.props ? data.props() : {} as T, + bakedOldProps: null, + preventSave: false + }; + }, + created() { + if (this.props) { + Object.keys(this.props).forEach(prop => { + if (this.widget.data.hasOwnProperty(prop)) { + this.props[prop] = this.widget.data[prop]; + } + }); + } + + this.bakeProps(); + + this.$watch('props', newProps => { + if (this.preventSave) { + this.preventSave = false; + this.bakeProps(); + return; + } + if (this.bakedOldProps == JSON.stringify(newProps)) return; + + this.bakeProps(); + + if (this.isMobile) { + (this as any).api('i/update_mobile_home', { + id: this.id, + data: newProps + }).then(() => { + (this as any).os.i.account.client_settings.mobile_home.find(w => w.id == this.id).data = newProps; + }); + } else { + (this as any).api('i/update_home', { + id: this.id, + data: newProps + }).then(() => { + (this as any).os.i.account.client_settings.home.find(w => w.id == this.id).data = newProps; + }); + } + }, { + deep: true + }); + }, + methods: { + bakeProps() { + this.bakedOldProps = JSON.stringify(this.props); + } + } + }); +} diff --git a/src/server/web/app/common/mios.ts b/src/server/web/app/common/mios.ts new file mode 100644 index 0000000000..2c6c9988e7 --- /dev/null +++ b/src/server/web/app/common/mios.ts @@ -0,0 +1,578 @@ +import Vue from 'vue'; +import { EventEmitter } from 'eventemitter3'; +import * as merge from 'object-assign-deep'; +import * as uuid from 'uuid'; + +import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config'; +import Progress from './scripts/loading'; +import Connection from './scripts/streaming/stream'; +import { HomeStreamManager } from './scripts/streaming/home'; +import { DriveStreamManager } from './scripts/streaming/drive'; +import { ServerStreamManager } from './scripts/streaming/server'; +import { RequestsStreamManager } from './scripts/streaming/requests'; +import { MessagingIndexStreamManager } from './scripts/streaming/messaging-index'; +import { OthelloStreamManager } from './scripts/streaming/othello'; + +import Err from '../common/views/components/connect-failed.vue'; + +//#region api requests +let spinner = null; +let pending = 0; +//#endregion + +export type API = { + chooseDriveFile: (opts: { + title?: string; + currentFolder?: any; + multiple?: boolean; + }) => Promise; + + chooseDriveFolder: (opts: { + title?: string; + currentFolder?: any; + }) => Promise; + + dialog: (opts: { + title: string; + text: string; + actions?: Array<{ + text: string; + id?: string; + }>; + }) => Promise; + + input: (opts: { + title: string; + placeholder?: string; + default?: string; + }) => Promise; + + post: (opts?: { + reply?: any; + repost?: any; + }) => void; + + notify: (message: string) => void; +}; + +/** + * Misskey Operating System + */ +export default class MiOS extends EventEmitter { + /** + * Misskeyの /meta で取得できるメタ情報 + */ + private meta: { + data: { [x: string]: any }; + chachedAt: Date; + }; + + private isMetaFetching = false; + + public app: Vue; + + public new(vm, props) { + const w = new vm({ + parent: this.app, + propsData: props + }).$mount(); + document.body.appendChild(w.$el); + } + + /** + * A signing user + */ + public i: { [x: string]: any }; + + /** + * Whether signed in + */ + public get isSignedIn() { + return this.i != null; + } + + /** + * Whether is debug mode + */ + public get debug() { + return localStorage.getItem('debug') == 'true'; + } + + /** + * Whether enable sounds + */ + public get isEnableSounds() { + return localStorage.getItem('enableSounds') == 'true'; + } + + public apis: API; + + /** + * A connection manager of home stream + */ + public stream: HomeStreamManager; + + /** + * Connection managers + */ + public streams: { + driveStream: DriveStreamManager; + serverStream: ServerStreamManager; + requestsStream: RequestsStreamManager; + messagingIndexStream: MessagingIndexStreamManager; + othelloStream: OthelloStreamManager; + } = { + driveStream: null, + serverStream: null, + requestsStream: null, + messagingIndexStream: null, + othelloStream: null + }; + + /** + * A registration of service worker + */ + private swRegistration: ServiceWorkerRegistration = null; + + /** + * Whether should register ServiceWorker + */ + private shouldRegisterSw: boolean; + + /** + * ウィンドウシステム + */ + public windows = new WindowSystem(); + + /** + * MiOSインスタンスを作成します + * @param shouldRegisterSw ServiceWorkerを登録するかどうか + */ + constructor(shouldRegisterSw = false) { + super(); + + this.shouldRegisterSw = shouldRegisterSw; + + //#region BIND + this.log = this.log.bind(this); + this.logInfo = this.logInfo.bind(this); + this.logWarn = this.logWarn.bind(this); + this.logError = this.logError.bind(this); + this.init = this.init.bind(this); + this.api = this.api.bind(this); + this.getMeta = this.getMeta.bind(this); + this.registerSw = this.registerSw.bind(this); + //#endregion + + if (this.debug) { + (window as any).os = this; + } + } + + private googleMapsIniting = false; + + public getGoogleMaps() { + return new Promise((res, rej) => { + if ((window as any).google && (window as any).google.maps) { + res((window as any).google.maps); + } else { + this.once('init-google-maps', () => { + res((window as any).google.maps); + }); + + //#region load google maps api + if (!this.googleMapsIniting) { + this.googleMapsIniting = true; + (window as any).initGoogleMaps = () => { + this.emit('init-google-maps'); + }; + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`); + script.setAttribute('async', 'true'); + script.setAttribute('defer', 'true'); + head.appendChild(script); + } + //#endregion + } + }); + } + + public log(...args) { + if (!this.debug) return; + console.log.apply(null, args); + } + + public logInfo(...args) { + if (!this.debug) return; + console.info.apply(null, args); + } + + public logWarn(...args) { + if (!this.debug) return; + console.warn.apply(null, args); + } + + public logError(...args) { + if (!this.debug) return; + console.error.apply(null, args); + } + + public signout() { + localStorage.removeItem('me'); + document.cookie = `i=; domain=.${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; + location.href = '/'; + } + + /** + * Initialize MiOS (boot) + * @param callback A function that call when initialized + */ + public async init(callback) { + //#region Init stream managers + this.streams.serverStream = new ServerStreamManager(this); + this.streams.requestsStream = new RequestsStreamManager(this); + + this.once('signedin', () => { + // Init home stream manager + this.stream = new HomeStreamManager(this, this.i); + + // Init other stream manager + this.streams.driveStream = new DriveStreamManager(this, this.i); + this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.i); + this.streams.othelloStream = new OthelloStreamManager(this, this.i); + }); + //#endregion + + // ユーザーをフェッチしてコールバックする + const fetchme = (token, cb) => { + let me = null; + + // Return when not signed in + if (token == null) { + return done(); + } + + // Fetch user + fetch(`${apiUrl}/i`, { + method: 'POST', + body: JSON.stringify({ + i: token + }) + }) + // When success + .then(res => { + // When failed to authenticate user + if (res.status !== 200) { + return this.signout(); + } + + // Parse response + res.json().then(i => { + me = i; + me.account.token = token; + done(); + }); + }) + // When failure + .catch(() => { + // Render the error screen + document.body.innerHTML = '
'; + new Vue({ + render: createEl => createEl(Err) + }).$mount('#err'); + + Progress.done(); + }); + + function done() { + if (cb) cb(me); + } + }; + + // フェッチが完了したとき + const fetched = me => { + if (me) { + // デフォルトの設定をマージ + me.account.client_settings = Object.assign({ + fetchOnScroll: true, + showMaps: true, + showPostFormOnTopOfTl: false, + gradientWindowHeader: false + }, me.account.client_settings); + + // ローカルストレージにキャッシュ + localStorage.setItem('me', JSON.stringify(me)); + } + + this.i = me; + + this.emit('signedin'); + + // Finish init + callback(); + + //#region Post + + // Init service worker + if (this.shouldRegisterSw) this.registerSw(); + + //#endregion + }; + + // Get cached account data + const cachedMe = JSON.parse(localStorage.getItem('me')); + + // キャッシュがあったとき + if (cachedMe) { + // とりあえずキャッシュされたデータでお茶を濁して(?)おいて、 + fetched(cachedMe); + + // 後から新鮮なデータをフェッチ + fetchme(cachedMe.account.token, freshData => { + merge(cachedMe, freshData); + }); + } else { + // Get token from cookie + const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; + + fetchme(i, fetched); + } + } + + /** + * Register service worker + */ + private registerSw() { + // Check whether service worker and push manager supported + const isSwSupported = + ('serviceWorker' in navigator) && ('PushManager' in window); + + // Reject when browser not service worker supported + if (!isSwSupported) return; + + // Reject when not signed in to Misskey + if (!this.isSignedIn) return; + + // When service worker activated + navigator.serviceWorker.ready.then(registration => { + this.log('[sw] ready: ', registration); + + this.swRegistration = registration; + + // Options of pushManager.subscribe + // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters + const opts = { + // A boolean indicating that the returned push subscription + // will only be used for messages whose effect is made visible to the user. + userVisibleOnly: true, + + // A public key your push server will use to send + // messages to client apps via a push server. + applicationServerKey: urlBase64ToUint8Array(swPublickey) + }; + + // Subscribe push notification + this.swRegistration.pushManager.subscribe(opts).then(subscription => { + this.log('[sw] Subscribe OK:', subscription); + + function encode(buffer: ArrayBuffer) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); + } + + // Register + this.api('sw/register', { + endpoint: subscription.endpoint, + auth: encode(subscription.getKey('auth')), + publickey: encode(subscription.getKey('p256dh')) + }); + }) + // When subscribe failed + .catch(async (err: Error) => { + this.logError('[sw] Subscribe Error:', err); + + // 通知が許可されていなかったとき + if (err.name == 'NotAllowedError') { + this.logError('[sw] Subscribe failed due to notification not allowed'); + return; + } + + // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが + // 既に存在していることが原因でエラーになった可能性があるので、 + // そのサブスクリプションを解除しておく + const subscription = await this.swRegistration.pushManager.getSubscription(); + if (subscription) subscription.unsubscribe(); + }); + }); + + // Whether use raw version script + const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug) + || process.env.NODE_ENV != 'production'; + + // The path of service worker script + const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`; + + // Register service worker + navigator.serviceWorker.register(sw).then(registration => { + // 登録成功 + this.logInfo('[sw] Registration successful with scope: ', registration.scope); + }).catch(err => { + // 登録失敗 :( + this.logError('[sw] Registration failed: ', err); + }); + } + + public requests = []; + + /** + * Misskey APIにリクエストします + * @param endpoint エンドポイント名 + * @param data パラメータ + */ + public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> { + if (++pending === 1) { + spinner = document.createElement('div'); + spinner.setAttribute('id', 'wait'); + document.body.appendChild(spinner); + } + + // Append a credential + if (this.isSignedIn) (data as any).i = this.i.account.token; + + // TODO + //const viaStream = localStorage.getItem('enableExperimental') == 'true'; + + return new Promise((resolve, reject) => { + /*if (viaStream) { + const stream = this.stream.borrow(); + const id = Math.random().toString(); + stream.once(`api-res:${id}`, res => { + resolve(res); + }); + stream.send({ + type: 'api', + id, + endpoint, + data + }); + } else {*/ + const req = { + id: uuid(), + date: new Date(), + name: endpoint, + data, + res: null, + status: null + }; + + if (this.debug) { + this.requests.push(req); + } + + // Send request + fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: endpoint === 'signin' ? 'include' : 'omit', + cache: 'no-cache' + }).then(async (res) => { + if (--pending === 0) spinner.parentNode.removeChild(spinner); + + const body = res.status === 204 ? null : await res.json(); + + if (this.debug) { + req.status = res.status; + req.res = body; + } + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + /*}*/ + }); + } + + /** + * Misskeyのメタ情報を取得します + * @param force キャッシュを無視するか否か + */ + public getMeta(force = false) { + return new Promise<{ [x: string]: any }>(async (res, rej) => { + if (this.isMetaFetching) { + this.once('_meta_fetched_', () => { + res(this.meta.data); + }); + return; + } + + const expire = 1000 * 60; // 1min + + // forceが有効, meta情報を保持していない or 期限切れ + if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) { + this.isMetaFetching = true; + const meta = await this.api('meta'); + this.meta = { + data: meta, + chachedAt: new Date() + }; + this.isMetaFetching = false; + this.emit('_meta_fetched_'); + res(meta); + } else { + res(this.meta.data); + } + }); + } + + public connections: Connection[] = []; + + public registerStreamConnection(connection: Connection) { + this.connections.push(connection); + } + + public unregisterStreamConnection(connection: Connection) { + this.connections = this.connections.filter(c => c != connection); + } +} + +class WindowSystem extends EventEmitter { + public windows = new Set(); + + public add(window) { + this.windows.add(window); + this.emit('added', window); + } + + public remove(window) { + this.windows.delete(window); + this.emit('removed', window); + } + + public getAll() { + return this.windows; + } +} + +/** + * Convert the URL safe base64 string to a Uint8Array + * @param base64String base64 string + */ +function urlBase64ToUint8Array(base64String: string): Uint8Array { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} diff --git a/src/server/web/app/common/scripts/check-for-update.ts b/src/server/web/app/common/scripts/check-for-update.ts new file mode 100644 index 0000000000..81c1eb9812 --- /dev/null +++ b/src/server/web/app/common/scripts/check-for-update.ts @@ -0,0 +1,33 @@ +import MiOS from '../mios'; +import { version as current } from '../../config'; + +export default async function(mios: MiOS, force = false, silent = false) { + const meta = await mios.getMeta(force); + const newer = meta.version; + + if (newer != current) { + localStorage.setItem('should-refresh', 'true'); + localStorage.setItem('v', newer); + + // Clear cache (serive worker) + try { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage('clear'); + } + + navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => registration.unregister()); + }); + } catch (e) { + console.error(e); + } + + if (!silent) { + alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)); + } + + return newer; + } else { + return null; + } +} diff --git a/src/server/web/app/common/scripts/compose-notification.ts b/src/server/web/app/common/scripts/compose-notification.ts new file mode 100644 index 0000000000..e1dbd3bc13 --- /dev/null +++ b/src/server/web/app/common/scripts/compose-notification.ts @@ -0,0 +1,67 @@ +import getPostSummary from '../../../../common/get-post-summary'; +import getReactionEmoji from '../../../../common/get-reaction-emoji'; + +type Notification = { + title: string; + body: string; + icon: string; + onclick?: any; +}; + +// TODO: i18n + +export default function(type, data): Notification { + switch (type) { + case 'drive_file_created': + return { + title: 'ファイルがアップロードされました', + body: data.name, + icon: data.url + '?thumbnail&size=64' + }; + + case 'mention': + return { + title: `${data.user.name}さんから:`, + body: getPostSummary(data), + icon: data.user.avatar_url + '?thumbnail&size=64' + }; + + case 'reply': + return { + title: `${data.user.name}さんから返信:`, + body: getPostSummary(data), + icon: data.user.avatar_url + '?thumbnail&size=64' + }; + + case 'quote': + return { + title: `${data.user.name}さんが引用:`, + body: getPostSummary(data), + icon: data.user.avatar_url + '?thumbnail&size=64' + }; + + case 'reaction': + return { + title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`, + body: getPostSummary(data.post), + icon: data.user.avatar_url + '?thumbnail&size=64' + }; + + case 'unread_messaging_message': + return { + title: `${data.user.name}さんからメッセージ:`, + body: data.text, // TODO: getMessagingMessageSummary(data), + icon: data.user.avatar_url + '?thumbnail&size=64' + }; + + case 'othello_invited': + return { + title: '対局への招待があります', + body: `${data.parent.name}さんから`, + icon: data.parent.avatar_url + '?thumbnail&size=64' + }; + + default: + return null; + } +} diff --git a/src/server/web/app/common/scripts/contains.ts b/src/server/web/app/common/scripts/contains.ts new file mode 100644 index 0000000000..a5071b3f25 --- /dev/null +++ b/src/server/web/app/common/scripts/contains.ts @@ -0,0 +1,8 @@ +export default (parent, child) => { + let node = child.parentNode; + while (node) { + if (node == parent) return true; + node = node.parentNode; + } + return false; +}; diff --git a/src/server/web/app/common/scripts/copy-to-clipboard.ts b/src/server/web/app/common/scripts/copy-to-clipboard.ts new file mode 100644 index 0000000000..3d2741f8d7 --- /dev/null +++ b/src/server/web/app/common/scripts/copy-to-clipboard.ts @@ -0,0 +1,13 @@ +/** + * Clipboardに値をコピー(TODO: 文字列以外も対応) + */ +export default val => { + const form = document.createElement('textarea'); + form.textContent = val; + document.body.appendChild(form); + form.select(); + const result = document.execCommand('copy'); + document.body.removeChild(form); + + return result; +}; diff --git a/src/server/web/app/common/scripts/date-stringify.ts b/src/server/web/app/common/scripts/date-stringify.ts new file mode 100644 index 0000000000..e51de8833d --- /dev/null +++ b/src/server/web/app/common/scripts/date-stringify.ts @@ -0,0 +1,13 @@ +export default date => { + if (typeof date == 'string') date = new Date(date); + return ( + date.getFullYear() + '年' + + (date.getMonth() + 1) + '月' + + date.getDate() + '日' + + ' ' + + date.getHours() + '時' + + date.getMinutes() + '分' + + ' ' + + `(${['日', '月', '火', '水', '木', '金', '土'][date.getDay()]})` + ); +}; diff --git a/src/server/web/app/common/scripts/fuck-ad-block.ts b/src/server/web/app/common/scripts/fuck-ad-block.ts new file mode 100644 index 0000000000..9bcf7deeff --- /dev/null +++ b/src/server/web/app/common/scripts/fuck-ad-block.ts @@ -0,0 +1,21 @@ +require('fuckadblock'); + +declare const fuckAdBlock: any; + +export default (os) => { + function adBlockDetected() { + os.apis.dialog({ + title: '%fa:exclamation-triangle%広告ブロッカーを無効にしてください', + text: 'Misskeyは広告を掲載していませんが、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。', + actins: [{ + text: 'OK' + }] + }); + } + + if (fuckAdBlock === undefined) { + adBlockDetected(); + } else { + fuckAdBlock.onDetected(adBlockDetected); + } +}; diff --git a/src/server/web/app/common/scripts/gcd.ts b/src/server/web/app/common/scripts/gcd.ts new file mode 100644 index 0000000000..9a19f9da66 --- /dev/null +++ b/src/server/web/app/common/scripts/gcd.ts @@ -0,0 +1,2 @@ +const gcd = (a, b) => !b ? a : gcd(b, a % b); +export default gcd; diff --git a/src/server/web/app/common/scripts/get-kao.ts b/src/server/web/app/common/scripts/get-kao.ts new file mode 100644 index 0000000000..2168c5be88 --- /dev/null +++ b/src/server/web/app/common/scripts/get-kao.ts @@ -0,0 +1,5 @@ +export default () => [ + '(=^・・^=)', + 'v(‘ω’)v', + '🐡( \'-\' 🐡 )フグパンチ!!!!' +][Math.floor(Math.random() * 3)]; diff --git a/src/server/web/app/common/scripts/get-median.ts b/src/server/web/app/common/scripts/get-median.ts new file mode 100644 index 0000000000..91a415d5b2 --- /dev/null +++ b/src/server/web/app/common/scripts/get-median.ts @@ -0,0 +1,11 @@ +/** + * 中央値を求めます + * @param samples サンプル + */ +export default function(samples) { + if (!samples.length) return 0; + const numbers = samples.slice(0).sort((a, b) => a - b); + const middle = Math.floor(numbers.length / 2); + const isEven = numbers.length % 2 === 0; + return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle]; +} diff --git a/src/server/web/app/common/scripts/loading.ts b/src/server/web/app/common/scripts/loading.ts new file mode 100644 index 0000000000..c48e626648 --- /dev/null +++ b/src/server/web/app/common/scripts/loading.ts @@ -0,0 +1,21 @@ +const NProgress = require('nprogress'); +NProgress.configure({ + trickleSpeed: 500, + showSpinner: false +}); + +const root = document.getElementsByTagName('html')[0]; + +export default { + start: () => { + root.classList.add('progress'); + NProgress.start(); + }, + done: () => { + root.classList.remove('progress'); + NProgress.done(); + }, + set: val => { + NProgress.set(val); + } +}; diff --git a/src/server/web/app/common/scripts/parse-search-query.ts b/src/server/web/app/common/scripts/parse-search-query.ts new file mode 100644 index 0000000000..512791ecb0 --- /dev/null +++ b/src/server/web/app/common/scripts/parse-search-query.ts @@ -0,0 +1,53 @@ +export default function(qs: string) { + const q = { + text: '' + }; + + qs.split(' ').forEach(x => { + if (/^([a-z_]+?):(.+?)$/.test(x)) { + const [key, value] = x.split(':'); + switch (key) { + case 'user': + q['include_user_usernames'] = value.split(','); + break; + case 'exclude_user': + q['exclude_user_usernames'] = value.split(','); + break; + case 'follow': + q['following'] = value == 'null' ? null : value == 'true'; + break; + case 'reply': + q['reply'] = value == 'null' ? null : value == 'true'; + break; + case 'repost': + q['repost'] = value == 'null' ? null : value == 'true'; + break; + case 'media': + q['media'] = value == 'null' ? null : value == 'true'; + break; + case 'poll': + q['poll'] = value == 'null' ? null : value == 'true'; + break; + case 'until': + case 'since': + // YYYY-MM-DD + if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { + const [yyyy, mm, dd] = value.split('-'); + q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); + } + break; + default: + q[key] = value; + break; + } + } else { + q.text += x + ' '; + } + }); + + if (q.text) { + q.text = q.text.trim(); + } + + return q; +} diff --git a/src/server/web/app/common/scripts/streaming/channel.ts b/src/server/web/app/common/scripts/streaming/channel.ts new file mode 100644 index 0000000000..cab5f4edb4 --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/channel.ts @@ -0,0 +1,13 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +/** + * Channel stream connection + */ +export default class Connection extends Stream { + constructor(os: MiOS, channelId) { + super(os, 'channel', { + channel: channelId + }); + } +} diff --git a/src/server/web/app/common/scripts/streaming/drive.ts b/src/server/web/app/common/scripts/streaming/drive.ts new file mode 100644 index 0000000000..f11573685e --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/drive.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Drive stream connection + */ +export class DriveStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'drive', { + i: me.account.token + }); + } +} + +export class DriveStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new DriveStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/server/web/app/common/scripts/streaming/home.ts b/src/server/web/app/common/scripts/streaming/home.ts new file mode 100644 index 0000000000..ffcf6e5360 --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/home.ts @@ -0,0 +1,57 @@ +import * as merge from 'object-assign-deep'; + +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Home stream connection + */ +export class HomeStream extends Stream { + constructor(os: MiOS, me) { + super(os, '', { + i: me.account.token + }); + + // 最終利用日時を更新するため定期的にaliveメッセージを送信 + setInterval(() => { + this.send({ type: 'alive' }); + me.account.last_used_at = new Date(); + }, 1000 * 60); + + // 自分の情報が更新されたとき + this.on('i_updated', i => { + if (os.debug) { + console.log('I updated:', i); + } + merge(me, i); + }); + + // トークンが再生成されたとき + // このままではAPIが利用できないので強制的にサインアウトさせる + this.on('my_token_regenerated', () => { + alert('%i18n:common.my-token-regenerated%'); + os.signout(); + }); + } +} + +export class HomeStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new HomeStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/server/web/app/common/scripts/streaming/messaging-index.ts b/src/server/web/app/common/scripts/streaming/messaging-index.ts new file mode 100644 index 0000000000..24f0ce0c9f --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/messaging-index.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Messaging index stream connection + */ +export class MessagingIndexStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'messaging-index', { + i: me.account.token + }); + } +} + +export class MessagingIndexStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new MessagingIndexStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/server/web/app/common/scripts/streaming/messaging.ts b/src/server/web/app/common/scripts/streaming/messaging.ts new file mode 100644 index 0000000000..4c593deb31 --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/messaging.ts @@ -0,0 +1,20 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +/** + * Messaging stream connection + */ +export class MessagingStream extends Stream { + constructor(os: MiOS, me, otherparty) { + super(os, 'messaging', { + i: me.account.token, + otherparty + }); + + (this as any).on('_connected_', () => { + this.send({ + i: me.account.token + }); + }); + } +} diff --git a/src/server/web/app/common/scripts/streaming/othello-game.ts b/src/server/web/app/common/scripts/streaming/othello-game.ts new file mode 100644 index 0000000000..f34ef35147 --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/othello-game.ts @@ -0,0 +1,11 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +export class OthelloGameStream extends Stream { + constructor(os: MiOS, me, game) { + super(os, 'othello-game', { + i: me ? me.account.token : null, + game: game.id + }); + } +} diff --git a/src/server/web/app/common/scripts/streaming/othello.ts b/src/server/web/app/common/scripts/streaming/othello.ts new file mode 100644 index 0000000000..8c6f4b9c3c --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/othello.ts @@ -0,0 +1,31 @@ +import StreamManager from './stream-manager'; +import Stream from './stream'; +import MiOS from '../../mios'; + +export class OthelloStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'othello', { + i: me.account.token + }); + } +} + +export class OthelloStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new OthelloStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/server/web/app/common/scripts/streaming/requests.ts b/src/server/web/app/common/scripts/streaming/requests.ts new file mode 100644 index 0000000000..5bec30143f --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/requests.ts @@ -0,0 +1,30 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Requests stream connection + */ +export class RequestsStream extends Stream { + constructor(os: MiOS) { + super(os, 'requests'); + } +} + +export class RequestsStreamManager extends StreamManager { + private os: MiOS; + + constructor(os: MiOS) { + super(); + + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new RequestsStream(this.os); + } + + return this.connection; + } +} diff --git a/src/server/web/app/common/scripts/streaming/server.ts b/src/server/web/app/common/scripts/streaming/server.ts new file mode 100644 index 0000000000..3d35ef4d9d --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/server.ts @@ -0,0 +1,30 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Server stream connection + */ +export class ServerStream extends Stream { + constructor(os: MiOS) { + super(os, 'server'); + } +} + +export class ServerStreamManager extends StreamManager { + private os: MiOS; + + constructor(os: MiOS) { + super(); + + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new ServerStream(this.os); + } + + return this.connection; + } +} diff --git a/src/server/web/app/common/scripts/streaming/stream-manager.ts b/src/server/web/app/common/scripts/streaming/stream-manager.ts new file mode 100644 index 0000000000..568b8b0372 --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/stream-manager.ts @@ -0,0 +1,108 @@ +import { EventEmitter } from 'eventemitter3'; +import * as uuid from 'uuid'; +import Connection from './stream'; + +/** + * ストリーム接続を管理するクラス + * 複数の場所から同じストリームを利用する際、接続をまとめたりする + */ +export default abstract class StreamManager extends EventEmitter { + private _connection: T = null; + + private disposeTimerId: any; + + /** + * コネクションを必要としているユーザー + */ + private users = []; + + protected set connection(connection: T) { + this._connection = connection; + + if (this._connection == null) { + this.emit('disconnected'); + } else { + this.emit('connected', this._connection); + + this._connection.on('_connected_', () => { + this.emit('_connected_'); + }); + + this._connection.on('_disconnected_', () => { + this.emit('_disconnected_'); + }); + + this._connection.user = 'Managed'; + } + } + + protected get connection() { + return this._connection; + } + + /** + * コネクションを持っているか否か + */ + public get hasConnection() { + return this._connection != null; + } + + public get state(): string { + if (!this.hasConnection) return 'no-connection'; + return this._connection.state; + } + + /** + * コネクションを要求します + */ + public abstract getConnection(): T; + + /** + * 現在接続しているコネクションを取得します + */ + public borrow() { + return this._connection; + } + + /** + * コネクションを要求するためのユーザーIDを発行します + */ + public use() { + // タイマー解除 + if (this.disposeTimerId) { + clearTimeout(this.disposeTimerId); + this.disposeTimerId = null; + } + + // ユーザーID生成 + const userId = uuid(); + + this.users.push(userId); + + this._connection.user = `Managed (${ this.users.length })`; + + return userId; + } + + /** + * コネクションを利用し終わってもう必要ないことを通知します + * @param userId use で発行したユーザーID + */ + public dispose(userId) { + this.users = this.users.filter(id => id != userId); + + this._connection.user = `Managed (${ this.users.length })`; + + // 誰もコネクションの利用者がいなくなったら + if (this.users.length == 0) { + // また直ぐに再利用される可能性があるので、一定時間待ち、 + // 新たな利用者が現れなければコネクションを切断する + this.disposeTimerId = setTimeout(() => { + this.disposeTimerId = null; + + this.connection.close(); + this.connection = null; + }, 3000); + } + } +} diff --git a/src/server/web/app/common/scripts/streaming/stream.ts b/src/server/web/app/common/scripts/streaming/stream.ts new file mode 100644 index 0000000000..3912186ad3 --- /dev/null +++ b/src/server/web/app/common/scripts/streaming/stream.ts @@ -0,0 +1,137 @@ +import { EventEmitter } from 'eventemitter3'; +import * as uuid from 'uuid'; +import * as ReconnectingWebsocket from 'reconnecting-websocket'; +import { wsUrl } from '../../../config'; +import MiOS from '../../mios'; + +/** + * Misskey stream connection + */ +export default class Connection extends EventEmitter { + public state: string; + private buffer: any[]; + public socket: ReconnectingWebsocket; + public name: string; + public connectedAt: Date; + public user: string = null; + public in: number = 0; + public out: number = 0; + public inout: Array<{ + type: 'in' | 'out', + at: Date, + data: string + }> = []; + public id: string; + public isSuspended = false; + private os: MiOS; + + constructor(os: MiOS, endpoint, params?) { + super(); + + //#region BIND + this.onOpen = this.onOpen.bind(this); + this.onClose = this.onClose.bind(this); + this.onMessage = this.onMessage.bind(this); + this.send = this.send.bind(this); + this.close = this.close.bind(this); + //#endregion + + this.id = uuid(); + this.os = os; + this.name = endpoint; + this.state = 'initializing'; + this.buffer = []; + + const query = params + ? Object.keys(params) + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) + .join('&') + : null; + + this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`); + this.socket.addEventListener('open', this.onOpen); + this.socket.addEventListener('close', this.onClose); + this.socket.addEventListener('message', this.onMessage); + + // Register this connection for debugging + this.os.registerStreamConnection(this); + } + + /** + * Callback of when open connection + */ + private onOpen() { + this.state = 'connected'; + this.emit('_connected_'); + + this.connectedAt = new Date(); + + // バッファーを処理 + const _buffer = [].concat(this.buffer); // Shallow copy + this.buffer = []; // Clear buffer + _buffer.forEach(data => { + this.send(data); // Resend each buffered messages + + if (this.os.debug) { + this.out++; + this.inout.push({ type: 'out', at: new Date(), data }); + } + }); + } + + /** + * Callback of when close connection + */ + private onClose() { + this.state = 'reconnecting'; + this.emit('_disconnected_'); + } + + /** + * Callback of when received a message from connection + */ + private onMessage(message) { + if (this.isSuspended) return; + + if (this.os.debug) { + this.in++; + this.inout.push({ type: 'in', at: new Date(), data: message.data }); + } + + try { + const msg = JSON.parse(message.data); + if (msg.type) this.emit(msg.type, msg.body); + } catch (e) { + // noop + } + } + + /** + * Send a message to connection + */ + public send(data) { + if (this.isSuspended) return; + + // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する + if (this.state != 'connected') { + this.buffer.push(data); + return; + } + + if (this.os.debug) { + this.out++; + this.inout.push({ type: 'out', at: new Date(), data }); + } + + this.socket.send(JSON.stringify(data)); + } + + /** + * Close this connection + */ + public close() { + this.os.unregisterStreamConnection(this); + this.socket.removeEventListener('open', this.onOpen); + this.socket.removeEventListener('message', this.onMessage); + } +} diff --git a/src/server/web/app/common/views/components/autocomplete.vue b/src/server/web/app/common/views/components/autocomplete.vue new file mode 100644 index 0000000000..8afa291e3c --- /dev/null +++ b/src/server/web/app/common/views/components/autocomplete.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue b/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue new file mode 100644 index 0000000000..cadbd36ba4 --- /dev/null +++ b/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/server/web/app/common/views/components/connect-failed.vue b/src/server/web/app/common/views/components/connect-failed.vue new file mode 100644 index 0000000000..185250dbd8 --- /dev/null +++ b/src/server/web/app/common/views/components/connect-failed.vue @@ -0,0 +1,106 @@ + + + + + + diff --git a/src/server/web/app/common/views/components/ellipsis.vue b/src/server/web/app/common/views/components/ellipsis.vue new file mode 100644 index 0000000000..07349902de --- /dev/null +++ b/src/server/web/app/common/views/components/ellipsis.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/server/web/app/common/views/components/file-type-icon.vue b/src/server/web/app/common/views/components/file-type-icon.vue new file mode 100644 index 0000000000..b7e868d1f7 --- /dev/null +++ b/src/server/web/app/common/views/components/file-type-icon.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/server/web/app/common/views/components/forkit.vue b/src/server/web/app/common/views/components/forkit.vue new file mode 100644 index 0000000000..6f334b965a --- /dev/null +++ b/src/server/web/app/common/views/components/forkit.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/server/web/app/common/views/components/index.ts b/src/server/web/app/common/views/components/index.ts new file mode 100644 index 0000000000..b58ba37ecb --- /dev/null +++ b/src/server/web/app/common/views/components/index.ts @@ -0,0 +1,51 @@ +import Vue from 'vue'; + +import signin from './signin.vue'; +import signup from './signup.vue'; +import forkit from './forkit.vue'; +import nav from './nav.vue'; +import postHtml from './post-html'; +import poll from './poll.vue'; +import pollEditor from './poll-editor.vue'; +import reactionIcon from './reaction-icon.vue'; +import reactionsViewer from './reactions-viewer.vue'; +import time from './time.vue'; +import timer from './timer.vue'; +import mediaList from './media-list.vue'; +import uploader from './uploader.vue'; +import specialMessage from './special-message.vue'; +import streamIndicator from './stream-indicator.vue'; +import ellipsis from './ellipsis.vue'; +import messaging from './messaging.vue'; +import messagingRoom from './messaging-room.vue'; +import urlPreview from './url-preview.vue'; +import twitterSetting from './twitter-setting.vue'; +import fileTypeIcon from './file-type-icon.vue'; +import Switch from './switch.vue'; +import Othello from './othello.vue'; +import welcomeTimeline from './welcome-timeline.vue'; + +Vue.component('mk-signin', signin); +Vue.component('mk-signup', signup); +Vue.component('mk-forkit', forkit); +Vue.component('mk-nav', nav); +Vue.component('mk-post-html', postHtml); +Vue.component('mk-poll', poll); +Vue.component('mk-poll-editor', pollEditor); +Vue.component('mk-reaction-icon', reactionIcon); +Vue.component('mk-reactions-viewer', reactionsViewer); +Vue.component('mk-time', time); +Vue.component('mk-timer', timer); +Vue.component('mk-media-list', mediaList); +Vue.component('mk-uploader', uploader); +Vue.component('mk-special-message', specialMessage); +Vue.component('mk-stream-indicator', streamIndicator); +Vue.component('mk-ellipsis', ellipsis); +Vue.component('mk-messaging', messaging); +Vue.component('mk-messaging-room', messagingRoom); +Vue.component('mk-url-preview', urlPreview); +Vue.component('mk-twitter-setting', twitterSetting); +Vue.component('mk-file-type-icon', fileTypeIcon); +Vue.component('mk-switch', Switch); +Vue.component('mk-othello', Othello); +Vue.component('mk-welcome-timeline', welcomeTimeline); diff --git a/src/server/web/app/common/views/components/media-list.vue b/src/server/web/app/common/views/components/media-list.vue new file mode 100644 index 0000000000..64172ad0b4 --- /dev/null +++ b/src/server/web/app/common/views/components/media-list.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/server/web/app/common/views/components/messaging-room.form.vue b/src/server/web/app/common/views/components/messaging-room.form.vue new file mode 100644 index 0000000000..01886b19c8 --- /dev/null +++ b/src/server/web/app/common/views/components/messaging-room.form.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/server/web/app/common/views/components/messaging-room.message.vue b/src/server/web/app/common/views/components/messaging-room.message.vue new file mode 100644 index 0000000000..5f2eb1ba86 --- /dev/null +++ b/src/server/web/app/common/views/components/messaging-room.message.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/src/server/web/app/common/views/components/messaging-room.vue b/src/server/web/app/common/views/components/messaging-room.vue new file mode 100644 index 0000000000..6ff808b617 --- /dev/null +++ b/src/server/web/app/common/views/components/messaging-room.vue @@ -0,0 +1,377 @@ + + + + + diff --git a/src/server/web/app/common/views/components/messaging.vue b/src/server/web/app/common/views/components/messaging.vue new file mode 100644 index 0000000000..88574b94d1 --- /dev/null +++ b/src/server/web/app/common/views/components/messaging.vue @@ -0,0 +1,463 @@ + + + + + diff --git a/src/server/web/app/common/views/components/nav.vue b/src/server/web/app/common/views/components/nav.vue new file mode 100644 index 0000000000..8ce75d3529 --- /dev/null +++ b/src/server/web/app/common/views/components/nav.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/server/web/app/common/views/components/othello.game.vue b/src/server/web/app/common/views/components/othello.game.vue new file mode 100644 index 0000000000..414d819a55 --- /dev/null +++ b/src/server/web/app/common/views/components/othello.game.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/src/server/web/app/common/views/components/othello.gameroom.vue b/src/server/web/app/common/views/components/othello.gameroom.vue new file mode 100644 index 0000000000..38a25f6686 --- /dev/null +++ b/src/server/web/app/common/views/components/othello.gameroom.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/server/web/app/common/views/components/othello.room.vue b/src/server/web/app/common/views/components/othello.room.vue new file mode 100644 index 0000000000..3965414836 --- /dev/null +++ b/src/server/web/app/common/views/components/othello.room.vue @@ -0,0 +1,297 @@ + + + + + + + + + diff --git a/src/server/web/app/common/views/components/othello.vue b/src/server/web/app/common/views/components/othello.vue new file mode 100644 index 0000000000..d650322341 --- /dev/null +++ b/src/server/web/app/common/views/components/othello.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/src/server/web/app/common/views/components/poll-editor.vue b/src/server/web/app/common/views/components/poll-editor.vue new file mode 100644 index 0000000000..47d901d7b1 --- /dev/null +++ b/src/server/web/app/common/views/components/poll-editor.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/server/web/app/common/views/components/poll.vue b/src/server/web/app/common/views/components/poll.vue new file mode 100644 index 0000000000..8156c8bc58 --- /dev/null +++ b/src/server/web/app/common/views/components/poll.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/server/web/app/common/views/components/post-html.ts b/src/server/web/app/common/views/components/post-html.ts new file mode 100644 index 0000000000..98da86617d --- /dev/null +++ b/src/server/web/app/common/views/components/post-html.ts @@ -0,0 +1,137 @@ +import Vue from 'vue'; +import * as emojilib from 'emojilib'; +import getAcct from '../../../../../common/user/get-acct'; +import { url } from '../../../config'; +import MkUrl from './url.vue'; + +const flatten = list => list.reduce( + (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] +); + +export default Vue.component('mk-post-html', { + props: { + ast: { + type: Array, + required: true + }, + shouldBreak: { + type: Boolean, + default: true + }, + i: { + type: Object, + default: null + } + }, + render(createElement) { + const els = flatten((this as any).ast.map(token => { + switch (token.type) { + case 'text': + const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); + + if ((this as any).shouldBreak) { + const x = text.split('\n') + .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return x; + } else { + return createElement('span', text.replace(/\n/g, ' ')); + } + + case 'bold': + return createElement('strong', token.bold); + + case 'url': + return createElement(MkUrl, { + props: { + url: token.content, + target: '_blank' + } + }); + + case 'link': + return createElement('a', { + attrs: { + class: 'link', + href: token.url, + target: '_blank', + title: token.url + } + }, token.title); + + case 'mention': + return (createElement as any)('a', { + attrs: { + href: `${url}/@${getAcct(token)}`, + target: '_blank', + dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token) + }, + directives: [{ + name: 'user-preview', + value: token.content + }] + }, token.content); + + case 'hashtag': + return createElement('a', { + attrs: { + href: `${url}/search?q=${token.content}`, + target: '_blank' + } + }, token.content); + + case 'code': + return createElement('pre', [ + createElement('code', { + domProps: { + innerHTML: token.html + } + }) + ]); + + case 'inline-code': + return createElement('code', token.html); + + case 'quote': + const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); + + if ((this as any).shouldBreak) { + const x = text2.split('\n') + .map(t => [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return createElement('div', { + attrs: { + class: 'quote' + } + }, x); + } else { + return createElement('span', { + attrs: { + class: 'quote' + } + }, text2.replace(/\n/g, ' ')); + } + + case 'emoji': + const emoji = emojilib.lib[token.emoji]; + return createElement('span', emoji ? emoji.char : token.content); + + default: + console.log('unknown ast type:', token.type); + } + })); + + const _els = []; + els.forEach((el, i) => { + if (el.tag == 'br') { + if (els[i - 1].tag != 'div') { + _els.push(el); + } + } else { + _els.push(el); + } + }); + + return createElement('span', _els); + } +}); diff --git a/src/server/web/app/common/views/components/post-menu.vue b/src/server/web/app/common/views/components/post-menu.vue new file mode 100644 index 0000000000..a53680e55a --- /dev/null +++ b/src/server/web/app/common/views/components/post-menu.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/server/web/app/common/views/components/reaction-icon.vue b/src/server/web/app/common/views/components/reaction-icon.vue new file mode 100644 index 0000000000..7d24f4f9e9 --- /dev/null +++ b/src/server/web/app/common/views/components/reaction-icon.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/server/web/app/common/views/components/reaction-picker.vue b/src/server/web/app/common/views/components/reaction-picker.vue new file mode 100644 index 0000000000..df8100f2fc --- /dev/null +++ b/src/server/web/app/common/views/components/reaction-picker.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/server/web/app/common/views/components/reactions-viewer.vue b/src/server/web/app/common/views/components/reactions-viewer.vue new file mode 100644 index 0000000000..f6a27d9139 --- /dev/null +++ b/src/server/web/app/common/views/components/reactions-viewer.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/server/web/app/common/views/components/signin.vue b/src/server/web/app/common/views/components/signin.vue new file mode 100644 index 0000000000..2434684085 --- /dev/null +++ b/src/server/web/app/common/views/components/signin.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/server/web/app/common/views/components/signup.vue b/src/server/web/app/common/views/components/signup.vue new file mode 100644 index 0000000000..c2e78aa8a3 --- /dev/null +++ b/src/server/web/app/common/views/components/signup.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/src/server/web/app/common/views/components/special-message.vue b/src/server/web/app/common/views/components/special-message.vue new file mode 100644 index 0000000000..2fd4d6515e --- /dev/null +++ b/src/server/web/app/common/views/components/special-message.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/server/web/app/common/views/components/stream-indicator.vue b/src/server/web/app/common/views/components/stream-indicator.vue new file mode 100644 index 0000000000..1f18fa76ed --- /dev/null +++ b/src/server/web/app/common/views/components/stream-indicator.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/server/web/app/common/views/components/switch.vue b/src/server/web/app/common/views/components/switch.vue new file mode 100644 index 0000000000..19a4adc3de --- /dev/null +++ b/src/server/web/app/common/views/components/switch.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/src/server/web/app/common/views/components/time.vue b/src/server/web/app/common/views/components/time.vue new file mode 100644 index 0000000000..6e0d2b0dcb --- /dev/null +++ b/src/server/web/app/common/views/components/time.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/server/web/app/common/views/components/timer.vue b/src/server/web/app/common/views/components/timer.vue new file mode 100644 index 0000000000..a3c4f01b77 --- /dev/null +++ b/src/server/web/app/common/views/components/timer.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/server/web/app/common/views/components/twitter-setting.vue b/src/server/web/app/common/views/components/twitter-setting.vue new file mode 100644 index 0000000000..15968d20a6 --- /dev/null +++ b/src/server/web/app/common/views/components/twitter-setting.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/server/web/app/common/views/components/uploader.vue b/src/server/web/app/common/views/components/uploader.vue new file mode 100644 index 0000000000..73006b16e9 --- /dev/null +++ b/src/server/web/app/common/views/components/uploader.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/src/server/web/app/common/views/components/url-preview.vue b/src/server/web/app/common/views/components/url-preview.vue new file mode 100644 index 0000000000..e91e510550 --- /dev/null +++ b/src/server/web/app/common/views/components/url-preview.vue @@ -0,0 +1,142 @@ +