diff options
| author | こぴなたみぽ <Syuilotan@yahoo.co.jp> | 2017-06-15 04:52:33 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-06-15 04:52:33 +0900 |
| commit | cb4b120548a53700bca8baf52bfe42fe8e5f95ef (patch) | |
| tree | b0e4116756c0a9d6b7a06f4a5012fc91b6b5bc75 | |
| parent | chore(package): update @types/mongodb to version 2.2.4 (diff) | |
| parent | Merge pull request #552 from syuilo/greenkeeper/@types/node-7.0.31 (diff) | |
| download | misskey-cb4b120548a53700bca8baf52bfe42fe8e5f95ef.tar.gz misskey-cb4b120548a53700bca8baf52bfe42fe8e5f95ef.tar.bz2 misskey-cb4b120548a53700bca8baf52bfe42fe8e5f95ef.zip | |
Merge branch 'master' into greenkeeper/@types/mongodb-2.2.4
44 files changed, 1316 insertions, 367 deletions
diff --git a/.gitignore b/.gitignore index 6573bcb1e1..42b1bde94f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ npm-debug.log *.pem run.bat api-docs.json +package-lock.json @@ -16,10 +16,10 @@ Key features ---------------------------------------------------------------- * Automatically updated timeline * Private messages -* Free 1GB storage +* Free 1GB storage for each all users * Mobile device support (smartphone, tablet, etc) * Web API for third-party applications -* Twitter integration +* No ads and more! You can touch with your own eyes at https://misskey.xyz/. diff --git a/locales/en.yml b/locales/en.yml index 74ff52979c..a01392809e 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -45,6 +45,8 @@ common: mk-messaging-room: empty: "No conversations" + more: "More" + no-history: "There is no more history" resize-form: "Drag to resize" new-message: "New message" @@ -52,7 +54,7 @@ common: no-apps: "No apps" mk-error: - title: "Cannot connect to the server" + title: "Unable to connect to the server" description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" thanks: "Thank you for using Misskey." @@ -219,7 +221,7 @@ desktop: attach-media-from-local: "Attach media from your pc" attach-media-from-drive: "Attach media from the drive" attach-cancel: "Cancel attachment" - insert-the-cat: "Insert a cat" + insert-a-kao: "v(‘ω’)v" create-poll: "Create a poll" text-remain: "{} chars remaining" @@ -243,8 +245,13 @@ desktop: title: "Notifications" settings: "Notification settings" + mk-server-home-widget: + title: "Server info" + toggle: "Toggle views" + mk-activity-home-widget: title: "Activity" + toggle: "Toggle views" mk-user-recommendation-home-widget: title: "Recommended users" diff --git a/locales/ja.yml b/locales/ja.yml index dda0da5856..d55e690e91 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -45,6 +45,8 @@ common: mk-messaging-room: empty: "このユーザーと話したことはありません" + more: "もっと読む" + no-history: "これより過去の履歴はありません" resize-form: "ドラッグしてフォームの広さを調整" new-message: "新しいメッセージがあります" @@ -219,7 +221,7 @@ desktop: attach-media-from-local: "PCからメディアを添付" attach-media-from-drive: "ドライブからメディアを添付" attach-cancel: "添付取り消し" - insert-the-cat: "猫挿入" + insert-a-kao: "v(‘ω’)v" create-poll: "投票を作成" text-remain: "のこり{}文字" @@ -243,8 +245,13 @@ desktop: title: "通知" settings: "通知の設定" + mk-server-home-widget: + title: "サーバー情報" + toggle: "表示を切り替え" + mk-activity-home-widget: title: "アクティビティ" + toggle: "表示を切り替え" mk-user-recommendation-home-widget: title: "おすすめユーザー" diff --git a/package.json b/package.json index aaa5359801..fb703b5548 100644 --- a/package.json +++ b/package.json @@ -1,150 +1,153 @@ { - "name": "misskey", - "author": "syuilo <i@syuilo.com>", - "version": "0.0.2027", - "license": "MIT", - "description": "A miniblog-based SNS", - "bugs": "https://github.com/syuilo/misskey/issues", - "repository": "https://github.com/syuilo/misskey.git", - "main": "./built/index.js", - "private": true, - "scripts": { - "config": "node ./tools/init.js", - "start": "node ./built", - "debug": "DEBUG=misskey:* node ./built", - "swagger": "node ./swagger.js", - "build": "gulp build", - "rebuild": "gulp rebuild", - "clean": "gulp clean", - "cleanall": "gulp cleanall", - "lint": "gulp lint", - "test": "gulp test" - }, - "devDependencies": { - "@types/bcryptjs": "2.4.0", - "@types/body-parser": "1.16.3", - "@types/chai": "4.0.0", - "@types/chai-http": "0.0.30", - "@types/chalk": "0.4.31", - "@types/compression": "0.0.33", - "@types/cors": "2.8.1", - "@types/debug": "0.0.29", - "@types/deep-equal": "1.0.0", - "@types/elasticsearch": "5.0.13", - "@types/event-stream": "3.3.31", - "@types/express": "4.0.35", - "@types/gm": "1.17.31", - "@types/gulp": "4.0.3", - "@types/gulp-mocha": "0.0.30", - "@types/gulp-rename": "0.0.32", - "@types/gulp-replace": "0.0.30", - "@types/gulp-tslint": "3.6.31", - "@types/gulp-typescript": "2.13.0", - "@types/gulp-uglify": "0.0.30", - "@types/gulp-util": "3.0.31", - "@types/inquirer": "0.0.34", - "@types/is-root": "1.0.0", - "@types/is-url": "1.2.28", - "@types/js-yaml": "3.5.30", - "@types/mocha": "2.2.41", - "@types/mongodb": "2.2.4", - "@types/monk": "1.0.5", - "@types/morgan": "1.7.32", - "@types/ms": "0.7.29", - "@types/multer": "0.0.34", - "@types/node": "7.0.29", - "@types/ratelimiter": "2.1.28", - "@types/redis": "2.6.0", - "@types/request": "0.0.43", - "@types/rimraf": "0.0.28", - "@types/riot": "2.6.2", - "@types/serve-favicon": "2.2.28", - "@types/uuid": "3.0.0", - "@types/webpack": "2.2.15", - "@types/webpack-stream": "3.2.7", - "@types/websocket": "0.0.33", - "chai": "4.0.2", - "chai-http": "3.0.0", - "css-loader": "0.28.4", - "event-stream": "3.3.4", - "gulp": "3.9.1", - "gulp-cssnano": "2.1.2", - "gulp-imagemin": "3.3.0", - "gulp-mocha": "4.3.1", - "gulp-pug": "3.3.0", - "gulp-rename": "1.2.2", - "gulp-replace": "0.5.4", - "gulp-tslint": "8.1.1", - "gulp-typescript": "3.1.7", - "gulp-uglify": "3.0.0", - "gulp-util": "3.0.8", - "mocha": "3.4.2", - "riot-tag-loader": "1.0.0", - "string-replace-webpack-plugin": "0.1.3", - "style-loader": "0.18.2", - "stylus": "0.54.5", - "stylus-loader": "3.0.1", - "swagger-jsdoc": "1.9.4", - "tslint": "5.4.3", - "uglify-es": "3.0.15", - "uglify-es-webpack-plugin": "0.0.2", - "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony", - "webpack": "2.6.1" - }, - "dependencies": { - "accesses": "2.5.0", - "animejs": "2.0.2", - "autwh": "0.0.1", - "bcryptjs": "2.4.3", - "body-parser": "1.17.2", - "cafy": "2.4.0", - "chalk": "1.1.3", - "compression": "1.6.2", - "cors": "2.8.3", - "cropperjs": "1.0.0-rc.2", - "crypto": "0.0.3", - "debug": "2.6.8", - "deep-equal": "1.0.1", - "deepcopy": "0.6.3", - "download": "6.2.2", - "elasticsearch": "13.0.1", - "escape-regexp": "0.0.1", - "express": "4.15.3", - "file-type": "5.0.0", - "fuckadblock": "3.2.1", - "gm": "1.23.0", - "inquirer": "3.1.0", - "is-root": "1.0.0", - "is-url": "1.2.2", - "js-yaml": "3.8.4", - "mongodb": "2.2.28", - "monk": "6.0.0", - "morgan": "1.8.2", - "ms": "2.0.0", - "multer": "1.3.0", - "nprogress": "0.2.0", - "page": "1.7.1", - "pictograph": "2.0.4", - "prominence": "0.2.0", - "pug": "2.0.0-rc.2", - "ratelimiter": "3.0.3", - "recaptcha-promise": "0.1.2", - "reconnecting-websocket": "3.0.5", - "redis": "2.7.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "riot": "3.5.1", - "rndstr": "1.0.0", - "s-age": "1.1.0", - "serve-favicon": "2.4.3", - "summaly": "2.0.3", - "syuilo-password-strength": "0.0.1", - "tcp-port-used": "0.1.2", - "textarea-caret": "3.0.2", - "ts-node": "3.0.6", - "typescript": "2.3.4", - "uuid": "3.0.1", - "vhost": "3.0.2", - "websocket": "1.0.24" - } + "name": "misskey", + "author": "syuilo <i@syuilo.com>", + "version": "0.0.2097", + "license": "MIT", + "description": "A miniblog-based SNS", + "bugs": "https://github.com/syuilo/misskey/issues", + "repository": "https://github.com/syuilo/misskey.git", + "main": "./built/index.js", + "private": true, + "scripts": { + "config": "node ./tools/init.js", + "start": "node ./built", + "debug": "DEBUG=misskey:* node ./built", + "swagger": "node ./swagger.js", + "build": "gulp build", + "rebuild": "gulp rebuild", + "clean": "gulp clean", + "cleanall": "gulp cleanall", + "lint": "gulp lint", + "test": "gulp test" + }, + "devDependencies": { + "@types/bcryptjs": "2.4.0", + "@types/body-parser": "1.16.3", + "@types/chai": "4.0.0", + "@types/chai-http": "0.0.30", + "@types/chalk": "0.4.31", + "@types/compression": "0.0.33", + "@types/cors": "2.8.1", + "@types/debug": "0.0.29", + "@types/deep-equal": "1.0.0", + "@types/elasticsearch": "5.0.13", + "@types/event-stream": "3.3.31", + "@types/express": "4.0.35", + "@types/gm": "1.17.31", + "@types/gulp": "4.0.3", + "@types/gulp-mocha": "0.0.30", + "@types/gulp-rename": "0.0.32", + "@types/gulp-replace": "0.0.30", + "@types/gulp-tslint": "3.6.31", + "@types/gulp-typescript": "2.13.0", + "@types/gulp-uglify": "0.0.30", + "@types/gulp-util": "3.0.31", + "@types/inquirer": "0.0.34", + "@types/is-root": "1.0.0", + "@types/is-url": "1.2.28", + "@types/js-yaml": "3.5.30", + "@types/mocha": "2.2.41", + "@types/mongodb": "2.2.4", + "@types/monk": "1.0.5", + "@types/morgan": "1.7.32", + "@types/ms": "0.7.29", + "@types/multer": "0.0.34", + "@types/node": "7.0.31", + "@types/ratelimiter": "2.1.28", + "@types/redis": "2.6.0", + "@types/request": "0.0.43", + "@types/rimraf": "0.0.28", + "@types/riot": "2.6.2", + "@types/serve-favicon": "2.2.28", + "@types/uuid": "3.0.0", + "@types/webpack": "2.2.15", + "@types/webpack-stream": "3.2.7", + "@types/websocket": "0.0.33", + "chai": "4.0.2", + "chai-http": "3.0.0", + "css-loader": "0.28.4", + "event-stream": "3.3.4", + "gulp": "3.9.1", + "gulp-cssnano": "2.1.2", + "gulp-imagemin": "3.3.0", + "gulp-mocha": "4.3.1", + "gulp-pug": "3.3.0", + "gulp-rename": "1.2.2", + "gulp-replace": "0.5.4", + "gulp-tslint": "8.1.1", + "gulp-typescript": "3.1.7", + "gulp-uglify": "3.0.0", + "gulp-util": "3.0.8", + "mocha": "3.4.2", + "riot-tag-loader": "1.0.0", + "string-replace-webpack-plugin": "0.1.3", + "style-loader": "0.18.2", + "stylus": "0.54.5", + "stylus-loader": "3.0.1", + "swagger-jsdoc": "1.9.4", + "tslint": "5.4.3", + "uglify-es": "3.0.15", + "uglify-es-webpack-plugin": "0.0.2", + "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony", + "webpack": "2.6.1" + }, + "dependencies": { + "accesses": "2.5.0", + "animejs": "2.0.2", + "autwh": "0.0.1", + "bcryptjs": "2.4.3", + "body-parser": "1.17.2", + "cafy": "2.4.0", + "chalk": "1.1.3", + "compression": "1.6.2", + "cors": "2.8.3", + "cropperjs": "1.0.0-rc.2", + "crypto": "0.0.3", + "debug": "2.6.8", + "deep-equal": "1.0.1", + "deepcopy": "0.6.3", + "diskusage": "^0.2.2", + "download": "6.2.2", + "elasticsearch": "13.0.1", + "escape-regexp": "0.0.1", + "express": "4.15.3", + "file-type": "5.1.1", + "fuckadblock": "3.2.1", + "gm": "1.23.0", + "inquirer": "3.1.0", + "is-root": "1.0.0", + "is-url": "1.2.2", + "js-yaml": "3.8.4", + "mongodb": "2.2.28", + "monk": "6.0.0", + "morgan": "1.8.2", + "ms": "2.0.0", + "multer": "1.3.0", + "nprogress": "0.2.0", + "os-utils": "0.0.14", + "page": "1.7.1", + "pictograph": "2.0.4", + "prominence": "0.2.0", + "pug": "2.0.0-rc.2", + "ratelimiter": "3.0.3", + "recaptcha-promise": "0.1.2", + "reconnecting-websocket": "3.0.5", + "redis": "2.7.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "riot": "3.6.0", + "rndstr": "1.0.0", + "s-age": "1.1.0", + "serve-favicon": "2.4.3", + "summaly": "2.0.3", + "syuilo-password-strength": "0.0.1", + "tcp-port-used": "0.1.2", + "textarea-caret": "3.0.2", + "ts-node": "3.0.6", + "typescript": "2.3.4", + "uuid": "3.0.1", + "vhost": "3.0.2", + "websocket": "1.0.24", + "xev": "^2.0.0" + } } diff --git a/src/api/common/read-messaging-message.ts b/src/api/common/read-messaging-message.ts new file mode 100644 index 0000000000..3257ec8b07 --- /dev/null +++ b/src/api/common/read-messaging-message.ts @@ -0,0 +1,64 @@ +import * as mongo from 'mongodb'; +import Message from '../models/messaging-message'; +import { IMessagingMessage as IMessage } from '../models/messaging-message'; +import publishUserStream from '../event'; +import { publishMessagingStream } from '../event'; + +/** + * Mark as read message(s) + */ +export default ( + user: string | mongo.ObjectID, + otherparty: string | mongo.ObjectID, + message: string | string[] | IMessage | IMessage[] | mongo.ObjectID | mongo.ObjectID[] +) => new Promise<any>(async (resolve, reject) => { + + const userId = mongo.ObjectID.prototype.isPrototypeOf(user) + ? user + : new mongo.ObjectID(user); + + const otherpartyId = mongo.ObjectID.prototype.isPrototypeOf(otherparty) + ? otherparty + : new mongo.ObjectID(otherparty); + + const ids: mongo.ObjectID[] = Array.isArray(message) + ? mongo.ObjectID.prototype.isPrototypeOf(message[0]) + ? (message as mongo.ObjectID[]) + : typeof message[0] === 'string' + ? (message as string[]).map(m => new mongo.ObjectID(m)) + : (message as IMessage[]).map(m => m._id) + : mongo.ObjectID.prototype.isPrototypeOf(message) + ? [(message as mongo.ObjectID)] + : typeof message === 'string' + ? [new mongo.ObjectID(message)] + : [(message as IMessage)._id]; + + // Update documents + await Message.update({ + _id: { $in: ids }, + user_id: otherpartyId, + recipient_id: userId, + is_read: false + }, { + $set: { + is_read: true + } + }, { + multi: true + }); + + // Publish event + publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString())); + + // Calc count of my unread messages + const count = await Message + .count({ + recipient_id: userId, + is_read: false + }); + + if (count == 0) { + // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 + publishUserStream(userId, 'read_all_messaging_messages'); + } +}); diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts index b3a5c57f6c..7b270924eb 100644 --- a/src/api/endpoints/messaging/messages.ts +++ b/src/api/endpoints/messaging/messages.ts @@ -5,8 +5,7 @@ import $ from 'cafy'; import Message from '../../models/messaging-message'; import User from '../../models/user'; import serialize from '../../serializers/messaging-message'; -import publishUserStream from '../../event'; -import { publishMessagingStream } from '../../event'; +import read from '../../common/read-messaging-message'; /** * Get messages @@ -98,32 +97,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Mark as read all if (markAsRead) { - const ids = messages - .filter(m => m.is_read == false) - .filter(m => m.recipient_id.equals(user._id)) - .map(m => m._id); - - // Update documents - await Message.update({ - _id: { $in: ids } - }, { - $set: { is_read: true } - }, { - multi: true - }); - - // Publish event - publishMessagingStream(recipient._id, user._id, 'read', ids.map(id => id.toString())); - - const count = await Message - .count({ - recipient_id: user._id, - is_read: false - }); - - if (count == 0) { - // 全ての(いままで未読だった)メッセージを(これで)読みましたよというイベントを発行 - publishUserStream(user._id, 'read_all_messaging_messages'); - } + read(user._id, recipient._id, messages); } }); diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts index 05f9cda4cb..8af55d850c 100644 --- a/src/api/endpoints/messaging/messages/create.ts +++ b/src/api/endpoints/messaging/messages/create.ts @@ -93,13 +93,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => { publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj); publishUserStream(message.recipient_id, 'messaging_message', messageObj); - // 5秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する + // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する setTimeout(async () => { const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true }); if (!freshMessage.is_read) { publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj); } - }, 5000); + }, 3000); // Register to search database if (message.text && config.elasticsearch.enable) { diff --git a/src/api/endpoints/meta.ts b/src/api/endpoints/meta.ts index 98f812abd5..a3f1d50329 100644 --- a/src/api/endpoints/meta.ts +++ b/src/api/endpoints/meta.ts @@ -1,6 +1,7 @@ /** * Module dependencies */ +import * as os from 'os'; import version from '../../version'; import config from '../../conf'; @@ -41,6 +42,13 @@ module.exports = (params) => new Promise(async (res, rej) => { res({ maintainer: config.maintainer, version: version, - secure: config.https.enable + secure: config.https.enable, + machine: os.hostname(), + os: os.platform(), + node: process.version, + cpu: { + model: os.cpus()[0].model, + cores: os.cpus().length + } }); }); diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts index a3f2fb796d..73a5db47e2 100644 --- a/src/api/endpoints/users/search.ts +++ b/src/api/endpoints/users/search.ts @@ -42,7 +42,7 @@ async function byNative(res, rej, me, query, offset, max) { const users = await User .find({ $or: [{ - username_lower: new RegExp(escapedQuery.toLowerCase()) + username_lower: new RegExp(escapedQuery.replace('@', '').toLowerCase()) }, { name: new RegExp(escapedQuery) }] diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts index 81aee2cab8..18afa57e44 100644 --- a/src/api/models/messaging-message.ts +++ b/src/api/models/messaging-message.ts @@ -1,7 +1,12 @@ +import * as mongo from 'mongodb'; import db from '../../db/mongodb'; export default db.get('messaging_messages') as any; // fuck type definition +export interface IMessagingMessage { + _id: mongo.ObjectID; +} + export function isValidText(text: string): boolean { return text.length <= 1000 && text.trim() != ''; } diff --git a/src/api/stream/messaging.ts b/src/api/stream/messaging.ts index 71bf7a34c6..3f505cfafa 100644 --- a/src/api/stream/messaging.ts +++ b/src/api/stream/messaging.ts @@ -1,8 +1,6 @@ -import * as mongodb from 'mongodb'; import * as websocket from 'websocket'; import * as redis from 'redis'; -import Message from '../models/messaging-message'; -import { publishMessagingStream } from '../event'; +import read from '../common/read-messaging-message'; export default function messagingStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { const otherparty = request.resourceURL.query.otherparty; @@ -18,42 +16,8 @@ export default function messagingStream(request: websocket.request, connection: switch (msg.type) { case 'read': - if (!msg.id) { - return; - } - - const id = new mongodb.ObjectID(msg.id); - - // Fetch message - // SELECT _id, user_id, is_read - const message = await Message.findOne({ - _id: id, - recipient_id: user._id - }, { - fields: { - _id: true, - user_id: true, - is_read: true - } - }); - - if (message == null) { - return; - } - - if (message.is_read) { - return; - } - - // Update documents - await Message.update({ - _id: id - }, { - $set: { is_read: true } - }); - - // Publish event - publishMessagingStream(message.user_id, user._id, 'read', id.toString()); + if (!msg.id) return; + read(user._id, otherparty, msg.id); break; } }); diff --git a/src/api/stream/server.ts b/src/api/stream/server.ts new file mode 100644 index 0000000000..6de5337499 --- /dev/null +++ b/src/api/stream/server.ts @@ -0,0 +1,20 @@ +import * as websocket from 'websocket'; +import Xev from 'xev'; + +const ev = new Xev(); + +export default function homeStream(request: websocket.request, connection: websocket.connection): void { + const onStats = stats => { + connection.send(JSON.stringify({ + type: 'stats', + body: stats + })); + }; + + ev.addListener('stats', onStats); + + connection.on('close', () => { + console.log('yooo'); + ev.removeListener('stats', onStats); + }); +} diff --git a/src/api/streaming.ts b/src/api/streaming.ts index e1d79481d3..c71132100c 100644 --- a/src/api/streaming.ts +++ b/src/api/streaming.ts @@ -8,6 +8,7 @@ import isNativeToken from './common/is-native-token'; import homeStream from './stream/home'; import messagingStream from './stream/messaging'; +import serverStream from './stream/server'; module.exports = (server: http.Server) => { /** @@ -20,6 +21,11 @@ module.exports = (server: http.Server) => { ws.on('request', async (request) => { const connection = request.accept(); + if (request.resourceURL.pathname === '/server') { + serverStream(request, connection); + return; + } + const user = await authenticate(connection, request.resourceURL.query.i); if (user == null) { diff --git a/src/index.ts b/src/index.ts index 0b568b5d44..da83dde108 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,17 +12,20 @@ import * as chalk from 'chalk'; // import portUsed = require('tcp-port-used'); import isRoot = require('is-root'); import { master } from 'accesses'; +import Xev from 'xev'; import Logger from './utils/logger'; import ProgressBar from './utils/cli/progressbar'; import EnvironmentInfo from './utils/environmentInfo'; import MachineInfo from './utils/machineInfo'; import DependencyInfo from './utils/dependencyInfo'; +import stats from './utils/stats'; import { Config, path as configPath } from './config'; import loadConfig from './config'; const clusterLog = debug('misskey:cluster'); +const ev = new Xev(); process.title = 'Misskey'; @@ -35,6 +38,9 @@ main(); function main() { if (cluster.isMaster) { masterMain(); + + ev.mount(); + stats(); } else { workerMain(); } diff --git a/src/utils/stats.ts b/src/utils/stats.ts new file mode 100644 index 0000000000..cfb710f5e1 --- /dev/null +++ b/src/utils/stats.ts @@ -0,0 +1,27 @@ +import * as os from 'os'; +const osUtils = require('os-utils'); +import * as diskusage from 'diskusage'; +import Xev from 'xev'; + +const ev = new Xev(); + +/** + * Report stats regularly + */ +export default function() { + setInterval(() => { + osUtils.cpuUsage(cpuUsage => { + const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/'); + ev.emit('stats', { + cpu_usage: cpuUsage, + mem: { + total: os.totalmem(), + free: os.freemem() + }, + disk, + os_uptime: os.uptime(), + process_uptime: process.uptime() + }); + }); + }, 1000); +} diff --git a/src/web/app/base.pug b/src/web/app/base.pug index f7da0574bd..b1ca80deb9 100644 --- a/src/web/app/base.pug +++ b/src/web/app/base.pug @@ -20,7 +20,7 @@ html script include ./../../../built/web/assets/safe.js - script(src='https://use.fontawesome.com/22aba0df4f.js' async) + script(src='https://use.fontawesome.com/db921426cb.js' async) body noscript: p diff --git a/src/web/app/base.styl b/src/web/app/base.styl index 9fdb39a16c..81c039f0a3 100644 --- a/src/web/app/base.styl +++ b/src/web/app/base.styl @@ -30,14 +30,7 @@ html cursor progress !important #error - position fixed - z-index 32768 - top 0 - left 0 - width 100% - height 100% padding 32px - background #1269e2 color #fff hr diff --git a/src/web/app/common/scripts/bytes-to-size.js b/src/web/app/common/scripts/bytes-to-size.js index e143387141..af0268dbd0 100644 --- a/src/web/app/common/scripts/bytes-to-size.js +++ b/src/web/app/common/scripts/bytes-to-size.js @@ -1,6 +1,6 @@ -export default bytes => { +export default (bytes, digits = 0) => { var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes == 0) return '0Byte'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); - return Math.round(bytes / Math.pow(1024, i), 2) + sizes[i]; + return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; }; diff --git a/src/web/app/common/scripts/get-cat.js b/src/web/app/common/scripts/get-cat.js deleted file mode 100644 index cad42c88c8..0000000000 --- a/src/web/app/common/scripts/get-cat.js +++ /dev/null @@ -1 +0,0 @@ -export default () => '(=^・・^=)'; diff --git a/src/web/app/common/scripts/get-kao.js b/src/web/app/common/scripts/get-kao.js new file mode 100644 index 0000000000..0b77ee285a --- /dev/null +++ b/src/web/app/common/scripts/get-kao.js @@ -0,0 +1,5 @@ +export default () => [ + '(=^・・^=)', + 'v(‘ω’)v', + '🐡( '-' 🐡 )フグパンチ!!!!' +][Math.floor(Math.random() * 3)]; diff --git a/src/web/app/common/scripts/home-stream.js b/src/web/app/common/scripts/home-stream.js new file mode 100644 index 0000000000..24f13cd291 --- /dev/null +++ b/src/web/app/common/scripts/home-stream.js @@ -0,0 +1,18 @@ +'use strict'; + +import Stream from './stream'; + +/** + * Home stream connection + */ +class Connection extends Stream { + constructor(me) { + super('', { + i: me.token + }); + + this.on('i_updated', me.update); + } +} + +export default Connection; diff --git a/src/web/app/common/scripts/messaging-stream.js b/src/web/app/common/scripts/messaging-stream.js index 50d41c2be9..261525d5f6 100644 --- a/src/web/app/common/scripts/messaging-stream.js +++ b/src/web/app/common/scripts/messaging-stream.js @@ -1,42 +1,22 @@ -const ReconnectingWebSocket = require('reconnecting-websocket'); -import * as riot from 'riot'; -import CONFIG from './config'; +'use strict'; -class Connection { - constructor(me, otherparty) { - // BIND ----------------------------------- - this.onOpen = this.onOpen.bind(this); - this.onMessage = this.onMessage.bind(this); - this.close = this.close.bind(this); - // ---------------------------------------- - - this.event = riot.observable(); - this.me = me; - - const host = CONFIG.apiUrl.replace('http', 'ws'); - this.socket = new ReconnectingWebSocket(`${host}/messaging?i=${me.token}&otherparty=${otherparty}`); - this.socket.addEventListener('open', this.onOpen); - this.socket.addEventListener('message', this.onMessage); - } +import Stream from './stream'; - onOpen() { - this.socket.send(JSON.stringify({ - i: this.me.token - })); - } - - onMessage(message) { - try { - const msg = JSON.parse(message.data); - if (msg.type) this.event.trigger(msg.type, msg.body); - } catch(e) { - // noop - } - } +/** + * Messaging stream connection + */ +class Connection extends Stream { + constructor(me, otherparty) { + super('messaging', { + i: me.token, + otherparty + }); - close() { - this.socket.removeEventListener('open', this.onOpen); - this.socket.removeEventListener('message', this.onMessage); + this.on('_connected_', () => { + this.send({ + i: me.token + }); + }); } } diff --git a/src/web/app/common/scripts/server-stream.js b/src/web/app/common/scripts/server-stream.js new file mode 100644 index 0000000000..a1c466b35d --- /dev/null +++ b/src/web/app/common/scripts/server-stream.js @@ -0,0 +1,14 @@ +'use strict'; + +import Stream from './stream'; + +/** + * Server stream connection + */ +class Connection extends Stream { + constructor() { + super('server'); + } +} + +export default Connection; diff --git a/src/web/app/common/scripts/stream.js b/src/web/app/common/scripts/stream.js index ac3dd67153..981118b5de 100644 --- a/src/web/app/common/scripts/stream.js +++ b/src/web/app/common/scripts/stream.js @@ -5,10 +5,10 @@ import * as riot from 'riot'; import CONFIG from './config'; /** - * Home stream connection + * Misskey stream connection */ class Connection { - constructor(me) { + constructor(endpoint, params) { // BIND ----------------------------------- this.onOpen = this.onOpen.bind(this); this.onClose = this.onClose.bind(this); @@ -20,16 +20,19 @@ class Connection { riot.observable(this); this.state = 'initializing'; - this.me = me; this.buffer = []; const host = CONFIG.apiUrl.replace('http', 'ws'); - this.socket = new ReconnectingWebSocket(`${host}?i=${me.token}`); + const query = params + ? Object.keys(params) + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) + .join('&') + : null; + + this.socket = new ReconnectingWebSocket(`${host}/${endpoint}${query ? '?' + query : ''}`); this.socket.addEventListener('open', this.onOpen); this.socket.addEventListener('close', this.onClose); this.socket.addEventListener('message', this.onMessage); - - this.on('i_updated', me.update); } /** diff --git a/src/web/app/common/scripts/text-compiler.js b/src/web/app/common/scripts/text-compiler.js index 20ed1877da..0a9b8022df 100644 --- a/src/web/app/common/scripts/text-compiler.js +++ b/src/web/app/common/scripts/text-compiler.js @@ -22,7 +22,7 @@ export default (tokens, shouldBreak) => { case 'bold': return `<strong>${escape(token.bold)}</strong>`; case 'url': - return `<mk-url href="${escape(token.content)}" target="_blank"/>`; + return `<mk-url href="${escape(token.content)}" target="_blank"></mk-url>`; case 'link': return `<a class="link" href="${escape(token.url)}" target="_blank" title="${escape(token.url)}">${escape(token.title)}</a>`; case 'mention': diff --git a/src/web/app/common/tags/activity-table.tag b/src/web/app/common/tags/activity-table.tag index 49c5fe26cd..2ecfc3b110 100644 --- a/src/web/app/common/tags/activity-table.tag +++ b/src/web/app/common/tags/activity-table.tag @@ -2,7 +2,15 @@ <svg if={ data } ref="canvas" viewBox="0 0 53 7" preserveAspectRatio="none"> <rect each={ data } width="1" height="1" riot-x={ x } riot-y={ date.weekday } - fill={ color }></rect> + rx="1" ry="1" + fill={ color } + style="transform: scale({ v });"/> + <rect class="today" width="1" height="1" + riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday } + rx="1" ry="1" + fill="none" + stroke-width="0.1" + stroke="#f73520"/> </svg> <style> :scope @@ -16,7 +24,6 @@ > rect transform-origin center - transform scale(0.8) </style> <script> @@ -33,9 +40,9 @@ let x = 0; data.reverse().forEach(d => { d.x = x; - let v = d.total / this.peak; - if (v > 1) v = 1; - d.color = `hsl(180, ${v * 100}%, ${15 + ((1 - v) * 80)}%)`; + d.v = d.total / this.peak; + if (d.v > 1) d.v = 1; + d.color = `hsl(170, ${d.v * 100}%, ${15 + ((1 - d.v) * 80)}%)`; d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay(); if (d.date.weekday == 6) x++; }); diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag index c7491d1c43..e4e0272a49 100644 --- a/src/web/app/common/tags/error.tag +++ b/src/web/app/common/tags/error.tag @@ -1,36 +1,25 @@ <mk-error> - <!--i: i.fa.fa-times-circle--> <img src="/assets/error.jpg" alt=""/> <h1>%i18n:common.tags.mk-error.title%</h1> <p class="text">%i18n:common.tags.mk-error.description%</p> <p class="thanks">%i18n:common.tags.mk-error.thanks%</p> <style> :scope - position fixed - z-index 100000 - top 0 - left 0 + display block width 100% - height 100% + padding 32px 18px text-align center - background #f8f8f8 - - > i - display block - margin-top 64px - font-size 5em - color #6998a0 > img display block height 200px - margin 64px auto 0 auto + margin 0 auto pointer-events none user-select none > h1 display block - margin 32px auto 16px auto + margin 1.25em auto 0.65em auto font-size 1.5em color #555 @@ -43,13 +32,26 @@ > .thanks display block - margin 32px auto 0 auto - padding 32px 0 32px 0 + margin 2em auto 0 auto + padding 2em 0 0 0 max-width 600px font-size 0.9em font-style oblique color #aaa border-top solid 1px #eee + @media (max-width 500px) + padding 24px 18px + font-size 80% + + > img + height 150px + </style> + <script> + this.on('mount', () => { + document.title = 'Oops!'; + document.documentElement.style.background = '#f8f8f8'; + }); + </script> </mk-error> diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag index ec8138da04..d6db9070e2 100644 --- a/src/web/app/common/tags/messaging/message.tag +++ b/src/web/app/common/tags/messaging/message.tag @@ -1,4 +1,7 @@ -<mk-messaging-message data-is-me={ message.is_me }><a class="avatar-anchor" href={ '/' + message.user.username } title={ message.user.username } target="_blank"><img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=64' } alt=""/></a> +<mk-messaging-message data-is-me={ message.is_me }> + <a class="avatar-anchor" href={ '/' + message.user.username } title={ message.user.username } target="_blank"> + <img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=80' } alt=""/> + </a> <div class="content-container"> <div class="balloon"> <p class="read" if={ message.is_me && message.is_read }>%i18n:common.tags.mk-messaging-message.is-read%</p> diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag index bd49a4d782..b1082e26be 100644 --- a/src/web/app/common/tags/messaging/room.tag +++ b/src/web/app/common/tags/messaging/room.tag @@ -2,6 +2,10 @@ <div class="stream"> <p class="init" if={ init }><i class="fa fa-spinner fa-spin"></i>%i18n:common.loading%</p> <p class="empty" if={ !init && messages.length == 0 }><i class="fa fa-info-circle"></i>%i18n:common.tags.mk-messaging-room.empty%</p> + <p class="no-history" if={ !init && messages.length > 0 && !moreMessagesIsInStock }><i class="fa fa-flag"></i>%i18n:common.tags.mk-messaging-room.no-history%</p> + <button class="more { fetching: fetchingMoreMessages }" if={ moreMessagesIsInStock } onclick={ fetchMoreMessages } disabled={ fetchingMoreMessages }> + <i class="fa fa-spinner fa-pulse fa-fw" if={ fetchingMoreMessages }></i>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' } + </button> <virtual each={ message, i in messages }> <mk-messaging-message message={ message }/> <p class="date" if={ i != messages.length - 1 && message._date != messages[i + 1]._date }><span>{ messages[i + 1]._datetext }</span></p> @@ -20,6 +24,17 @@ max-width 600px margin 0 auto + > .init + width 100% + margin 0 + padding 16px 8px 8px 8px + text-align center + font-size 0.8em + color rgba(0, 0, 0, 0.4) + + i + margin-right 4px + > .empty width 100% margin 0 @@ -42,6 +57,27 @@ i margin-right 4px + > .more + display block + margin 16px auto + padding 0 12px + line-height 24px + color #fff + background rgba(0, 0, 0, 0.3) + border-radius 12px + + &:hover + background rgba(0, 0, 0, 0.4) + + &:active + background rgba(0, 0, 0, 0.5) + + &.fetching + cursor wait + + > i + margin-right 4px + > .message // something @@ -90,6 +126,9 @@ padding 8px 0 text-align center + &:empty + display none + > p display inline-block margin 0 @@ -137,24 +176,21 @@ this.connection = new MessagingStreamConnection(this.I, this.user.id); this.on('mount', () => { - this.connection.event.on('message', this.onMessage); - this.connection.event.on('read', this.onRead); + this.connection.on('message', this.onMessage); + this.connection.on('read', this.onRead); document.addEventListener('visibilitychange', this.onVisibilitychange); - this.api('messaging/messages', { - user_id: this.user.id - }).then(messages => { + this.fetchMessages().then(() => { this.init = false; - this.messages = messages.reverse(); this.update(); this.scrollToBottom(); }); }); this.on('unmount', () => { - this.connection.event.off('message', this.onMessage); - this.connection.event.off('read', this.onRead); + this.connection.off('message', this.onMessage); + this.connection.off('read', this.onRead); this.connection.close(); document.removeEventListener('visibilitychange', this.onVisibilitychange); @@ -174,10 +210,10 @@ this.messages.push(message); if (message.user_id != this.I.id && !document.hidden) { - this.connection.socket.send(JSON.stringify({ + this.connection.send({ type: 'read', id: message.id - })); + }); } this.update(); @@ -201,6 +237,39 @@ }); }; + this.fetchMoreMessages = () => { + this.update({ + fetchingMoreMessages: true + }); + this.fetchMessages().then(() => { + this.update({ + fetchingMoreMessages: false + }); + }); + }; + + this.fetchMessages = () => new Promise((resolve, reject) => { + const max = this.moreMessagesIsInStock ? 20 : 10; + + this.api('messaging/messages', { + user_id: this.user.id, + limit: max + 1, + max_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined + }).then(messages => { + if (messages.length == max + 1) { + this.moreMessagesIsInStock = true; + messages.pop(); + } else { + this.moreMessagesIsInStock = false; + } + + this.messages.unshift.apply(this.messages, messages.reverse()); + this.update(); + + resolve(); + }); + }); + this.isBottom = () => { const asobi = 32; const current = this.isNaked @@ -239,10 +308,10 @@ if (document.hidden) return; this.messages.forEach(message => { if (message.user_id !== this.I.id && !message.is_read) { - this.connection.socket.send(JSON.stringify({ + this.connection.send({ type: 'read', id: message.id - })); + }); } }); }; diff --git a/src/web/app/desktop/script.js b/src/web/app/desktop/script.js index f9f0896ddd..2e81147943 100644 --- a/src/web/app/desktop/script.js +++ b/src/web/app/desktop/script.js @@ -74,4 +74,18 @@ function registerNotifications(stream) { }); setTimeout(n.close.bind(n), 6000); }); + + stream.on('unread_messaging_message', message => { + const n = new Notification(`${message.user.name}さんからメッセージ:`, { + body: message.text, // TODO: getMessagingMessageSummary(message), + icon: message.user.avatar_url + '?thumbnail&size=64' + }); + n.onclick = () => { + n.close(); + riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), { + user: message.user + }); + }; + setTimeout(n.close.bind(n), 7000); + }); } diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag index ba201e1c29..29ce8760fd 100644 --- a/src/web/app/desktop/tags/home-widgets/activity.tag +++ b/src/web/app/desktop/tags/home-widgets/activity.tag @@ -1,11 +1,9 @@ <mk-activity-home-widget> <p class="title"><i class="fa fa-bar-chart"></i>%i18n:desktop.tags.mk-activity-home-widget.title%</p> + <button onclick={ toggle } title="%i18n:desktop.tags.mk-activity-home-widget.toggle%"><i class="fa fa-sort"></i></button> <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> - <svg if={ !initializing } ref="canvas" viewBox="0 0 21 7" preserveAspectRatio="none"> - <rect each={ data } width="1" height="1" - riot-x={ x } riot-y={ date.weekday } - fill={ color }></rect> - </svg> + <mk-activity-home-widget-calender if={ !initializing && view == 0 } data={ [].concat(data) }/> + <mk-activity-home-widget-chart if={ !initializing && view == 1 } data={ [].concat(data) }/> <style> :scope display block @@ -24,6 +22,23 @@ > i margin-right 4px + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + > .initializing margin 0 padding 16px @@ -33,44 +48,182 @@ > i margin-right 4px - > svg - display block - padding 10px - width 100% - - > rect - transform-origin center - transform scale(0.8) - </style> <script> this.mixin('i'); this.mixin('api'); this.initializing = true; + this.view = 0; this.on('mount', () => { this.api('aggregation/users/activity', { user_id: this.I.id, limit: 20 * 7 }).then(data => { - data.forEach(d => d.total = d.posts + d.replies + d.reposts); - this.peak = Math.max.apply(null, data.map(d => d.total)) / 2; - let x = 0; - data.reverse().forEach(d => { - d.x = x; - let v = d.total / this.peak; - if (v > 1) v = 1; - d.color = `hsl(180, ${v * 100}%, ${15 + ((1 - v) * 80)}%)`; - d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay(); - if (d.date.weekday == 6) x++; - }); - this.update({ initializing: false, data }); }); }); + + this.toggle = () => { + this.view++; + if (this.view == 2) this.view = 0; + }; </script> </mk-activity-home-widget> + +<mk-activity-home-widget-calender> + <svg viewBox="0 0 21 7" preserveAspectRatio="none"> + <rect each={ data } class="day" + width="1" height="1" + riot-x={ x } riot-y={ date.weekday } + rx="1" ry="1" + fill="transparent"> + <title>{ date.year }/{ date.month }/{ date.day }<br/>Post: { posts }, Reply: { replies }, Repost: { reposts }</title> + </rect> + <rect each={ data } + width="1" height="1" + riot-x={ x } riot-y={ date.weekday } + rx="1" ry="1" + fill={ color } + style="pointer-events: none; transform: scale({ v });"/> + <rect class="today" + width="1" height="1" + riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday } + rx="1" ry="1" + fill="none" + stroke-width="0.1" + stroke="#f73520"/> + </svg> + <style> + :scope + display block + + > svg + display block + padding 10px + width 100% + + > rect + transform-origin center + + &.day + &:hover + fill rgba(0, 0, 0, 0.05) + + </style> + <script> + this.data = this.opts.data; + this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); + const peak = Math.max.apply(null, this.data.map(d => d.total)); + + let x = 0; + this.data.reverse().forEach(d => { + d.x = x; + d.v = d.total / (peak / 2); + if (d.v > 1) d.v = 1; + d.color = `hsl(170, ${d.v * 100}%, ${15 + ((1 - d.v) * 80)}%)`; + d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay(); + if (d.date.weekday == 6) x++; + }); + </script> +</mk-activity-home-widget-calender> + +<mk-activity-home-widget-chart> + <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none" onmousedown={ onMousedown }> + <title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title> + <polyline + riot-points={ pointsPost } + fill="none" + stroke-width="1" + stroke="#41ddde"/> + <polyline + riot-points={ pointsReply } + fill="none" + stroke-width="1" + stroke="#f7796c"/> + <polyline + riot-points={ pointsRepost } + fill="none" + stroke-width="1" + stroke="#a1de41"/> + <polyline + riot-points={ pointsTotal } + fill="none" + stroke-width="1" + stroke="#555" + stroke-dasharray="2 2"/> + </svg> + <style> + :scope + display block + + > svg + display block + padding 10px + width 100% + cursor all-scroll + </style> + <script> + this.viewBoxX = 140; + this.viewBoxY = 60; + this.zoom = 1; + this.pos = 0; + + this.data = this.opts.data.reverse(); + this.data.forEach(d => d.total = d.posts + d.replies + d.reposts); + const peak = Math.max.apply(null, this.data.map(d => d.total)); + + this.on('mount', () => { + this.render(); + }); + + this.render = () => { + this.update({ + pointsPost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '), + pointsReply: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '), + pointsRepost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '), + pointsTotal: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ') + }); + }; + + this.onMousedown = e => { + e.preventDefault(); + + const clickX = e.clientX; + const clickY = e.clientY; + const baseZoom = this.zoom; + const basePos = this.pos; + + // 動かした時 + dragListen(me => { + let moveLeft = me.clientX - clickX; + let moveTop = me.clientY - clickY; + + this.zoom = baseZoom + (-moveTop / 20); + this.pos = basePos + moveLeft; + if (this.zoom < 1) this.zoom = 1; + if (this.pos > 0) this.pos = 0; + if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX); + + this.render(); + }); + }; + + function dragListen(fn) { + window.addEventListener('mousemove', fn); + window.addEventListener('mouseleave', dragClear.bind(null, fn)); + window.addEventListener('mouseup', dragClear.bind(null, fn)); + } + + function dragClear(fn) { + window.removeEventListener('mousemove', fn); + window.removeEventListener('mouseleave', dragClear); + window.removeEventListener('mouseup', dragClear); + } + </script> +</mk-activity-home-widget-chart> + diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag new file mode 100644 index 0000000000..bc8f313d53 --- /dev/null +++ b/src/web/app/desktop/tags/home-widgets/server.tag @@ -0,0 +1,510 @@ +<mk-server-home-widget> + <p class="title"><i class="fa fa-server"></i>%i18n:desktop.tags.mk-server-home-widget.title%</p> + <button onclick={ toggle } title="%i18n:desktop.tags.mk-server-home-widget.toggle%"><i class="fa fa-sort"></i></button> + <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p> + <mk-server-home-widget-cpu-and-memory-usage if={ !initializing } show={ view == 0 } connection={ connection }/> + <mk-server-home-widget-cpu if={ !initializing } show={ view == 1 } connection={ connection } meta={ meta }/> + <mk-server-home-widget-memory if={ !initializing } show={ view == 2 } connection={ connection }/> + <mk-server-home-widget-disk if={ !initializing } show={ view == 3 } connection={ connection }/> + <mk-server-home-widget-uptimes if={ !initializing } show={ view == 4 } connection={ connection }/> + <mk-server-home-widget-info if={ !initializing } show={ view == 5 } connection={ connection } meta={ meta }/> + <style> + :scope + display block + background #fff + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color #888 + box-shadow 0 1px rgba(0, 0, 0, 0.07) + + > i + margin-right 4px + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color #ccc + + &:hover + color #aaa + + &:active + color #999 + + > .initializing + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + + </style> + <script> + import Connection from '../../../common/scripts/server-stream'; + + this.mixin('api'); + + this.initializing = true; + this.view = 0; + this.connection = new Connection(); + + this.on('mount', () => { + this.api('meta').then(meta => { + this.update({ + initializing: false, + meta + }); + }); + }); + + this.on('unmount', () => { + this.connection.close(); + }); + + this.toggle = () => { + this.view++; + if (this.view == 6) this.view = 0; + }; + </script> +</mk-server-home-widget> + +<mk-server-home-widget-cpu-and-memory-usage> + <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> + <defs> + <linearGradient id={ cpuGradientId } x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="33%" stop-color="hsl(120, 80%, 70%)"></stop> + <stop offset="66%" stop-color="hsl(60, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask id={ cpuMaskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }> + <polygon + riot-points={ cpuPolygonPoints } + fill="#fff" + fill-opacity="0.5"/> + <polyline + riot-points={ cpuPolylinePoints } + fill="none" + stroke="#fff" + stroke-width="1"/> + </mask> + </defs> + <rect + x="-1" y="-1" + riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 } + style="stroke: none; fill: url(#{ cpuGradientId }); mask: url(#{ cpuMaskId })"/> + <text x="1" y="5">CPU <tspan>{ cpuP }%</tspan></text> + </svg> + <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> + <defs> + <linearGradient id={ memGradientId } x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="33%" stop-color="hsl(120, 80%, 70%)"></stop> + <stop offset="66%" stop-color="hsl(60, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask id={ memMaskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }> + <polygon + riot-points={ memPolygonPoints } + fill="#fff" + fill-opacity="0.5"/> + <polyline + riot-points={ memPolylinePoints } + fill="none" + stroke="#fff" + stroke-width="1"/> + </mask> + </defs> + <rect + x="-1" y="-1" + riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 } + style="stroke: none; fill: url(#{ memGradientId }); mask: url(#{ memMaskId })"/> + <text x="1" y="5">MEM <tspan>{ memP }%</tspan></text> + </svg> + <style> + :scope + display block + + > svg + display block + padding 10px + width 50% + float left + + &:first-child + padding-right 5px + + &:last-child + padding-left 5px + + > text + font-size 5px + fill rgba(0, 0, 0, 0.55) + + > tspan + opacity 0.5 + + &:after + content "" + display block + clear both + </style> + <script> + import uuid from '../../../common/scripts/uuid'; + + this.viewBoxX = 50; + this.viewBoxY = 30; + this.stats = []; + this.connection = this.opts.connection; + this.cpuGradientId = uuid(); + this.cpuMaskId = uuid(); + this.memGradientId = uuid(); + this.memMaskId = uuid(); + + this.on('mount', () => { + this.connection.on('stats', this.onStats); + }); + + this.on('unmount', () => { + this.connection.off('stats', this.onStats); + }); + + this.onStats = stats => { + stats.mem.used = stats.mem.total - stats.mem.free; + this.stats.push(stats); + if (this.stats.length > 50) this.stats.shift(); + + const cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' '); + const memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' '); + + const cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; + const memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; + + const cpuP = (stats.cpu_usage * 100).toFixed(0); + const memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); + + this.update({ + cpuPolylinePoints, + memPolylinePoints, + cpuPolygonPoints, + memPolygonPoints, + cpuP, + memP + }); + }; + </script> +</mk-server-home-widget-cpu-and-memory-usage> + +<mk-server-home-widget-cpu> + <mk-server-home-widget-pie ref="pie"/> + <div> + <p><i class="fa fa-microchip"></i>CPU</p> + <p>{ cores } Cores</p> + <p>{ model }</p> + </div> + <style> + :scope + display block + + > mk-server-home-widget-pie + padding 10px + height 100px + float left + + > div + float left + width calc(100% - 100px) + padding 10px 10px 10px 0 + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold + + > i + margin-right 4px + + &:after + content "" + display block + clear both + + </style> + <script> + this.cores = this.opts.meta.cpu.cores; + this.model = this.opts.meta.cpu.model; + this.connection = this.opts.connection; + + this.on('mount', () => { + this.connection.on('stats', this.onStats); + }); + + this.on('unmount', () => { + this.connection.off('stats', this.onStats); + }); + + this.onStats = stats => { + this.refs.pie.render(stats.cpu_usage); + }; + </script> +</mk-server-home-widget-cpu> + +<mk-server-home-widget-memory> + <mk-server-home-widget-pie ref="pie"/> + <div> + <p><i class="fa fa-flask"></i>Memory</p> + <p>Total: { bytesToSize(total, 1) }</p> + <p>Used: { bytesToSize(used, 1) }</p> + <p>Free: { bytesToSize(free, 1) }</p> + </div> + <style> + :scope + display block + + > mk-server-home-widget-pie + padding 10px + height 100px + float left + + > div + float left + width calc(100% - 100px) + padding 10px 10px 10px 0 + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold + + > i + margin-right 4px + + &:after + content "" + display block + clear both + + </style> + <script> + import bytesToSize from '../../../common/scripts/bytes-to-size'; + + this.connection = this.opts.connection; + this.bytesToSize = bytesToSize; + + this.on('mount', () => { + this.connection.on('stats', this.onStats); + }); + + this.on('unmount', () => { + this.connection.off('stats', this.onStats); + }); + + this.onStats = stats => { + stats.mem.used = stats.mem.total - stats.mem.free; + this.refs.pie.render(stats.mem.used / stats.mem.total); + + this.update({ + total: stats.mem.total, + used: stats.mem.used, + free: stats.mem.free + }); + }; + </script> +</mk-server-home-widget-memory> + +<mk-server-home-widget-disk> + <mk-server-home-widget-pie ref="pie"/> + <div> + <p><i class="fa fa-hdd-o"></i>Storage</p> + <p>Total: { bytesToSize(total, 1) }</p> + <p>Available: { bytesToSize(available, 1) }</p> + <p>Used: { bytesToSize(used, 1) }</p> + </div> + <style> + :scope + display block + + > mk-server-home-widget-pie + padding 10px + height 100px + float left + + > div + float left + width calc(100% - 100px) + padding 10px 10px 10px 0 + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold + + > i + margin-right 4px + + &:after + content "" + display block + clear both + + </style> + <script> + import bytesToSize from '../../../common/scripts/bytes-to-size'; + + this.connection = this.opts.connection; + this.bytesToSize = bytesToSize; + + this.on('mount', () => { + this.connection.on('stats', this.onStats); + }); + + this.on('unmount', () => { + this.connection.off('stats', this.onStats); + }); + + this.onStats = stats => { + stats.disk.used = stats.disk.total - stats.disk.free; + + this.refs.pie.render(stats.disk.used / stats.disk.total); + + this.update({ + total: stats.disk.total, + used: stats.disk.used, + available: stats.disk.available + }); + }; + </script> +</mk-server-home-widget-disk> + +<mk-server-home-widget-uptimes> + <p>Uptimes</p> + <p>Process: { process ? process.toFixed(0) : '---' }s</p> + <p>OS: { os ? os.toFixed(0) : '---' }s</p> + <style> + :scope + display block + padding 10px 14px + + > p + margin 0 + font-size 12px + color #505050 + + &:first-child + font-weight bold + + </style> + <script> + this.connection = this.opts.connection; + + this.on('mount', () => { + this.connection.on('stats', this.onStats); + }); + + this.on('unmount', () => { + this.connection.off('stats', this.onStats); + }); + + this.onStats = stats => { + this.update({ + process: stats.process_uptime, + os: stats.os_uptime + }); + }; + </script> +</mk-server-home-widget-uptimes> + +<mk-server-home-widget-info> + <p>Maintainer: <b>{ meta.maintainer }</b></p> + <p>Machine: { meta.machine }</p> + <p>Node: { meta.node }</p> + <style> + :scope + display block + padding 10px 14px + + > p + margin 0 + font-size 12px + color #505050 + + </style> + <script> + this.meta = this.opts.meta; + </script> +</mk-server-home-widget-info> + +<mk-server-home-widget-pie> + <svg viewBox="0 0 1 1" preserveAspectRatio="none"> + <circle + riot-r={ r } + cx="50%" cy="50%" + fill="none" + stroke-width="0.1" + stroke="rgba(0, 0, 0, 0.05)"/> + <circle + riot-r={ r } + cx="50%" cy="50%" + riot-stroke-dasharray={ Math.PI * (r * 2) } + riot-stroke-dashoffset={ strokeDashoffset } + fill="none" + stroke-width="0.1" + riot-stroke={ color }/> + <text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (p * 100).toFixed(0) }%</text> + </svg> + <style> + :scope + display block + + > svg + display block + height 100% + + > circle + transform-origin center + transform rotate(-90deg) + transition stroke-dashoffset 0.5s ease + + > text + font-size 0.15px + fill rgba(0, 0, 0, 0.6) + + </style> + <script> + this.r = 0.4; + + this.render = p => { + const color = `hsl(${180 - (p * 180)}, 80%, 70%)`; + const strokeDashoffset = (1 - p) * (Math.PI * (this.r * 2)); + + this.update({ + p, + color, + strokeDashoffset + }); + }; + </script> +</mk-server-home-widget-pie> diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag index 0e4a2ced10..37b2d3cf7e 100644 --- a/src/web/app/desktop/tags/home.tag +++ b/src/web/app/desktop/tags/home.tag @@ -77,6 +77,7 @@ 'notifications', 'user-recommendation', 'recommended-polls', + 'server', 'donation', 'nav', 'tips' diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index 1e0ebd44cf..177ba41293 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.js @@ -45,6 +45,7 @@ require('./home-widgets/version.tag'); require('./home-widgets/recommended-polls.tag'); require('./home-widgets/trends.tag'); require('./home-widgets/activity.tag'); +require('./home-widgets/server.tag'); require('./timeline.tag'); require('./messaging/window.tag'); require('./messaging/room-window.tag'); diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag index ce24ac4375..21e4fe7fa5 100644 --- a/src/web/app/desktop/tags/notifications.tag +++ b/src/web/app/desktop/tags/notifications.tag @@ -63,7 +63,7 @@ <p class="date" if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }><span><i class="fa fa-angle-up"></i>{ notification._datetext }</span><span><i class="fa fa-angle-down"></i>{ notifications[i + 1]._datetext }</span></p> </virtual> </div> - <button class="more" if={ moreNotifications } onclick={ fetchMoreNotifications } disabled={ fetchingMoreNotifications }> + <button class="more { fetching: fetchingMoreNotifications }" if={ moreNotifications } onclick={ fetchMoreNotifications } disabled={ fetchingMoreNotifications }> <i class="fa fa-spinner fa-pulse fa-fw" if={ fetchingMoreNotifications }></i>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' } </button> <p class="empty" if={ notifications.length == 0 && !loading }>ありません!</p> @@ -184,6 +184,9 @@ &:active background rgba(0, 0, 0, 0.05) + &.fetching + cursor wait + > i margin-right 4px diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag index 024cd095e5..6a363d67cd 100644 --- a/src/web/app/desktop/tags/post-form.tag +++ b/src/web/app/desktop/tags/post-form.tag @@ -16,7 +16,7 @@ <mk-uploader ref="uploader"/> <button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }><i class="fa fa-upload"></i></button> <button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" onclick={ selectFileFromDrive }><i class="fa fa-cloud"></i></button> - <button class="cat" title="%i18n:desktop.tags.mk-post-form.insert-the-cat%" onclick={ cat }><i class="fa fa-smile-o"></i></button> + <button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" onclick={ kao }><i class="fa fa-smile-o"></i></button> <button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" onclick={ addPoll }><i class="fa fa-pie-chart"></i></button> <p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p> <button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } onclick={ post }> @@ -258,7 +258,7 @@ [ref='upload'] [ref='drive'] - .cat + .kao .poll display inline-block cursor pointer @@ -306,7 +306,7 @@ </style> <script> - import getCat from '../../common/scripts/get-cat'; + import getKao from '../../common/scripts/get-kao'; import notify from '../scripts/notify'; import Autocomplete from '../scripts/autocomplete'; @@ -500,8 +500,8 @@ }); }; - this.cat = () => { - this.refs.text.value += getCat(); + this.kao = () => { + this.refs.text.value += getKao(); }; this.on('update', () => { diff --git a/src/web/app/init.js b/src/web/app/init.js index 17f9a2d09e..44391b8fcb 100644 --- a/src/web/app/init.js +++ b/src/web/app/init.js @@ -8,7 +8,8 @@ import * as riot from 'riot'; import api from './common/scripts/api'; import signout from './common/scripts/signout'; import checkForUpdate from './common/scripts/check-for-update'; -import Connection from './common/scripts/stream'; +import Connection from './common/scripts/home-stream'; +import Progress from './common/scripts/loading'; import mixin from './common/mixins'; import generateDefaultUserdata from './common/scripts/generate-default-userdata'; import CONFIG from './common/scripts/config'; @@ -95,7 +96,7 @@ export default callback => { }); } - // Init stream connection + // Init home stream connection const stream = me ? new Connection(me) : null; // ミックスイン初期化 @@ -147,9 +148,10 @@ function fetchme(token, cb) { me.data ? done() : init(); }); }, () => { // When failure - // Display error screen - riot.mount(document.body.appendChild( - document.createElement('mk-error'))); + // Render the error screen + document.body.innerHTML = '<mk-error />'; + riot.mount('*'); + Progress.done(); }); function done() { @@ -173,6 +175,7 @@ function panic(e) { console.error(e); // Display blue screen + document.documentElement.style.background = '#1269e2'; document.body.innerHTML = '<div id="error">' + '<h1>:( 致命的な問題が発生しました。</h1>' diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag index bafa5a4fca..e19325091d 100644 --- a/src/web/app/mobile/tags/drive.tag +++ b/src/web/app/mobile/tags/drive.tag @@ -39,7 +39,7 @@ </button> </div> <div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }> - <p if={ !folder == null }>%i18n:mobile.tags.mk-drive.nothing-in-drive%</p> + <p if={ folder == null }>%i18n:mobile.tags.mk-drive.nothing-in-drive%</p> <p if={ folder != null }>%i18n:mobile.tags.mk-drive.folder-is-empty%</p> </div> </div> diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag index a5ebf480d5..1169e3b9eb 100644 --- a/src/web/app/mobile/tags/page/drive.tag +++ b/src/web/app/mobile/tags/page/drive.tag @@ -63,7 +63,7 @@ document.title = title; // TODO: escape html characters in file.name - ui.trigger('title', '<mk-file-type-icon class="icon"/>' + file.name); + ui.trigger('title', '<mk-file-type-icon class="icon"></mk-file-type-icon>' + file.name); riot.mount('mk-file-type-icon', { type: file.type }); diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag index 239473703b..28c7796840 100644 --- a/src/web/app/mobile/tags/post-form.tag +++ b/src/web/app/mobile/tags/post-form.tag @@ -23,7 +23,7 @@ <mk-uploader ref="uploader"/> <button ref="upload" onclick={ selectFile }><i class="fa fa-upload"></i></button> <button ref="drive" onclick={ selectFileFromDrive }><i class="fa fa-cloud"></i></button> - <button class="cat" onclick={ cat }><i class="fa fa-smile-o"></i></button> + <button class="kao" onclick={ kao }><i class="fa fa-smile-o"></i></button> <button class="poll" onclick={ addPoll }><i class="fa fa-pie-chart"></i></button> <input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/> </div> @@ -165,7 +165,7 @@ > [ref='upload'] > [ref='drive'] - .cat + .kao .poll display inline-block padding 0 @@ -182,7 +182,7 @@ </style> <script> - import getCat from '../../common/scripts/get-cat'; + import getKao from '../../common/scripts/get-kao'; this.mixin('api'); @@ -285,8 +285,8 @@ this.unmount(); }; - this.cat = () => { - this.refs.text.value += getCat(); + this.kao = () => { + this.refs.text.value += getKao(); }; </script> </mk-post-form> diff --git a/src/web/app/mobile/tags/ui-header.tag b/src/web/app/mobile/tags/ui-header.tag index 264fa022d8..10b44b2153 100644 --- a/src/web/app/mobile/tags/ui-header.tag +++ b/src/web/app/mobile/tags/ui-header.tag @@ -4,6 +4,7 @@ <div class="backdrop"></div> <div class="content"> <button class="nav" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button> + <i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i> <h1 ref="title">Misskey</h1> <button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button> </div> @@ -74,6 +75,14 @@ > i transition all 0.2s ease + > i + position absolute + top 8px + left 8px + pointer-events none + font-size 10px + color $theme-color + > button:last-child display block position absolute @@ -90,14 +99,46 @@ <script> import ui from '../scripts/ui-event'; + this.mixin('api'); + this.mixin('stream'); + this.func = null; this.funcIcon = null; + this.on('mount', () => { + this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage); + + // Fetch count of unread messaging messages + this.api('messaging/unread').then(res => { + if (res.count > 0) { + this.update({ + hasUnreadMessagingMessages: true + }); + } + }); + }); + this.on('unmount', () => { + this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage); + ui.off('title', this.setTitle); ui.off('func', this.setFunc); }); + this.onReadAllMessagingMessages = () => { + this.update({ + hasUnreadMessagingMessages: false + }); + }; + + this.onUnreadMessagingMessage = () => { + this.update({ + hasUnreadMessagingMessages: true + }); + }; + this.setTitle = title => { this.refs.title.innerHTML = title; }; diff --git a/src/web/app/mobile/tags/ui-nav.tag b/src/web/app/mobile/tags/ui-nav.tag index 964eab9a58..76c43ade66 100644 --- a/src/web/app/mobile/tags/ui-nav.tag +++ b/src/web/app/mobile/tags/ui-nav.tag @@ -7,18 +7,18 @@ </a> <div class="links"> <ul> - <li class="home"><a href="/"><i class="icon fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="angle fa fa-angle-right"></i></a></li> - <li class="notifications"><a href="/i/notifications"><i class="icon fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="angle fa fa-angle-right"></i></a></li> - <li class="messaging"><a href="/i/messaging"><i class="icon fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="angle fa fa-angle-right"></i></a></li> + <li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li> + <li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="fa fa-angle-right"></i></a></li> + <li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li> </ul> <ul> - <li class="settings"><a onclick={ search }><i class="icon fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="angle fa fa-angle-right"></i></a></li> + <li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li> </ul> <ul> - <li class="settings"><a href="/i/drive"><i class="icon fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="angle fa fa-angle-right"></i></a></li> + <li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li> </ul> <ul> - <li class="settings"><a href="/i/settings"><i class="icon fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="angle fa fa-angle-right"></i></a></li> + <li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li> </ul> </div> <a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a> @@ -94,10 +94,16 @@ color #777 text-decoration none - > .icon + > i:first-child margin-right 0.5em - > .angle + > .i + margin-left 6px + vertical-align super + font-size 10px + color $theme-color + + > i:last-child position absolute top 0 right 0 @@ -120,6 +126,39 @@ <script> this.mixin('i'); this.mixin('page'); + this.mixin('api'); + this.mixin('stream'); + + this.on('mount', () => { + this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage); + + // Fetch count of unread messaging messages + this.api('messaging/unread').then(res => { + if (res.count > 0) { + this.update({ + hasUnreadMessagingMessages: true + }); + } + }); + }); + + this.on('unmount', () => { + this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages); + this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage); + }); + + this.onReadAllMessagingMessages = () => { + this.update({ + hasUnreadMessagingMessages: false + }); + }; + + this.onUnreadMessagingMessage = () => { + this.update({ + hasUnreadMessagingMessages: true + }); + }; this.search = () => { const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%'); diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag index 9f36e60741..81eb6ba2e4 100644 --- a/src/web/app/mobile/tags/user.tag +++ b/src/web/app/mobile/tags/user.tag @@ -5,7 +5,7 @@ <div class="body"> <div class="top"> <a class="avatar"> - <img src={ user.avatar_url + '?thumbnail&size=160' } alt="avatar"/> + <img src={ user.avatar_url + '?thumbnail&size=200' } alt="avatar"/> </a> <mk-follow-button if={ SIGNIN && I.id != user.id } user={ user }/> </div> |