mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-30 11:53:09 +02:00
Merge branch 'master' into greenkeeper/@types/redis-2.8.1
This commit is contained in:
commit
0877f5ce0a
107 changed files with 2507 additions and 763 deletions
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -2,6 +2,50 @@ ChangeLog (Release Notes)
|
|||
=========================
|
||||
主に notable な changes を書いていきます
|
||||
|
||||
2807 (2017/11/02)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2805 (2017/11/02)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2801 (2017/11/01)
|
||||
-----------------
|
||||
* チャンネルのWatch実装
|
||||
|
||||
2799 (2017/11/01)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2795 (2017/11/01)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2793 (2017/11/01)
|
||||
-----------------
|
||||
* なんか
|
||||
|
||||
2783 (2017/11/01)
|
||||
-----------------
|
||||
* なんか
|
||||
|
||||
2777 (2017/11/01)
|
||||
-----------------
|
||||
* 細かいブラッシュアップ
|
||||
|
||||
2775 (2017/11/01)
|
||||
-----------------
|
||||
* Fix: バグ修正
|
||||
|
||||
2769 (2017/11/01)
|
||||
-----------------
|
||||
* New: チャンネルシステム
|
||||
|
||||
2752 (2017/10/30)
|
||||
-----------------
|
||||
* New: 未読の通知がある場合アイコンを表示するように
|
||||
|
||||
2747 (2017/10/25)
|
||||
-----------------
|
||||
* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
|
||||
|
|
|
@ -25,6 +25,7 @@ Note that Misskey uses following subdomains:
|
|||
* **api**.*{primary domain}*
|
||||
* **auth**.*{primary domain}*
|
||||
* **about**.*{primary domain}*
|
||||
* **ch**.*{primary domain}*
|
||||
* **stats**.*{primary domain}*
|
||||
* **status**.*{primary domain}*
|
||||
* **dev**.*{primary domain}*
|
||||
|
|
|
@ -26,6 +26,7 @@ Misskeyは以下のサブドメインを使います:
|
|||
* **api**.*{primary domain}*
|
||||
* **auth**.*{primary domain}*
|
||||
* **about**.*{primary domain}*
|
||||
* **ch**.*{primary domain}*
|
||||
* **stats**.*{primary domain}*
|
||||
* **status**.*{primary domain}*
|
||||
* **dev**.*{primary domain}*
|
||||
|
|
|
@ -164,6 +164,19 @@ common:
|
|||
mk-uploader:
|
||||
waiting: "Waiting"
|
||||
|
||||
ch:
|
||||
tags:
|
||||
mk-index:
|
||||
new: "Create new channel"
|
||||
channel-title: "Channel title"
|
||||
|
||||
mk-channel-form:
|
||||
textarea: "Write here"
|
||||
upload: "Upload"
|
||||
drive: "Drive"
|
||||
post: "Do"
|
||||
posting: "Doing"
|
||||
|
||||
desktop:
|
||||
tags:
|
||||
mk-api-info:
|
||||
|
@ -241,6 +254,7 @@ desktop:
|
|||
mk-ui-header-nav:
|
||||
home: "Home"
|
||||
messaging: "Messages"
|
||||
ch: "Channels"
|
||||
info: "News"
|
||||
|
||||
mk-ui-header-search:
|
||||
|
@ -353,6 +367,9 @@ desktop:
|
|||
|
||||
mobile:
|
||||
tags:
|
||||
mk-selectdrive-page:
|
||||
select-file: "Select file(s)"
|
||||
|
||||
mk-drive-file-viewer:
|
||||
download: "Download"
|
||||
rename: "Rename"
|
||||
|
@ -389,6 +406,7 @@ mobile:
|
|||
|
||||
mk-notifications-page:
|
||||
notifications: "Notifications"
|
||||
read-all: "Are you sure you want to mark all unread notifications as read?"
|
||||
|
||||
mk-post-page:
|
||||
title: "Post"
|
||||
|
@ -490,6 +508,7 @@ mobile:
|
|||
home: "Home"
|
||||
notifications: "Notifications"
|
||||
messaging: "Messages"
|
||||
ch: "Channels"
|
||||
drive: "Drive"
|
||||
settings: "Settings"
|
||||
about: "About Misskey"
|
||||
|
|
|
@ -84,7 +84,7 @@ common:
|
|||
no-internet: "インターネットに接続されていません"
|
||||
no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。"
|
||||
no-server: "Misskeyのサーバーに接続できません"
|
||||
no-server-desc: "お使いのPCのネットワーク接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。"
|
||||
no-server-desc: "お使いのPCのインターネット接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。"
|
||||
success: "Misskeyのサーバーに接続できました"
|
||||
success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
|
||||
|
||||
|
@ -164,6 +164,19 @@ common:
|
|||
mk-uploader:
|
||||
waiting: "待機中"
|
||||
|
||||
ch:
|
||||
tags:
|
||||
mk-index:
|
||||
new: "チャンネルを作成"
|
||||
channel-title: "チャンネルのタイトル"
|
||||
|
||||
mk-channel-form:
|
||||
textarea: "書いて"
|
||||
upload: "アップロード"
|
||||
drive: "ドライブ"
|
||||
post: "やる"
|
||||
posting: "やってます"
|
||||
|
||||
desktop:
|
||||
tags:
|
||||
mk-api-info:
|
||||
|
@ -241,6 +254,7 @@ desktop:
|
|||
mk-ui-header-nav:
|
||||
home: "ホーム"
|
||||
messaging: "メッセージ"
|
||||
ch: "チャンネル"
|
||||
info: "お知らせ"
|
||||
|
||||
mk-ui-header-search:
|
||||
|
@ -353,6 +367,9 @@ desktop:
|
|||
|
||||
mobile:
|
||||
tags:
|
||||
mk-selectdrive-page:
|
||||
select-file: "ファイルを選択"
|
||||
|
||||
mk-drive-file-viewer:
|
||||
download: "ダウンロード"
|
||||
rename: "名前を変更"
|
||||
|
@ -389,6 +406,7 @@ mobile:
|
|||
|
||||
mk-notifications-page:
|
||||
notifications: "通知"
|
||||
read-all: "すべての通知を既読にしますか?"
|
||||
|
||||
mk-post-page:
|
||||
title: "投稿"
|
||||
|
@ -490,6 +508,7 @@ mobile:
|
|||
home: "ホーム"
|
||||
notifications: "通知"
|
||||
messaging: "メッセージ"
|
||||
ch: "チャンネル"
|
||||
search: "検索"
|
||||
drive: "ドライブ"
|
||||
settings: "設定"
|
||||
|
|
13
package.json
13
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "0.0.2747",
|
||||
"version": "0.0.2807",
|
||||
"license": "MIT",
|
||||
"description": "A miniblog-based SNS",
|
||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||
|
@ -53,10 +53,10 @@
|
|||
"@types/morgan": "1.7.33",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/multer": "1.3.2",
|
||||
"@types/node": "8.0.33",
|
||||
"@types/node": "8.0.47",
|
||||
"@types/ratelimiter": "2.1.28",
|
||||
"@types/redis": "2.8.1",
|
||||
"@types/request": "2.0.4",
|
||||
"@types/request": "2.0.7",
|
||||
"@types/rimraf": "2.0.0",
|
||||
"@types/riot": "3.6.0",
|
||||
"@types/serve-favicon": "2.2.28",
|
||||
|
@ -91,10 +91,11 @@
|
|||
"tslint": "5.7.0",
|
||||
"uglify-es": "3.0.27",
|
||||
"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
|
||||
"uglifyjs-webpack-plugin": "1.0.0-beta.2",
|
||||
"uglifyjs-webpack-plugin": "1.0.1",
|
||||
"webpack": "3.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prezzemolo/rap": "0.1.2",
|
||||
"accesses": "2.5.0",
|
||||
"animejs": "2.2.0",
|
||||
"autwh": "0.0.1",
|
||||
|
@ -114,7 +115,7 @@
|
|||
"elasticsearch": "13.3.1",
|
||||
"escape-regexp": "0.0.1",
|
||||
"express": "4.15.4",
|
||||
"file-type": "6.2.0",
|
||||
"file-type": "7.2.0",
|
||||
"fuckadblock": "3.2.1",
|
||||
"gm": "1.23.0",
|
||||
"inquirer": "3.3.0",
|
||||
|
@ -140,7 +141,7 @@
|
|||
"redis": "2.8.0",
|
||||
"request": "2.83.0",
|
||||
"rimraf": "2.6.2",
|
||||
"riot": "3.7.3",
|
||||
"riot": "3.7.4",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.0",
|
||||
"serve-favicon": "2.4.5",
|
||||
|
|
|
@ -4,14 +4,27 @@ import * as gm from 'gm';
|
|||
import * as debug from 'debug';
|
||||
import fileType = require('file-type');
|
||||
import prominence = require('prominence');
|
||||
import DriveFile from '../models/drive-file';
|
||||
import DriveFile, { getGridFSBucket } from '../models/drive-file';
|
||||
import DriveFolder from '../models/drive-folder';
|
||||
import serialize from '../serializers/drive-file';
|
||||
import event from '../event';
|
||||
import config from '../../conf';
|
||||
import { Duplex } from 'stream';
|
||||
|
||||
const log = debug('misskey:register-drive-file');
|
||||
|
||||
const addToGridFS = (name, binary, metadata): Promise<any> => new Promise(async (resolve, reject) => {
|
||||
const dataStream = new Duplex();
|
||||
dataStream.push(binary);
|
||||
dataStream.push(null);
|
||||
|
||||
const bucket = await getGridFSBucket();
|
||||
const writeStream = bucket.openUploadStream(name, { metadata });
|
||||
writeStream.once('finish', (doc) => { resolve(doc); });
|
||||
writeStream.on('error', reject);
|
||||
dataStream.pipe(writeStream);
|
||||
});
|
||||
|
||||
/**
|
||||
* Add file to drive
|
||||
*
|
||||
|
@ -58,7 +71,7 @@ export default (
|
|||
|
||||
// Generate hash
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.createHash('md5')
|
||||
.update(data)
|
||||
.digest('hex') as string;
|
||||
|
||||
|
@ -67,8 +80,8 @@ export default (
|
|||
if (!force) {
|
||||
// Check if there is a file with the same hash
|
||||
const much = await DriveFile.findOne({
|
||||
user_id: user._id,
|
||||
hash: hash
|
||||
md5: hash,
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (much !== null) {
|
||||
|
@ -82,13 +95,13 @@ export default (
|
|||
// Calculate drive usage
|
||||
const usage = ((await DriveFile
|
||||
.aggregate([
|
||||
{ $match: { user_id: user._id } },
|
||||
{ $match: { 'metadata.user_id': user._id } },
|
||||
{ $project: {
|
||||
datasize: true
|
||||
length: true
|
||||
}},
|
||||
{ $group: {
|
||||
_id: null,
|
||||
usage: { $sum: '$datasize' }
|
||||
usage: { $sum: '$length' }
|
||||
}}
|
||||
]))[0] || {
|
||||
usage: 0
|
||||
|
@ -131,21 +144,15 @@ export default (
|
|||
}
|
||||
|
||||
// Create DriveFile document
|
||||
const file = await DriveFile.insert({
|
||||
created_at: new Date(),
|
||||
const file = await addToGridFS(`${user._id}/${name}`, data, {
|
||||
user_id: user._id,
|
||||
folder_id: folder !== null ? folder._id : null,
|
||||
data: data,
|
||||
datasize: size,
|
||||
type: mime,
|
||||
name: name,
|
||||
comment: comment,
|
||||
hash: hash,
|
||||
properties: properties
|
||||
});
|
||||
|
||||
delete file.data;
|
||||
|
||||
log(`drive file has been created ${file._id}`);
|
||||
|
||||
resolve(file);
|
||||
|
|
52
src/api/common/read-notification.ts
Normal file
52
src/api/common/read-notification.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import { default as Notification, INotification } from '../models/notification';
|
||||
import publishUserStream from '../event';
|
||||
|
||||
/**
|
||||
* Mark as read notification(s)
|
||||
*/
|
||||
export default (
|
||||
user: string | mongo.ObjectID,
|
||||
message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[]
|
||||
) => new Promise<any>(async (resolve, reject) => {
|
||||
|
||||
const userId = mongo.ObjectID.prototype.isPrototypeOf(user)
|
||||
? user
|
||||
: new mongo.ObjectID(user);
|
||||
|
||||
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 INotification[]).map(m => m._id)
|
||||
: mongo.ObjectID.prototype.isPrototypeOf(message)
|
||||
? [(message as mongo.ObjectID)]
|
||||
: typeof message === 'string'
|
||||
? [new mongo.ObjectID(message)]
|
||||
: [(message as INotification)._id];
|
||||
|
||||
// Update documents
|
||||
await Notification.update({
|
||||
_id: { $in: ids },
|
||||
is_read: false
|
||||
}, {
|
||||
$set: {
|
||||
is_read: true
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Calc count of my unread notifications
|
||||
const count = await Notification
|
||||
.count({
|
||||
notifiee_id: userId,
|
||||
is_read: false
|
||||
});
|
||||
|
||||
if (count == 0) {
|
||||
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
|
||||
publishUserStream(userId, 'read_all_notifications');
|
||||
}
|
||||
});
|
|
@ -195,6 +195,11 @@ const endpoints: Endpoint[] = [
|
|||
withCredential: true,
|
||||
kind: 'notification-read'
|
||||
},
|
||||
{
|
||||
name: 'notifications/get_unread_count',
|
||||
withCredential: true,
|
||||
kind: 'notification-read'
|
||||
},
|
||||
{
|
||||
name: 'notifications/delete',
|
||||
withCredential: true,
|
||||
|
@ -205,11 +210,6 @@ const endpoints: Endpoint[] = [
|
|||
withCredential: true,
|
||||
kind: 'notification-write'
|
||||
},
|
||||
{
|
||||
name: 'notifications/mark_as_read',
|
||||
withCredential: true,
|
||||
kind: 'notification-write'
|
||||
},
|
||||
{
|
||||
name: 'notifications/mark_as_read_all',
|
||||
withCredential: true,
|
||||
|
@ -474,8 +474,33 @@ const endpoints: Endpoint[] = [
|
|||
name: 'messaging/messages/create',
|
||||
withCredential: true,
|
||||
kind: 'messaging-write'
|
||||
},
|
||||
{
|
||||
name: 'channels/create',
|
||||
withCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 3,
|
||||
minInterval: ms('10seconds')
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
name: 'channels/show'
|
||||
},
|
||||
{
|
||||
name: 'channels/posts'
|
||||
},
|
||||
{
|
||||
name: 'channels/watch',
|
||||
withCredential: true
|
||||
},
|
||||
{
|
||||
name: 'channels/unwatch',
|
||||
withCredential: true
|
||||
},
|
||||
{
|
||||
name: 'channels'
|
||||
},
|
||||
];
|
||||
|
||||
export default endpoints;
|
||||
|
|
|
@ -19,7 +19,7 @@ module.exports = params => new Promise(async (res, rej) => {
|
|||
.aggregate([
|
||||
{ $project: {
|
||||
repost_id: '$repost_id',
|
||||
reply_to_id: '$reply_to_id',
|
||||
reply_id: '$reply_id',
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
{ $project: {
|
||||
|
@ -34,7 +34,7 @@ module.exports = params => new Promise(async (res, rej) => {
|
|||
then: 'repost',
|
||||
else: {
|
||||
$cond: {
|
||||
if: { $ne: ['$reply_to_id', null] },
|
||||
if: { $ne: ['$reply_id', null] },
|
||||
then: 'reply',
|
||||
else: 'post'
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
|||
|
||||
const datas = await Post
|
||||
.aggregate([
|
||||
{ $match: { reply_to: post._id } },
|
||||
{ $match: { reply: post._id } },
|
||||
{ $project: {
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
|
|
|
@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
|||
{ $match: { user_id: user._id } },
|
||||
{ $project: {
|
||||
repost_id: '$repost_id',
|
||||
reply_to_id: '$reply_to_id',
|
||||
reply_id: '$reply_id',
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
{ $project: {
|
||||
|
@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
|||
then: 'repost',
|
||||
else: {
|
||||
$cond: {
|
||||
if: { $ne: ['$reply_to_id', null] },
|
||||
if: { $ne: ['$reply_id', null] },
|
||||
then: 'reply',
|
||||
else: 'post'
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
|||
{ $match: { user_id: user._id } },
|
||||
{ $project: {
|
||||
repost_id: '$repost_id',
|
||||
reply_to_id: '$reply_to_id',
|
||||
reply_id: '$reply_id',
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
{ $project: {
|
||||
|
@ -49,7 +49,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
|||
then: 'repost',
|
||||
else: {
|
||||
$cond: {
|
||||
if: { $ne: ['$reply_to_id', null] },
|
||||
if: { $ne: ['$reply_id', null] },
|
||||
then: 'reply',
|
||||
else: 'post'
|
||||
}
|
||||
|
|
59
src/api/endpoints/channels.ts
Normal file
59
src/api/endpoints/channels.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../models/channel';
|
||||
import serialize from '../serializers/channel';
|
||||
|
||||
/**
|
||||
* Get all channels
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} me
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
// Get 'since_id' parameter
|
||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||
if (sinceIdErr) return rej('invalid since_id param');
|
||||
|
||||
// Get 'max_id' parameter
|
||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||
if (maxIdErr) return rej('invalid max_id param');
|
||||
|
||||
// Check if both of since_id and max_id is specified
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
}
|
||||
|
||||
// Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
const query = {} as any;
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
$gt: sinceId
|
||||
};
|
||||
} else if (maxId) {
|
||||
query._id = {
|
||||
$lt: maxId
|
||||
};
|
||||
}
|
||||
|
||||
// Issue query
|
||||
const channels = await Channel
|
||||
.find(query, {
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(channels.map(async channel =>
|
||||
await serialize(channel, me))));
|
||||
});
|
39
src/api/endpoints/channels/create.ts
Normal file
39
src/api/endpoints/channels/create.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../../models/channel';
|
||||
import Watching from '../../models/channel-watching';
|
||||
import serialize from '../../serializers/channel';
|
||||
|
||||
/**
|
||||
* Create a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = async (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'title' parameter
|
||||
const [title, titleErr] = $(params.title).string().range(1, 100).$;
|
||||
if (titleErr) return rej('invalid title param');
|
||||
|
||||
// Create a channel
|
||||
const channel = await Channel.insert({
|
||||
created_at: new Date(),
|
||||
user_id: user._id,
|
||||
title: title,
|
||||
index: 0,
|
||||
watching_count: 1
|
||||
});
|
||||
|
||||
// Response
|
||||
res(await serialize(channel));
|
||||
|
||||
// Create Watching
|
||||
await Watching.insert({
|
||||
created_at: new Date(),
|
||||
user_id: user._id,
|
||||
channel_id: channel._id
|
||||
});
|
||||
});
|
79
src/api/endpoints/channels/posts.ts
Normal file
79
src/api/endpoints/channels/posts.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import { default as Channel, IChannel } from '../../models/channel';
|
||||
import { default as Post, IPost } from '../../models/post';
|
||||
import serialize from '../../serializers/post';
|
||||
|
||||
/**
|
||||
* Show a posts of a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
// Get 'since_id' parameter
|
||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||
if (sinceIdErr) return rej('invalid since_id param');
|
||||
|
||||
// Get 'max_id' parameter
|
||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||
if (maxIdErr) return rej('invalid max_id param');
|
||||
|
||||
// Check if both of since_id and max_id is specified
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
}
|
||||
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
// Fetch channel
|
||||
const channel: IChannel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
|
||||
const query = {
|
||||
channel_id: channel._id
|
||||
} as any;
|
||||
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
$gt: sinceId
|
||||
};
|
||||
} else if (maxId) {
|
||||
query._id = {
|
||||
$lt: maxId
|
||||
};
|
||||
}
|
||||
//#endregion Construct query
|
||||
|
||||
// Issue query
|
||||
const posts = await Post
|
||||
.find(query, {
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(posts.map(async (post) =>
|
||||
await serialize(post, user)
|
||||
)));
|
||||
});
|
31
src/api/endpoints/channels/show.ts
Normal file
31
src/api/endpoints/channels/show.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import { default as Channel, IChannel } from '../../models/channel';
|
||||
import serialize from '../../serializers/channel';
|
||||
|
||||
/**
|
||||
* Show a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
// Fetch channel
|
||||
const channel: IChannel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
|
||||
// Serialize
|
||||
res(await serialize(channel, user));
|
||||
});
|
60
src/api/endpoints/channels/unwatch.ts
Normal file
60
src/api/endpoints/channels/unwatch.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../../models/channel';
|
||||
import Watching from '../../models/channel-watching';
|
||||
|
||||
/**
|
||||
* Unwatch a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
//#region Fetch channel
|
||||
const channel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Check whether not watching
|
||||
const exist = await Watching.findOne({
|
||||
user_id: user._id,
|
||||
channel_id: channel._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
if (exist === null) {
|
||||
return rej('already not watching');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Delete watching
|
||||
await Watching.update({
|
||||
_id: exist._id
|
||||
}, {
|
||||
$set: {
|
||||
deleted_at: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Send response
|
||||
res();
|
||||
|
||||
// Decrement watching count
|
||||
Channel.update(channel._id, {
|
||||
$inc: {
|
||||
watching_count: -1
|
||||
}
|
||||
});
|
||||
});
|
58
src/api/endpoints/channels/watch.ts
Normal file
58
src/api/endpoints/channels/watch.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../../models/channel';
|
||||
import Watching from '../../models/channel-watching';
|
||||
|
||||
/**
|
||||
* Watch a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
//#region Fetch channel
|
||||
const channel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Check whether already watching
|
||||
const exist = await Watching.findOne({
|
||||
user_id: user._id,
|
||||
channel_id: channel._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
if (exist !== null) {
|
||||
return rej('already watching');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Create Watching
|
||||
await Watching.insert({
|
||||
created_at: new Date(),
|
||||
user_id: user._id,
|
||||
channel_id: channel._id
|
||||
});
|
||||
|
||||
// Send response
|
||||
res();
|
||||
|
||||
// Increment watching count
|
||||
Channel.update(channel._id, {
|
||||
$inc: {
|
||||
watching_count: 1
|
||||
}
|
||||
});
|
||||
});
|
|
@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Calculate drive usage
|
||||
const usage = ((await DriveFile
|
||||
.aggregate([
|
||||
{ $match: { user_id: user._id } },
|
||||
{ $match: { 'metadata.user_id': user._id } },
|
||||
{
|
||||
$project: {
|
||||
datasize: true
|
||||
length: true
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
usage: { $sum: '$datasize' }
|
||||
usage: { $sum: '$length' }
|
||||
}
|
||||
}
|
||||
]))[0] || {
|
||||
|
|
|
@ -13,35 +13,35 @@ import serialize from '../../serializers/drive-file';
|
|||
* @param {any} app
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||
module.exports = async (params, user, app) => {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
if (limitErr) throw 'invalid limit param';
|
||||
|
||||
// Get 'since_id' parameter
|
||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||
if (sinceIdErr) return rej('invalid since_id param');
|
||||
if (sinceIdErr) throw 'invalid since_id param';
|
||||
|
||||
// Get 'max_id' parameter
|
||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||
if (maxIdErr) return rej('invalid max_id param');
|
||||
if (maxIdErr) throw 'invalid max_id param';
|
||||
|
||||
// Check if both of since_id and max_id is specified
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
throw 'cannot set since_id and max_id';
|
||||
}
|
||||
|
||||
// Get 'folder_id' parameter
|
||||
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
||||
if (folderIdErr) return rej('invalid folder_id param');
|
||||
if (folderIdErr) throw 'invalid folder_id param';
|
||||
|
||||
// Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
const query = {
|
||||
user_id: user._id,
|
||||
folder_id: folderId
|
||||
'metadata.user_id': user._id,
|
||||
'metadata.folder_id': folderId
|
||||
} as any;
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
|
@ -57,14 +57,11 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
|||
// Issue query
|
||||
const files = await DriveFile
|
||||
.find(query, {
|
||||
fields: {
|
||||
data: false
|
||||
},
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(files.map(async file =>
|
||||
await serialize(file))));
|
||||
});
|
||||
const _files = await Promise.all(files.map(file => serialize(file)));
|
||||
return _files;
|
||||
};
|
||||
|
|
|
@ -24,13 +24,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Issue query
|
||||
const files = await DriveFile
|
||||
.find({
|
||||
name: name,
|
||||
user_id: user._id,
|
||||
folder_id: folderId
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
'metadata.name': name,
|
||||
'metadata.user_id': user._id,
|
||||
'metadata.folder_id': folderId
|
||||
});
|
||||
|
||||
// Serialize
|
||||
|
|
|
@ -12,28 +12,26 @@ import serialize from '../../../serializers/drive-file';
|
|||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
module.exports = async (params, user) => {
|
||||
// Get 'file_id' parameter
|
||||
const [fileId, fileIdErr] = $(params.file_id).id().$;
|
||||
if (fileIdErr) return rej('invalid file_id param');
|
||||
if (fileIdErr) throw 'invalid file_id param';
|
||||
|
||||
// Fetch file
|
||||
const file = await DriveFile
|
||||
.findOne({
|
||||
_id: fileId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
return rej('file-not-found');
|
||||
throw 'file-not-found';
|
||||
}
|
||||
|
||||
// Serialize
|
||||
res(await serialize(file, {
|
||||
const _file = await serialize(file, {
|
||||
detail: true
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
return _file;
|
||||
};
|
||||
|
|
|
@ -24,11 +24,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
const file = await DriveFile
|
||||
.findOne({
|
||||
_id: fileId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
|
@ -38,7 +34,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Get 'name' parameter
|
||||
const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
|
||||
if (nameErr) return rej('invalid name param');
|
||||
if (name) file.name = name;
|
||||
if (name) file.metadata.name = name;
|
||||
|
||||
// Get 'folder_id' parameter
|
||||
const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
||||
|
@ -46,7 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
|
||||
if (folderId !== undefined) {
|
||||
if (folderId === null) {
|
||||
file.folder_id = null;
|
||||
file.metadata.folder_id = null;
|
||||
} else {
|
||||
// Fetch folder
|
||||
const folder = await DriveFolder
|
||||
|
@ -59,14 +55,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
return rej('folder-not-found');
|
||||
}
|
||||
|
||||
file.folder_id = folder._id;
|
||||
file.metadata.folder_id = folder._id;
|
||||
}
|
||||
}
|
||||
|
||||
DriveFile.update(file._id, {
|
||||
await DriveFile.update(file._id, {
|
||||
$set: {
|
||||
name: file.name,
|
||||
folder_id: file.folder_id
|
||||
'metadata.name': file.metadata.name,
|
||||
'metadata.folder_id': file.metadata.folder_id
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(folders.map(async folder =>
|
||||
await serialize(folder))));
|
||||
res(await Promise.all(folders.map(folder => serialize(folder))));
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import $ from 'cafy';
|
||||
import DriveFolder from '../../../models/drive-folder';
|
||||
import { isValidFolderName } from '../../../models/drive-folder';
|
||||
import serialize from '../../../serializers/drive-file';
|
||||
import serialize from '../../../serializers/drive-folder';
|
||||
import event from '../../../event';
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@ import $ from 'cafy';
|
|||
import Notification from '../../models/notification';
|
||||
import serialize from '../../serializers/notification';
|
||||
import getFriends from '../../common/get-friends';
|
||||
import read from '../../common/read-notification';
|
||||
|
||||
/**
|
||||
* Get notifications
|
||||
|
@ -91,17 +92,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
|
||||
// Mark as read all
|
||||
if (notifications.length > 0 && markAsRead) {
|
||||
const ids = notifications
|
||||
.filter(x => x.is_read == false)
|
||||
.map(x => x._id);
|
||||
|
||||
// Update documents
|
||||
await Notification.update({
|
||||
_id: { $in: ids }
|
||||
}, {
|
||||
$set: { is_read: true }
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
read(user._id, notifications);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -54,9 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
if (fileId !== undefined) {
|
||||
file = await DriveFile.findOne({
|
||||
_id: fileId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
data: false
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
|
|
23
src/api/endpoints/notifications/get_unread_count.ts
Normal file
23
src/api/endpoints/notifications/get_unread_count.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import Notification from '../../models/notification';
|
||||
|
||||
/**
|
||||
* Get count of unread notifications
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const count = await Notification
|
||||
.count({
|
||||
notifiee_id: user._id,
|
||||
is_read: false
|
||||
});
|
||||
|
||||
res({
|
||||
count: count
|
||||
});
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Notification from '../../models/notification';
|
||||
import serialize from '../../serializers/notification';
|
||||
import event from '../../event';
|
||||
|
||||
/**
|
||||
* Mark as read a notification
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const [notificationId, notificationIdErr] = $(params.notification_id).id().$;
|
||||
if (notificationIdErr) return rej('invalid notification_id param');
|
||||
|
||||
// Get notification
|
||||
const notification = await Notification
|
||||
.findOne({
|
||||
_id: notificationId,
|
||||
i: user._id
|
||||
});
|
||||
|
||||
if (notification === null) {
|
||||
return rej('notification-not-found');
|
||||
}
|
||||
|
||||
// Update
|
||||
notification.is_read = true;
|
||||
Notification.update({ _id: notification._id }, {
|
||||
$set: {
|
||||
is_read: true
|
||||
}
|
||||
});
|
||||
|
||||
// Response
|
||||
res();
|
||||
|
||||
// Serialize
|
||||
const notificationObj = await serialize(notification);
|
||||
|
||||
// Publish read_notification event
|
||||
event(user._id, 'read_notification', notificationObj);
|
||||
});
|
32
src/api/endpoints/notifications/mark_as_read_all.ts
Normal file
32
src/api/endpoints/notifications/mark_as_read_all.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import Notification from '../../models/notification';
|
||||
import event from '../../event';
|
||||
|
||||
/**
|
||||
* Mark as read all notifications
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Update documents
|
||||
await Notification.update({
|
||||
notifiee_id: user._id,
|
||||
is_read: false
|
||||
}, {
|
||||
$set: {
|
||||
is_read: true
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Response
|
||||
res();
|
||||
|
||||
// 全ての通知を読みましたよというイベントを発行
|
||||
event(user._id, 'read_all_notifications');
|
||||
});
|
|
@ -62,7 +62,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
|||
}
|
||||
|
||||
if (reply != undefined) {
|
||||
query.reply_to_id = reply ? { $exists: true, $ne: null } : null;
|
||||
query.reply_id = reply ? { $exists: true, $ne: null } : null;
|
||||
}
|
||||
|
||||
if (repost != undefined) {
|
||||
|
|
|
@ -49,13 +49,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (p.reply_to_id) {
|
||||
await get(p.reply_to_id);
|
||||
if (p.reply_id) {
|
||||
await get(p.reply_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (post.reply_to_id) {
|
||||
await get(post.reply_to_id);
|
||||
if (post.reply_id) {
|
||||
await get(post.reply_id);
|
||||
}
|
||||
|
||||
// Serialize
|
||||
|
|
|
@ -4,16 +4,17 @@
|
|||
import $ from 'cafy';
|
||||
import deepEqual = require('deep-equal');
|
||||
import parse from '../../common/text';
|
||||
import Post from '../../models/post';
|
||||
import { isValidText } from '../../models/post';
|
||||
import { default as Post, IPost, isValidText } from '../../models/post';
|
||||
import { default as User, IUser } from '../../models/user';
|
||||
import { default as Channel, IChannel } from '../../models/channel';
|
||||
import Following from '../../models/following';
|
||||
import DriveFile from '../../models/drive-file';
|
||||
import Watching from '../../models/post-watching';
|
||||
import ChannelWatching from '../../models/channel-watching';
|
||||
import serialize from '../../serializers/post';
|
||||
import notify from '../../common/notify';
|
||||
import watch from '../../common/watch-post';
|
||||
import event from '../../event';
|
||||
import { default as event, publishChannelStream } from '../../event';
|
||||
import config from '../../../conf';
|
||||
|
||||
/**
|
||||
|
@ -43,9 +44,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
// SELECT _id
|
||||
const entity = await DriveFile.findOne({
|
||||
_id: mediaId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
_id: true
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (entity === null) {
|
||||
|
@ -62,7 +61,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
|
||||
if (repostIdErr) return rej('invalid repost_id');
|
||||
|
||||
let repost = null;
|
||||
let repost: IPost = null;
|
||||
let isQuote = false;
|
||||
if (repostId !== undefined) {
|
||||
// Fetch repost to post
|
||||
repost = await Post.findOne({
|
||||
|
@ -84,43 +84,86 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
}
|
||||
});
|
||||
|
||||
isQuote = text != null || files != null;
|
||||
|
||||
// 直近と同じRepost対象かつ引用じゃなかったらエラー
|
||||
if (latestPost &&
|
||||
latestPost.repost_id &&
|
||||
latestPost.repost_id.equals(repost._id) &&
|
||||
text === undefined && files === null) {
|
||||
!isQuote) {
|
||||
return rej('cannot repost same post that already reposted in your latest post');
|
||||
}
|
||||
|
||||
// 直近がRepost対象かつ引用じゃなかったらエラー
|
||||
if (latestPost &&
|
||||
latestPost._id.equals(repost._id) &&
|
||||
text === undefined && files === null) {
|
||||
!isQuote) {
|
||||
return rej('cannot repost your latest post');
|
||||
}
|
||||
}
|
||||
|
||||
// Get 'in_reply_to_post_id' parameter
|
||||
const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$;
|
||||
if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id');
|
||||
// Get 'reply_id' parameter
|
||||
const [replyId, replyIdErr] = $(params.reply_id).optional.id().$;
|
||||
if (replyIdErr) return rej('invalid reply_id');
|
||||
|
||||
let inReplyToPost = null;
|
||||
if (inReplyToPostId !== undefined) {
|
||||
let reply: IPost = null;
|
||||
if (replyId !== undefined) {
|
||||
// Fetch reply
|
||||
inReplyToPost = await Post.findOne({
|
||||
_id: inReplyToPostId
|
||||
reply = await Post.findOne({
|
||||
_id: replyId
|
||||
});
|
||||
|
||||
if (inReplyToPost === null) {
|
||||
if (reply === null) {
|
||||
return rej('in reply to post is not found');
|
||||
}
|
||||
|
||||
// 返信対象が引用でないRepostだったらエラー
|
||||
if (inReplyToPost.repost_id && !inReplyToPost.text && !inReplyToPost.media_ids) {
|
||||
if (reply.repost_id && !reply.text && !reply.media_ids) {
|
||||
return rej('cannot reply to repost');
|
||||
}
|
||||
}
|
||||
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).optional.id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id');
|
||||
|
||||
let channel: IChannel = null;
|
||||
if (channelId !== undefined) {
|
||||
// Fetch channel
|
||||
channel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
|
||||
// 返信対象の投稿がこのチャンネルじゃなかったらダメ
|
||||
if (reply && !channelId.equals(reply.channel_id)) {
|
||||
return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
|
||||
}
|
||||
|
||||
// Repost対象の投稿がこのチャンネルじゃなかったらダメ
|
||||
if (repost && !channelId.equals(repost.channel_id)) {
|
||||
return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません');
|
||||
}
|
||||
|
||||
// 引用ではないRepostはダメ
|
||||
if (repost && !isQuote) {
|
||||
return rej('チャンネル内部では引用ではないRepostをすることはできません');
|
||||
}
|
||||
} else {
|
||||
// 返信対象の投稿がチャンネルへの投稿だったらダメ
|
||||
if (reply && reply.channel_id != null) {
|
||||
return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
|
||||
}
|
||||
|
||||
// Repost対象の投稿がチャンネルへの投稿だったらダメ
|
||||
if (repost && repost.channel_id != null) {
|
||||
return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません');
|
||||
}
|
||||
}
|
||||
|
||||
// Get 'poll' parameter
|
||||
const [poll, pollErr] = $(params.poll).optional.strict.object()
|
||||
.have('choices', $().array('string')
|
||||
|
@ -148,12 +191,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
if (user.latest_post) {
|
||||
if (deepEqual({
|
||||
text: user.latest_post.text,
|
||||
reply: user.latest_post.reply_to_id ? user.latest_post.reply_to_id.toString() : null,
|
||||
reply: user.latest_post.reply_id ? user.latest_post.reply_id.toString() : null,
|
||||
repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null,
|
||||
media_ids: (user.latest_post.media_ids || []).map(id => id.toString())
|
||||
}, {
|
||||
text: text,
|
||||
reply: inReplyToPost ? inReplyToPost._id.toString() : null,
|
||||
reply: reply ? reply._id.toString() : null,
|
||||
repost: repost ? repost._id.toString() : null,
|
||||
media_ids: (files || []).map(file => file._id.toString())
|
||||
})) {
|
||||
|
@ -164,8 +207,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
// 投稿を作成
|
||||
const post = await Post.insert({
|
||||
created_at: new Date(),
|
||||
channel_id: channel ? channel._id : undefined,
|
||||
index: channel ? channel.index + 1 : undefined,
|
||||
media_ids: files ? files.map(file => file._id) : undefined,
|
||||
reply_to_id: inReplyToPost ? inReplyToPost._id : undefined,
|
||||
reply_id: reply ? reply._id : undefined,
|
||||
repost_id: repost ? repost._id : undefined,
|
||||
poll: poll,
|
||||
text: text,
|
||||
|
@ -179,8 +224,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
// Reponse
|
||||
res(postObj);
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// Post processes
|
||||
//#region Post processes
|
||||
|
||||
User.update({ _id: user._id }, {
|
||||
$set: {
|
||||
|
@ -203,6 +247,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
}
|
||||
}
|
||||
|
||||
// タイムラインへの投稿
|
||||
if (!channel) {
|
||||
// Publish event to myself's stream
|
||||
event(user._id, 'post', postObj);
|
||||
|
||||
|
@ -220,6 +266,32 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
// Publish event to followers stream
|
||||
followers.forEach(following =>
|
||||
event(following.follower_id, 'post', postObj));
|
||||
}
|
||||
|
||||
// チャンネルへの投稿
|
||||
if (channel) {
|
||||
// Increment channel index(posts count)
|
||||
Channel.update({ _id: channel._id }, {
|
||||
$inc: {
|
||||
index: 1
|
||||
}
|
||||
});
|
||||
|
||||
// Publish event to channel
|
||||
publishChannelStream(channel._id, 'post', postObj);
|
||||
|
||||
// Get channel watchers
|
||||
const watches = await ChannelWatching.find({
|
||||
channel_id: channel._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
// チャンネルの視聴者(のタイムライン)に配信
|
||||
watches.forEach(w => {
|
||||
event(w.user_id, 'post', postObj);
|
||||
});
|
||||
}
|
||||
|
||||
// Increment my posts count
|
||||
User.update({ _id: user._id }, {
|
||||
|
@ -229,23 +301,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
});
|
||||
|
||||
// If has in reply to post
|
||||
if (inReplyToPost) {
|
||||
if (reply) {
|
||||
// Increment replies count
|
||||
Post.update({ _id: inReplyToPost._id }, {
|
||||
Post.update({ _id: reply._id }, {
|
||||
$inc: {
|
||||
replies_count: 1
|
||||
}
|
||||
});
|
||||
|
||||
// 自分自身へのリプライでない限りは通知を作成
|
||||
notify(inReplyToPost.user_id, user._id, 'reply', {
|
||||
notify(reply.user_id, user._id, 'reply', {
|
||||
post_id: post._id
|
||||
});
|
||||
|
||||
// Fetch watchers
|
||||
Watching
|
||||
.find({
|
||||
post_id: inReplyToPost._id,
|
||||
post_id: reply._id,
|
||||
user_id: { $ne: user._id },
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
|
@ -265,10 +337,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
// この投稿をWatchする
|
||||
// TODO: ユーザーが「返信したときに自動でWatchする」設定を
|
||||
// オフにしていた場合はしない
|
||||
watch(user._id, inReplyToPost);
|
||||
watch(user._id, reply);
|
||||
|
||||
// Add mention
|
||||
addMention(inReplyToPost.user_id, 'reply');
|
||||
addMention(reply.user_id, 'reply');
|
||||
}
|
||||
|
||||
// If it is repost
|
||||
|
@ -369,7 +441,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
if (mentionee == null) return;
|
||||
|
||||
// 既に言及されたユーザーに対する返信や引用repostの場合も無視
|
||||
if (inReplyToPost && inReplyToPost.user_id.equals(mentionee._id)) return;
|
||||
if (reply && reply.user_id.equals(mentionee._id)) return;
|
||||
if (repost && repost.user_id.equals(mentionee._id)) return;
|
||||
|
||||
// Add mention
|
||||
|
@ -406,4 +478,6 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
|
||||
// Issue query
|
||||
const replies = await Post
|
||||
.find({ reply_to_id: post._id }, {
|
||||
.find({ reply_id: post._id }, {
|
||||
limit: limit,
|
||||
skip: offset,
|
||||
sort: {
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import Post from '../../models/post';
|
||||
import ChannelWatching from '../../models/channel-watching';
|
||||
import getFriends from '../../common/get-friends';
|
||||
import serialize from '../../serializers/post';
|
||||
|
||||
|
@ -14,36 +16,62 @@ import serialize from '../../serializers/post';
|
|||
* @param {any} app
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||
module.exports = async (params, user, app) => {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
if (limitErr) throw 'invalid limit param';
|
||||
|
||||
// Get 'since_id' parameter
|
||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||
if (sinceIdErr) return rej('invalid since_id param');
|
||||
if (sinceIdErr) throw 'invalid since_id param';
|
||||
|
||||
// Get 'max_id' parameter
|
||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||
if (maxIdErr) return rej('invalid max_id param');
|
||||
if (maxIdErr) throw 'invalid max_id param';
|
||||
|
||||
// Check if both of since_id and max_id is specified
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
throw 'cannot set since_id and max_id';
|
||||
}
|
||||
|
||||
// ID list of the user $self and other users who the user follows
|
||||
const followingIds = await getFriends(user._id);
|
||||
const { followingIds, watchChannelIds } = await rap({
|
||||
// ID list of the user itself and other users who the user follows
|
||||
followingIds: getFriends(user._id),
|
||||
// Watchしているチャンネルを取得
|
||||
watchChannelIds: ChannelWatching.find({
|
||||
user_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
}).then(watches => watches.map(w => w.channel_id))
|
||||
});
|
||||
|
||||
// Construct query
|
||||
//#region Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
|
||||
const query = {
|
||||
$or: [{
|
||||
// フォローしている人のタイムラインへの投稿
|
||||
user_id: {
|
||||
$in: followingIds
|
||||
},
|
||||
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
|
||||
$or: [{
|
||||
channel_id: {
|
||||
$exists: false
|
||||
}
|
||||
}, {
|
||||
channel_id: null
|
||||
}]
|
||||
}, {
|
||||
// Watchしているチャンネルへの投稿
|
||||
channel_id: {
|
||||
$in: watchChannelIds
|
||||
}
|
||||
}]
|
||||
} as any;
|
||||
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
|
@ -54,6 +82,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
|||
$lt: maxId
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Issue query
|
||||
const timeline = await Post
|
||||
|
@ -63,7 +92,6 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
|||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(timeline.map(async post =>
|
||||
await serialize(post, user)
|
||||
)));
|
||||
});
|
||||
const _timeline = await Promise.all(timeline.map(post => serialize(post, user)));
|
||||
return _timeline;
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
} as any;
|
||||
|
||||
if (reply != undefined) {
|
||||
query.reply_to_id = reply ? { $exists: true, $ne: null } : null;
|
||||
query.reply_id = reply ? { $exists: true, $ne: null } : null;
|
||||
}
|
||||
|
||||
if (repost != undefined) {
|
||||
|
|
|
@ -27,7 +27,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
// Fetch recent posts
|
||||
const recentPosts = await Post.find({
|
||||
user_id: user._id,
|
||||
reply_to_id: {
|
||||
reply_id: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
limit: 1000,
|
||||
fields: {
|
||||
_id: false,
|
||||
reply_to_id: true
|
||||
reply_id: true
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -49,7 +49,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
|
||||
const replyTargetPosts = await Post.find({
|
||||
_id: {
|
||||
$in: recentPosts.map(p => p.reply_to_id)
|
||||
$in: recentPosts.map(p => p.reply_id)
|
||||
},
|
||||
user_id: {
|
||||
$ne: user._id
|
||||
|
|
|
@ -85,7 +85,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
}
|
||||
|
||||
if (!includeReplies) {
|
||||
query.reply_to_id = null;
|
||||
query.reply_id = null;
|
||||
}
|
||||
|
||||
if (withMedia) {
|
||||
|
|
|
@ -25,6 +25,10 @@ class MisskeyEvent {
|
|||
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
public publishChannelStream(channelId: ID, type: string, value?: any): void {
|
||||
this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
private publish(channel: string, type: string, value?: any): void {
|
||||
const message = value == null ?
|
||||
{ type: type } :
|
||||
|
@ -41,3 +45,5 @@ export default ev.publishUserStream.bind(ev);
|
|||
export const publishPostStream = ev.publishPostStream.bind(ev);
|
||||
|
||||
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
|
||||
|
||||
export const publishChannelStream = ev.publishChannelStream.bind(ev);
|
||||
|
|
3
src/api/models/channel-watching.ts
Normal file
3
src/api/models/channel-watching.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import db from '../../db/mongodb';
|
||||
|
||||
export default db.get('channel_watching') as any; // fuck type definition
|
14
src/api/models/channel.ts
Normal file
14
src/api/models/channel.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import db from '../../db/mongodb';
|
||||
|
||||
const collection = db.get('channels');
|
||||
|
||||
export default collection as any; // fuck type definition
|
||||
|
||||
export type IChannel = {
|
||||
_id: mongo.ObjectID;
|
||||
created_at: Date;
|
||||
title: string;
|
||||
user_id: mongo.ObjectID;
|
||||
index: number;
|
||||
};
|
|
@ -1,11 +1,22 @@
|
|||
import db from '../../db/mongodb';
|
||||
import * as mongodb from 'mongodb';
|
||||
import monkDb, { nativeDbConn } from '../../db/mongodb';
|
||||
|
||||
const collection = db.get('drive_files');
|
||||
const collection = monkDb.get('drive_files.files');
|
||||
|
||||
(collection as any).createIndex('hash'); // fuck type definition
|
||||
|
||||
export default collection as any; // fuck type definition
|
||||
|
||||
const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
|
||||
const db = await nativeDbConn();
|
||||
const bucket = new mongodb.GridFSBucket(db, {
|
||||
bucketName: 'drive_files'
|
||||
});
|
||||
return bucket;
|
||||
};
|
||||
|
||||
export { getGridFSBucket };
|
||||
|
||||
export function validateFileName(name: string): boolean {
|
||||
return (
|
||||
(name.trim().length > 0) &&
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import db from '../../db/mongodb';
|
||||
|
||||
export default db.get('notifications') as any; // fuck type definition
|
||||
|
||||
export interface INotification {
|
||||
_id: mongo.ObjectID;
|
||||
}
|
||||
|
|
|
@ -10,9 +10,10 @@ export function isValidText(text: string): boolean {
|
|||
|
||||
export type IPost = {
|
||||
_id: mongo.ObjectID;
|
||||
channel_id: mongo.ObjectID;
|
||||
created_at: Date;
|
||||
media_ids: mongo.ObjectID[];
|
||||
reply_to_id: mongo.ObjectID;
|
||||
reply_id: mongo.ObjectID;
|
||||
repost_id: mongo.ObjectID;
|
||||
poll: {}; // todo
|
||||
text: string;
|
||||
|
|
66
src/api/serializers/channel.ts
Normal file
66
src/api/serializers/channel.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import * as mongo from 'mongodb';
|
||||
import deepcopy = require('deepcopy');
|
||||
import { IUser } from '../models/user';
|
||||
import { default as Channel, IChannel } from '../models/channel';
|
||||
import Watching from '../models/channel-watching';
|
||||
|
||||
/**
|
||||
* Serialize a channel
|
||||
*
|
||||
* @param channel target
|
||||
* @param me? serializee
|
||||
* @return response
|
||||
*/
|
||||
export default (
|
||||
channel: string | mongo.ObjectID | IChannel,
|
||||
me?: string | mongo.ObjectID | IUser
|
||||
) => new Promise<any>(async (resolve, reject) => {
|
||||
|
||||
let _channel: any;
|
||||
|
||||
// Populate the channel if 'channel' is ID
|
||||
if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
|
||||
_channel = await Channel.findOne({
|
||||
_id: channel
|
||||
});
|
||||
} else if (typeof channel === 'string') {
|
||||
_channel = await Channel.findOne({
|
||||
_id: new mongo.ObjectID(channel)
|
||||
});
|
||||
} else {
|
||||
_channel = deepcopy(channel);
|
||||
}
|
||||
|
||||
// Rename _id to id
|
||||
_channel.id = _channel._id;
|
||||
delete _channel._id;
|
||||
|
||||
// Remove needless properties
|
||||
delete _channel.user_id;
|
||||
|
||||
// Me
|
||||
const meId: mongo.ObjectID = me
|
||||
? mongo.ObjectID.prototype.isPrototypeOf(me)
|
||||
? me as mongo.ObjectID
|
||||
: typeof me === 'string'
|
||||
? new mongo.ObjectID(me)
|
||||
: (me as IUser)._id
|
||||
: null;
|
||||
|
||||
if (me) {
|
||||
//#region Watchしているかどうか
|
||||
const watch = await Watching.findOne({
|
||||
user_id: meId,
|
||||
channel_id: _channel.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
_channel.is_watching = watch !== null;
|
||||
//#endregion
|
||||
}
|
||||
|
||||
resolve(_channel);
|
||||
});
|
|
@ -31,44 +31,40 @@ export default (
|
|||
if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
|
||||
_file = await DriveFile.findOne({
|
||||
_id: file
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
});
|
||||
} else if (typeof file === 'string') {
|
||||
_file = await DriveFile.findOne({
|
||||
_id: new mongo.ObjectID(file)
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_file = deepcopy(file);
|
||||
}
|
||||
|
||||
// Rename _id to id
|
||||
_file.id = _file._id;
|
||||
delete _file._id;
|
||||
if (!_file) return reject('invalid file arg.');
|
||||
|
||||
delete _file.data;
|
||||
// rendered target
|
||||
let _target: any = {};
|
||||
|
||||
_file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`;
|
||||
_target.id = _file._id;
|
||||
_target.created_at = _file.uploadDate;
|
||||
|
||||
if (opts.detail && _file.folder_id) {
|
||||
_target = Object.assign(_target, _file.metadata);
|
||||
|
||||
_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
|
||||
|
||||
if (opts.detail && _target.folder_id) {
|
||||
// Populate folder
|
||||
_file.folder = await serializeDriveFolder(_file.folder_id, {
|
||||
_target.folder = await serializeDriveFolder(_target.folder_id, {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.detail && _file.tags) {
|
||||
if (opts.detail && _target.tags) {
|
||||
// Populate tags
|
||||
_file.tags = await _file.tags.map(async (tag: any) =>
|
||||
_target.tags = await _target.tags.map(async (tag: any) =>
|
||||
await serializeDriveTag(tag)
|
||||
);
|
||||
}
|
||||
|
||||
resolve(_file);
|
||||
resolve(_target);
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ const self = (
|
|||
});
|
||||
|
||||
const childFilesCount = await DriveFile.count({
|
||||
folder_id: _folder.id
|
||||
'metadata.folder_id': _folder.id
|
||||
});
|
||||
|
||||
_folder.folders_count = childFoldersCount;
|
||||
|
|
|
@ -8,9 +8,11 @@ import Reaction from '../models/post-reaction';
|
|||
import { IUser } from '../models/user';
|
||||
import Vote from '../models/poll-vote';
|
||||
import serializeApp from './app';
|
||||
import serializeChannel from './channel';
|
||||
import serializeUser from './user';
|
||||
import serializeDriveFile from './drive-file';
|
||||
import parse from '../common/text';
|
||||
import rap from '@prezzemolo/rap';
|
||||
|
||||
/**
|
||||
* Serialize a post
|
||||
|
@ -20,13 +22,13 @@ import parse from '../common/text';
|
|||
* @param options? serialize options
|
||||
* @return response
|
||||
*/
|
||||
const self = (
|
||||
const self = async (
|
||||
post: string | mongo.ObjectID | IPost,
|
||||
me?: string | mongo.ObjectID | IUser,
|
||||
options?: {
|
||||
detail: boolean
|
||||
}
|
||||
) => new Promise<any>(async (resolve, reject) => {
|
||||
) => {
|
||||
const opts = options || {
|
||||
detail: true,
|
||||
};
|
||||
|
@ -55,6 +57,8 @@ const self = (
|
|||
_post = deepcopy(post);
|
||||
}
|
||||
|
||||
if (!_post) throw 'invalid post arg.';
|
||||
|
||||
const id = _post._id;
|
||||
|
||||
// Rename _id to id
|
||||
|
@ -69,23 +73,29 @@ const self = (
|
|||
}
|
||||
|
||||
// Populate user
|
||||
_post.user = await serializeUser(_post.user_id, meId);
|
||||
_post.user = serializeUser(_post.user_id, meId);
|
||||
|
||||
// Populate app
|
||||
if (_post.app_id) {
|
||||
_post.app = await serializeApp(_post.app_id);
|
||||
_post.app = serializeApp(_post.app_id);
|
||||
}
|
||||
|
||||
// Populate channel
|
||||
if (_post.channel_id) {
|
||||
_post.channel = serializeChannel(_post.channel_id);
|
||||
}
|
||||
|
||||
if (_post.media_ids) {
|
||||
// Populate media
|
||||
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
||||
await serializeDriveFile(fileId)
|
||||
if (_post.media_ids) {
|
||||
_post.media = Promise.all(_post.media_ids.map(fileId =>
|
||||
serializeDriveFile(fileId)
|
||||
));
|
||||
}
|
||||
|
||||
// When requested a detailed post data
|
||||
if (opts.detail) {
|
||||
// Get previous post info
|
||||
_post.prev = (async () => {
|
||||
const prev = await Post.findOne({
|
||||
user_id: _post.user_id,
|
||||
_id: {
|
||||
|
@ -99,9 +109,11 @@ const self = (
|
|||
_id: -1
|
||||
}
|
||||
});
|
||||
_post.prev = prev ? prev._id : null;
|
||||
return prev ? prev._id : null;
|
||||
})();
|
||||
|
||||
// Get next post info
|
||||
_post.next = (async () => {
|
||||
const next = await Post.findOne({
|
||||
user_id: _post.user_id,
|
||||
_id: {
|
||||
|
@ -115,24 +127,26 @@ const self = (
|
|||
_id: 1
|
||||
}
|
||||
});
|
||||
_post.next = next ? next._id : null;
|
||||
return next ? next._id : null;
|
||||
})();
|
||||
|
||||
if (_post.reply_to_id) {
|
||||
if (_post.reply_id) {
|
||||
// Populate reply to post
|
||||
_post.reply_to = await self(_post.reply_to_id, meId, {
|
||||
_post.reply = self(_post.reply_id, meId, {
|
||||
detail: false
|
||||
});
|
||||
}
|
||||
|
||||
if (_post.repost_id) {
|
||||
// Populate repost
|
||||
_post.repost = await self(_post.repost_id, meId, {
|
||||
_post.repost = self(_post.repost_id, meId, {
|
||||
detail: _post.text == null
|
||||
});
|
||||
}
|
||||
|
||||
// Poll
|
||||
if (meId && _post.poll) {
|
||||
_post.poll = (async (poll) => {
|
||||
const vote = await Vote
|
||||
.findOne({
|
||||
user_id: meId,
|
||||
|
@ -140,15 +154,19 @@ const self = (
|
|||
});
|
||||
|
||||
if (vote != null) {
|
||||
const myChoice = _post.poll.choices
|
||||
const myChoice = poll.choices
|
||||
.filter(c => c.id == vote.choice)[0];
|
||||
|
||||
myChoice.is_voted = true;
|
||||
}
|
||||
|
||||
return poll;
|
||||
})(_post.poll);
|
||||
}
|
||||
|
||||
// Fetch my reaction
|
||||
if (meId) {
|
||||
_post.my_reaction = (async () => {
|
||||
const reaction = await Reaction
|
||||
.findOne({
|
||||
user_id: meId,
|
||||
|
@ -157,12 +175,18 @@ const self = (
|
|||
});
|
||||
|
||||
if (reaction) {
|
||||
_post.my_reaction = reaction.reaction;
|
||||
return reaction.reaction;
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
resolve(_post);
|
||||
});
|
||||
// resolve promises in _post object
|
||||
_post = await rap(_post);
|
||||
|
||||
return _post;
|
||||
};
|
||||
|
||||
export default self;
|
||||
|
|
|
@ -8,6 +8,7 @@ import serializePost from './post';
|
|||
import Following from '../models/following';
|
||||
import getFriends from '../common/get-friends';
|
||||
import config from '../../conf';
|
||||
import rap from '@prezzemolo/rap';
|
||||
|
||||
/**
|
||||
* Serialize a user
|
||||
|
@ -55,6 +56,8 @@ export default (
|
|||
_user = deepcopy(user);
|
||||
}
|
||||
|
||||
if (!_user) return reject('invalid user arg.');
|
||||
|
||||
// Me
|
||||
const meId: mongo.ObjectID = me
|
||||
? mongo.ObjectID.prototype.isPrototypeOf(me)
|
||||
|
@ -104,26 +107,30 @@ export default (
|
|||
|
||||
if (meId && !meId.equals(_user.id)) {
|
||||
// If the user is following
|
||||
_user.is_following = (async () => {
|
||||
const follow = await Following.findOne({
|
||||
follower_id: meId,
|
||||
followee_id: _user.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.is_following = follow !== null;
|
||||
return follow !== null;
|
||||
})();
|
||||
|
||||
// If the user is followed
|
||||
_user.is_followed = (async () => {
|
||||
const follow2 = await Following.findOne({
|
||||
follower_id: _user.id,
|
||||
followee_id: meId,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.is_followed = follow2 !== null;
|
||||
return follow2 !== null;
|
||||
})();
|
||||
}
|
||||
|
||||
if (opts.detail) {
|
||||
if (_user.pinned_post_id) {
|
||||
// Populate pinned post
|
||||
_user.pinned_post = await serializePost(_user.pinned_post_id, meId, {
|
||||
_user.pinned_post = serializePost(_user.pinned_post_id, meId, {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
|
@ -132,23 +139,24 @@ export default (
|
|||
const myFollowingIds = await getFriends(meId);
|
||||
|
||||
// Get following you know count
|
||||
const followingYouKnowCount = await Following.count({
|
||||
_user.following_you_know_count = Following.count({
|
||||
followee_id: { $in: myFollowingIds },
|
||||
follower_id: _user.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.following_you_know_count = followingYouKnowCount;
|
||||
|
||||
// Get followers you know count
|
||||
const followersYouKnowCount = await Following.count({
|
||||
_user.followers_you_know_count = Following.count({
|
||||
followee_id: _user.id,
|
||||
follower_id: { $in: myFollowingIds },
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.followers_you_know_count = followersYouKnowCount;
|
||||
}
|
||||
}
|
||||
|
||||
// resolve promises in _user object
|
||||
_user = await rap(_user);
|
||||
|
||||
resolve(_user);
|
||||
});
|
||||
/*
|
||||
|
|
12
src/api/stream/channel.ts
Normal file
12
src/api/stream/channel.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as websocket from 'websocket';
|
||||
import * as redis from 'redis';
|
||||
|
||||
export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void {
|
||||
const channel = request.resourceURL.query.channel;
|
||||
|
||||
// Subscribe channel stream
|
||||
subscriber.subscribe(`misskey:channel-stream:${channel}`);
|
||||
subscriber.on('message', (_, data) => {
|
||||
connection.send(data);
|
||||
});
|
||||
}
|
|
@ -4,6 +4,7 @@ import * as debug from 'debug';
|
|||
|
||||
import User from '../models/user';
|
||||
import serializePost from '../serializers/post';
|
||||
import readNotification from '../common/read-notification';
|
||||
|
||||
const log = debug('misskey');
|
||||
|
||||
|
@ -45,6 +46,11 @@ export default function homeStream(request: websocket.request, connection: webso
|
|||
});
|
||||
break;
|
||||
|
||||
case 'read_notification':
|
||||
if (!msg.id) return;
|
||||
readNotification(user._id, msg.id);
|
||||
break;
|
||||
|
||||
case 'capture':
|
||||
if (!msg.id) return;
|
||||
const postId = msg.id;
|
||||
|
|
|
@ -9,6 +9,7 @@ import isNativeToken from './common/is-native-token';
|
|||
import homeStream from './stream/home';
|
||||
import messagingStream from './stream/messaging';
|
||||
import serverStream from './stream/server';
|
||||
import channelStream from './stream/channel';
|
||||
|
||||
module.exports = (server: http.Server) => {
|
||||
/**
|
||||
|
@ -26,14 +27,6 @@ module.exports = (server: http.Server) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const user = await authenticate(request.resourceURL.query.i);
|
||||
|
||||
if (user == null) {
|
||||
connection.send('authentication-failed');
|
||||
connection.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Redis
|
||||
const subscriber = redis.createClient(
|
||||
config.redis.port, config.redis.host);
|
||||
|
@ -43,6 +36,19 @@ module.exports = (server: http.Server) => {
|
|||
subscriber.quit();
|
||||
});
|
||||
|
||||
if (request.resourceURL.pathname === '/channel') {
|
||||
channelStream(request, connection, subscriber);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await authenticate(request.resourceURL.query.i);
|
||||
|
||||
if (user == null) {
|
||||
connection.send('authentication-failed');
|
||||
connection.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const channel =
|
||||
request.resourceURL.pathname === '/' ? homeStream :
|
||||
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
* @param {*} post 投稿
|
||||
*/
|
||||
const summarize = (post: any): string => {
|
||||
let summary = post.text ? post.text : '';
|
||||
let summary = '';
|
||||
|
||||
// チャンネル
|
||||
summary += post.channel ? `${post.channel.title}:` : '';
|
||||
|
||||
// 本文
|
||||
summary += post.text ? post.text : '';
|
||||
|
||||
// メディアが添付されているとき
|
||||
if (post.media) {
|
||||
|
@ -16,9 +22,9 @@ const summarize = (post: any): string => {
|
|||
}
|
||||
|
||||
// 返信のとき
|
||||
if (post.reply_to_id) {
|
||||
if (post.reply_to) {
|
||||
summary += ` RE: ${summarize(post.reply_to)}`;
|
||||
if (post.reply_id) {
|
||||
if (post.reply) {
|
||||
summary += ` RE: ${summarize(post.reply)}`;
|
||||
} else {
|
||||
summary += ' RE: ...';
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ type Mixin = {
|
|||
api_url: string;
|
||||
auth_url: string;
|
||||
about_url: string;
|
||||
ch_url: string;
|
||||
stats_url: string;
|
||||
status_url: string;
|
||||
dev_url: string;
|
||||
|
@ -122,6 +123,7 @@ export default function load() {
|
|||
mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
|
||||
mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
|
||||
mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
|
||||
mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
|
||||
mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
|
||||
mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
|
||||
mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
|
||||
|
|
|
@ -1,11 +1,38 @@
|
|||
import * as mongo from 'monk';
|
||||
|
||||
import config from '../conf';
|
||||
|
||||
const uri = config.mongodb.user && config.mongodb.pass
|
||||
? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
|
||||
: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
||||
? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
|
||||
: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
||||
|
||||
/**
|
||||
* monk
|
||||
*/
|
||||
import * as mongo from 'monk';
|
||||
|
||||
const db = mongo(uri);
|
||||
|
||||
export default db;
|
||||
|
||||
/**
|
||||
* MongoDB native module (officialy)
|
||||
*/
|
||||
import * as mongodb from 'mongodb';
|
||||
|
||||
let mdb: mongodb.Db;
|
||||
|
||||
const nativeDbConn = async (): Promise<mongodb.Db> => {
|
||||
if (mdb) return mdb;
|
||||
|
||||
const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
|
||||
mongodb.MongoClient.connect(uri, (e, db) => {
|
||||
if (e) return reject(e);
|
||||
resolve(db);
|
||||
});
|
||||
}))();
|
||||
|
||||
mdb = db;
|
||||
|
||||
return db;
|
||||
};
|
||||
|
||||
export { nativeDbConn };
|
||||
|
|
|
@ -52,11 +52,11 @@ block content
|
|||
td Number
|
||||
td 返信数
|
||||
tr.optional
|
||||
td reply_to
|
||||
td reply
|
||||
td: a(href='./post', target='_blank') Post
|
||||
td 返信先の投稿
|
||||
tr.nullable
|
||||
td reply_to_id
|
||||
td reply_id
|
||||
td ID
|
||||
td 返信先の投稿のID
|
||||
tr.optional
|
||||
|
@ -90,7 +90,7 @@ block content
|
|||
{
|
||||
"created_at": "2016-12-10T00:28:50.114Z",
|
||||
"media_ids": null,
|
||||
"reply_to_id": "584a16b15860fc52320137e3",
|
||||
"reply_id": "584a16b15860fc52320137e3",
|
||||
"repost_id": null,
|
||||
"text": "小日向美穂だぞ!",
|
||||
"user_id": "5848bf7764e572683f4402f8",
|
||||
|
@ -117,10 +117,10 @@ block content
|
|||
"is_following": true,
|
||||
"is_followed": true
|
||||
},
|
||||
"reply_to": {
|
||||
"reply": {
|
||||
"created_at": "2016-12-09T02:28:01.563Z",
|
||||
"media_ids": null,
|
||||
"reply_to_id": "5849d35e547e4249be329884",
|
||||
"reply_id": "5849d35e547e4249be329884",
|
||||
"repost_id": null,
|
||||
"text": "アイコン小日向美穂?",
|
||||
"user_id": "57d01a501fdf2d07be417afe",
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as cors from 'cors';
|
|||
import * as mongodb from 'mongodb';
|
||||
import * as gm from 'gm';
|
||||
|
||||
import File from '../api/models/drive-file';
|
||||
import DriveFile, { getGridFSBucket } from '../api/models/drive-file';
|
||||
|
||||
/**
|
||||
* Init app
|
||||
|
@ -97,17 +97,28 @@ app.get('/:id', async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
|
||||
const fileId = new mongodb.ObjectID(req.params.id);
|
||||
const file = await DriveFile.findOne({ _id: fileId });
|
||||
|
||||
if (file == null) {
|
||||
res.status(404).sendFile(`${__dirname} / assets / dummy.png`);
|
||||
return;
|
||||
} else if (file.data == null) {
|
||||
res.sendStatus(400);
|
||||
res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
|
||||
return;
|
||||
}
|
||||
|
||||
send(file.data.buffer, file.type, req, res);
|
||||
const bucket = await getGridFSBucket();
|
||||
|
||||
const buffer = await ((id): Promise<Buffer> => new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
const readableStream = bucket.openDownloadStream(id);
|
||||
readableStream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
readableStream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
}))(fileId);
|
||||
|
||||
send(buffer, file.metadata.type, req, res);
|
||||
});
|
||||
|
||||
app.get('/:id/:name', async (req, res) => {
|
||||
|
@ -117,17 +128,28 @@ app.get('/:id/:name', async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
|
||||
const fileId = new mongodb.ObjectID(req.params.id);
|
||||
const file = await DriveFile.findOne({ _id: fileId });
|
||||
|
||||
if (file == null) {
|
||||
res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
|
||||
return;
|
||||
} else if (file.data == null) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
send(file.data.buffer, file.type, req, res);
|
||||
const bucket = await getGridFSBucket();
|
||||
|
||||
const buffer = await ((id): Promise<Buffer> => new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
const readableStream = bucket.openDownloadStream(id);
|
||||
readableStream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
readableStream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
}))(fileId);
|
||||
|
||||
send(buffer, file.metadata.type, req, res);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
|
|
@ -5,8 +5,6 @@ json('../../const.json')
|
|||
$theme-color = themeColor
|
||||
$theme-color-foreground = themeColorForeground
|
||||
|
||||
@import './reset'
|
||||
|
||||
/*
|
||||
::selection
|
||||
background $theme-color
|
||||
|
@ -14,6 +12,9 @@ $theme-color-foreground = themeColorForeground
|
|||
*/
|
||||
|
||||
*
|
||||
position relative
|
||||
box-sizing border-box
|
||||
background-clip padding-box !important
|
||||
tap-highlight-color rgba($theme-color, 0.7)
|
||||
-webkit-tap-highlight-color rgba($theme-color, 0.7)
|
||||
|
||||
|
@ -29,6 +30,9 @@ html
|
|||
&, *
|
||||
cursor progress !important
|
||||
|
||||
body
|
||||
overflow-wrap break-word
|
||||
|
||||
#error
|
||||
padding 32px
|
||||
color #fff
|
|
@ -1,4 +1,5 @@
|
|||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
background #eee
|
||||
|
|
32
src/web/app/ch/router.js
Normal file
32
src/web/app/ch/router.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import * as riot from 'riot';
|
||||
const route = require('page');
|
||||
let page = null;
|
||||
|
||||
export default me => {
|
||||
route('/', index);
|
||||
route('/:channel', channel);
|
||||
route('*', notFound);
|
||||
|
||||
function index() {
|
||||
mount(document.createElement('mk-index'));
|
||||
}
|
||||
|
||||
function channel(ctx) {
|
||||
const el = document.createElement('mk-channel');
|
||||
el.setAttribute('id', ctx.params.channel);
|
||||
mount(el);
|
||||
}
|
||||
|
||||
function notFound() {
|
||||
mount(document.createElement('mk-not-found'));
|
||||
}
|
||||
|
||||
// EXEC
|
||||
route();
|
||||
};
|
||||
|
||||
function mount(content) {
|
||||
if (page) page.unmount();
|
||||
const body = document.getElementById('app');
|
||||
page = riot.mount(body.appendChild(content))[0];
|
||||
}
|
18
src/web/app/ch/script.js
Normal file
18
src/web/app/ch/script.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Channels
|
||||
*/
|
||||
|
||||
// Style
|
||||
import './style.styl';
|
||||
|
||||
require('./tags');
|
||||
import init from '../init';
|
||||
import route from './router';
|
||||
|
||||
/**
|
||||
* init
|
||||
*/
|
||||
init(me => {
|
||||
// Start routing
|
||||
route(me);
|
||||
});
|
10
src/web/app/ch/style.styl
Normal file
10
src/web/app/ch/style.styl
Normal file
|
@ -0,0 +1,10 @@
|
|||
@import "../app"
|
||||
|
||||
html
|
||||
padding 8px
|
||||
background #efefef
|
||||
|
||||
#wait
|
||||
top auto
|
||||
bottom 15px
|
||||
left 15px
|
403
src/web/app/ch/tags/channel.tag
Normal file
403
src/web/app/ch/tags/channel.tag
Normal file
|
@ -0,0 +1,403 @@
|
|||
<mk-channel>
|
||||
<mk-header/>
|
||||
<hr>
|
||||
<main if={ !fetching }>
|
||||
<h1>{ channel.title }</h1>
|
||||
|
||||
<div if={ SIGNIN }>
|
||||
<p if={ channel.is_watching }>このチャンネルをウォッチしています <a onclick={ unwatch }>ウォッチ解除</a></p>
|
||||
<p if={ !channel.is_watching }><a onclick={ watch }>このチャンネルをウォッチする</a></p>
|
||||
</div>
|
||||
|
||||
<div class="share">
|
||||
<mk-twitter-button/>
|
||||
<mk-line-button/>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
|
||||
<div if={ !postsFetching }>
|
||||
<p if={ posts == null || posts.length == 0 }>まだ投稿がありません</p>
|
||||
<virtual if={ posts != null }>
|
||||
<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
|
||||
</virtual>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
|
||||
<div if={ !SIGNIN }>
|
||||
<p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p>
|
||||
</div>
|
||||
<hr>
|
||||
<footer>
|
||||
<small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small>
|
||||
</footer>
|
||||
</main>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> main
|
||||
> h1
|
||||
font-size 1.5em
|
||||
color #f00
|
||||
|
||||
> .share
|
||||
> *
|
||||
margin-right 4px
|
||||
|
||||
> .body
|
||||
margin 8px 0 0 0
|
||||
|
||||
> mk-channel-form
|
||||
max-width 500px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import Progress from '../../common/scripts/loading';
|
||||
import ChannelStream from '../../common/scripts/channel-stream';
|
||||
|
||||
this.mixin('i');
|
||||
this.mixin('api');
|
||||
|
||||
this.id = this.opts.id;
|
||||
this.fetching = true;
|
||||
this.postsFetching = true;
|
||||
this.channel = null;
|
||||
this.posts = null;
|
||||
this.connection = new ChannelStream(this.id);
|
||||
this.version = VERSION;
|
||||
this.unreadCount = 0;
|
||||
|
||||
this.on('mount', () => {
|
||||
document.documentElement.style.background = '#efefef';
|
||||
|
||||
Progress.start();
|
||||
|
||||
let fetched = false;
|
||||
|
||||
// チャンネル概要読み込み
|
||||
this.api('channels/show', {
|
||||
channel_id: this.id
|
||||
}).then(channel => {
|
||||
if (fetched) {
|
||||
Progress.done();
|
||||
} else {
|
||||
Progress.set(0.5);
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
this.update({
|
||||
fetching: false,
|
||||
channel: channel
|
||||
});
|
||||
|
||||
document.title = channel.title + ' | Misskey'
|
||||
});
|
||||
|
||||
// 投稿読み込み
|
||||
this.api('channels/posts', {
|
||||
channel_id: this.id
|
||||
}).then(posts => {
|
||||
if (fetched) {
|
||||
Progress.done();
|
||||
} else {
|
||||
Progress.set(0.5);
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
this.update({
|
||||
postsFetching: false,
|
||||
posts: posts
|
||||
});
|
||||
});
|
||||
|
||||
this.connection.on('post', this.onPost);
|
||||
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.connection.off('post', this.onPost);
|
||||
this.connection.close();
|
||||
document.removeEventListener('visibilitychange', this.onVisibilitychange);
|
||||
});
|
||||
|
||||
this.onPost = post => {
|
||||
this.posts.unshift(post);
|
||||
this.update();
|
||||
|
||||
if (document.hidden && this.SIGNIN && post.user_id !== this.I.id) {
|
||||
this.unreadCount++;
|
||||
document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
|
||||
}
|
||||
};
|
||||
|
||||
this.onVisibilitychange = () => {
|
||||
if (!document.hidden) {
|
||||
this.unreadCount = 0;
|
||||
document.title = this.channel.title + ' | Misskey'
|
||||
}
|
||||
};
|
||||
|
||||
this.watch = () => {
|
||||
this.api('channels/watch', {
|
||||
channel_id: this.id
|
||||
}).then(() => {
|
||||
this.channel.is_watching = true;
|
||||
this.update();
|
||||
}, e => {
|
||||
alert('error');
|
||||
});
|
||||
};
|
||||
|
||||
this.unwatch = () => {
|
||||
this.api('channels/unwatch', {
|
||||
channel_id: this.id
|
||||
}).then(() => {
|
||||
this.channel.is_watching = false;
|
||||
this.update();
|
||||
}, e => {
|
||||
alert('error');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-channel>
|
||||
|
||||
<mk-channel-post>
|
||||
<header>
|
||||
<a class="index" onclick={ reply }>{ post.index }:</a>
|
||||
<a class="name" href={ CONFIG.url + '/' + post.user.username }><b>{ post.user.name }</b></a>
|
||||
<mk-time time={ post.created_at }/>
|
||||
<mk-time time={ post.created_at } mode="detail"/>
|
||||
<span>ID:<i>{ post.user.username }</i></span>
|
||||
</header>
|
||||
<div>
|
||||
<a if={ post.reply }>>>{ post.reply.index }</a>
|
||||
{ post.text }
|
||||
<div class="media" if={ post.media }>
|
||||
<virtual each={ file in post.media }>
|
||||
<a href={ file.url } target="_blank">
|
||||
<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
|
||||
</a>
|
||||
</virtual>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
> header
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
z-index 1
|
||||
top 0
|
||||
background rgba(239, 239, 239, 0.9)
|
||||
|
||||
> .index
|
||||
margin-right 0.25em
|
||||
color #000
|
||||
|
||||
> .name
|
||||
margin-right 0.5em
|
||||
color #008000
|
||||
|
||||
> mk-time
|
||||
margin-right 0.5em
|
||||
|
||||
&:first-of-type
|
||||
display none
|
||||
|
||||
@media (max-width 600px)
|
||||
> mk-time
|
||||
&:first-of-type
|
||||
display initial
|
||||
|
||||
&:last-of-type
|
||||
display none
|
||||
|
||||
> div
|
||||
padding 0 0 1em 2em
|
||||
|
||||
> .media
|
||||
> a
|
||||
display inline-block
|
||||
|
||||
> img
|
||||
max-width 100%
|
||||
vertical-align bottom
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.post = this.opts.post;
|
||||
this.form = this.opts.form;
|
||||
|
||||
this.reply = () => {
|
||||
this.form.update({
|
||||
reply: this.post
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-channel-post>
|
||||
|
||||
<mk-channel-form>
|
||||
<p if={ reply }><b>>>{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
|
||||
<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
|
||||
<div class="actions">
|
||||
<button onclick={ selectFile }><i class="fa fa-upload"></i>%i18n:ch.tags.mk-channel-form.upload%</button>
|
||||
<button onclick={ drive }><i class="fa fa-cloud"></i>%i18n:ch.tags.mk-channel-form.drive%</button>
|
||||
<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
|
||||
<i class="fa fa-paper-plane" if={ !wait }></i>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis if={ wait }/>
|
||||
</button>
|
||||
</div>
|
||||
<mk-uploader ref="uploader"/>
|
||||
<ol if={ files }>
|
||||
<li each={ files }>{ name }</li>
|
||||
</ol>
|
||||
<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> textarea
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
min-height 5em
|
||||
|
||||
> .actions
|
||||
display flex
|
||||
|
||||
> button
|
||||
> i
|
||||
margin-right 0.25em
|
||||
|
||||
&:last-child
|
||||
margin-left auto
|
||||
|
||||
&.wait
|
||||
cursor wait
|
||||
|
||||
> input[type='file']
|
||||
display none
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import CONFIG from '../../common/scripts/config';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.channel = this.opts.channel;
|
||||
this.files = null;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.refs.uploader.on('uploaded', file => {
|
||||
this.update({
|
||||
files: [file]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.upload = file => {
|
||||
this.refs.uploader.upload(file);
|
||||
};
|
||||
|
||||
this.clearReply = () => {
|
||||
this.update({
|
||||
reply: null
|
||||
});
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
this.clearReply();
|
||||
this.update({
|
||||
files: null
|
||||
});
|
||||
this.refs.text.value = '';
|
||||
};
|
||||
|
||||
this.post = () => {
|
||||
this.update({
|
||||
wait: true
|
||||
});
|
||||
|
||||
const files = this.files && this.files.length > 0
|
||||
? this.files.map(f => f.id)
|
||||
: undefined;
|
||||
|
||||
this.api('posts/create', {
|
||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||
media_ids: files,
|
||||
reply_id: this.reply ? this.reply.id : undefined,
|
||||
channel_id: this.channel.id
|
||||
}).then(data => {
|
||||
this.clear();
|
||||
}).catch(err => {
|
||||
alert('失敗した');
|
||||
}).then(() => {
|
||||
this.update({
|
||||
wait: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.changeFile = () => {
|
||||
this.refs.file.files.forEach(this.upload);
|
||||
};
|
||||
|
||||
this.selectFile = () => {
|
||||
this.refs.file.click();
|
||||
};
|
||||
|
||||
this.drive = () => {
|
||||
window['cb'] = files => {
|
||||
this.update({
|
||||
files: files
|
||||
});
|
||||
};
|
||||
|
||||
window.open(CONFIG.url + '/selectdrive?multiple=true',
|
||||
'drive_window',
|
||||
'height=500,width=800');
|
||||
};
|
||||
|
||||
this.onkeydown = e => {
|
||||
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
|
||||
};
|
||||
|
||||
this.onpaste = e => {
|
||||
e.clipboardData.items.forEach(item => {
|
||||
if (item.kind == 'file') {
|
||||
this.upload(item.getAsFile());
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-channel-form>
|
||||
|
||||
<mk-twitter-button>
|
||||
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
|
||||
<script>
|
||||
this.on('mount', () => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
|
||||
script.setAttribute('async', 'async');
|
||||
head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</mk-twitter-button>
|
||||
|
||||
<mk-line-button>
|
||||
<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ CONFIG.chUrl } style="display: none;"></div>
|
||||
<script>
|
||||
this.on('mount', () => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://d.line-scdn.net/r/web/social-plugin/js/thirdparty/loader.min.js');
|
||||
script.setAttribute('async', 'async');
|
||||
head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</mk-line-button>
|
20
src/web/app/ch/tags/header.tag
Normal file
20
src/web/app/ch/tags/header.tag
Normal file
|
@ -0,0 +1,20 @@
|
|||
<mk-header>
|
||||
<div>
|
||||
<a href={ CONFIG.chUrl }>Index</a> | <a href={ CONFIG.url }>Misskey</a>
|
||||
</div>
|
||||
<div>
|
||||
<a if={ !SIGNIN } href={ CONFIG.url }>ログイン(新規登録)</a>
|
||||
<a if={ SIGNIN } href={ CONFIG.url + '/' + I.username }>{ I.username }</a>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display flex
|
||||
|
||||
> div:last-child
|
||||
margin-left auto
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
</script>
|
||||
</mk-header>
|
3
src/web/app/ch/tags/index.js
Normal file
3
src/web/app/ch/tags/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
require('./index.tag');
|
||||
require('./channel.tag');
|
||||
require('./header.tag');
|
35
src/web/app/ch/tags/index.tag
Normal file
35
src/web/app/ch/tags/index.tag
Normal file
|
@ -0,0 +1,35 @@
|
|||
<mk-index>
|
||||
<mk-header/>
|
||||
<hr>
|
||||
<button onclick={ n }>%i18n:ch.tags.mk-index.new%</button>
|
||||
<hr>
|
||||
<ul if={ channels }>
|
||||
<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
|
||||
</ul>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('channels').then(channels => {
|
||||
this.update({
|
||||
channels: channels
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.n = () => {
|
||||
const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%');
|
||||
|
||||
this.api('channels/create', {
|
||||
title: title
|
||||
}).then(channel => {
|
||||
location.href = '/' + channel.id;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-index>
|
16
src/web/app/common/scripts/channel-stream.js
Normal file
16
src/web/app/common/scripts/channel-stream.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
import Stream from './stream';
|
||||
|
||||
/**
|
||||
* Channel stream connection
|
||||
*/
|
||||
class Connection extends Stream {
|
||||
constructor(channelId) {
|
||||
super('channel', {
|
||||
channel: channelId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Connection;
|
|
@ -6,6 +6,7 @@ const host = isRoot ? Url.host : Url.host.substring(Url.host.indexOf('.') + 1, U
|
|||
const scheme = Url.protocol;
|
||||
const url = `${scheme}//${host}`;
|
||||
const apiUrl = `${scheme}//api.${host}`;
|
||||
const chUrl = `${scheme}//ch.${host}`;
|
||||
const devUrl = `${scheme}//dev.${host}`;
|
||||
const aboutUrl = `${scheme}//about.${host}`;
|
||||
const statsUrl = `${scheme}//stats.${host}`;
|
||||
|
@ -16,6 +17,7 @@ export default {
|
|||
scheme,
|
||||
url,
|
||||
apiUrl,
|
||||
chUrl,
|
||||
devUrl,
|
||||
aboutUrl,
|
||||
statsUrl,
|
||||
|
|
|
@ -8,6 +8,7 @@ let page = null;
|
|||
|
||||
export default me => {
|
||||
route('/', index);
|
||||
route('/selectdrive', selectDrive);
|
||||
route('/i>mentions', mentions);
|
||||
route('/post::post', post);
|
||||
route('/search::query', search);
|
||||
|
@ -54,6 +55,10 @@ export default me => {
|
|||
mount(el);
|
||||
}
|
||||
|
||||
function selectDrive() {
|
||||
mount(document.createElement('mk-selectdrive-page'));
|
||||
}
|
||||
|
||||
function notFound() {
|
||||
mount(document.createElement('mk-not-found'));
|
||||
}
|
||||
|
@ -67,6 +72,7 @@ export default me => {
|
|||
};
|
||||
|
||||
function mount(content) {
|
||||
document.documentElement.style.background = '#313a42';
|
||||
document.documentElement.removeAttribute('data-page');
|
||||
if (page) page.unmount();
|
||||
const body = document.getElementById('app');
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
@import "../../../../node_modules/cropperjs/dist/cropper.css"
|
||||
|
||||
*::input-placeholder
|
||||
|
|
|
@ -61,6 +61,7 @@ require('./pages/user.tag');
|
|||
require('./pages/post.tag');
|
||||
require('./pages/search.tag');
|
||||
require('./pages/not-found.tag');
|
||||
require('./pages/selectdrive.tag');
|
||||
require('./autocomplete-suggestion.tag');
|
||||
require('./progress-dialog.tag');
|
||||
require('./user-preview.tag');
|
||||
|
|
|
@ -252,6 +252,12 @@
|
|||
});
|
||||
|
||||
this.onNotification = notification => {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.stream.send({
|
||||
type: 'read_notification',
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.notifications.unshift(notification);
|
||||
this.update();
|
||||
};
|
||||
|
|
160
src/web/app/desktop/tags/pages/selectdrive.tag
Normal file
160
src/web/app/desktop/tags/pages/selectdrive.tag
Normal file
|
@ -0,0 +1,160 @@
|
|||
<mk-selectdrive-page>
|
||||
<mk-drive-browser ref="browser" multiple={ multiple }/>
|
||||
<div>
|
||||
<button class="upload" title="PCからドライブにファイルをアップロード" onclick={ upload }><i class="fa fa-upload"></i></button>
|
||||
<button class="cancel" onclick={ close }>キャンセル</button>
|
||||
<button class="ok" onclick={ ok }>決定</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
position fixed
|
||||
height 100%
|
||||
background #fff
|
||||
|
||||
> mk-drive-browser
|
||||
height calc(100% - 72px)
|
||||
|
||||
> div
|
||||
position fixed
|
||||
bottom 0
|
||||
left 0
|
||||
width 100%
|
||||
height 72px
|
||||
background lighten($theme-color, 95%)
|
||||
|
||||
.upload
|
||||
display inline-block
|
||||
position absolute
|
||||
top 8px
|
||||
left 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 8px 4px 0 0
|
||||
width 40px
|
||||
height 40px
|
||||
font-size 1em
|
||||
color rgba($theme-color, 0.5)
|
||||
background transparent
|
||||
outline none
|
||||
border solid 1px transparent
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background transparent
|
||||
border-color rgba($theme-color, 0.3)
|
||||
|
||||
&:active
|
||||
color rgba($theme-color, 0.6)
|
||||
background transparent
|
||||
border-color rgba($theme-color, 0.5)
|
||||
box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
.ok
|
||||
.cancel
|
||||
display block
|
||||
position absolute
|
||||
bottom 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 0
|
||||
width 120px
|
||||
height 40px
|
||||
font-size 1em
|
||||
outline none
|
||||
border-radius 4px
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
cursor default
|
||||
|
||||
.ok
|
||||
right 16px
|
||||
color $theme-color-foreground
|
||||
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||
border solid 1px lighten($theme-color, 15%)
|
||||
|
||||
&:not(:disabled)
|
||||
font-weight bold
|
||||
|
||||
&:hover:not(:disabled)
|
||||
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
|
||||
border-color $theme-color
|
||||
|
||||
&:active:not(:disabled)
|
||||
background $theme-color
|
||||
border-color $theme-color
|
||||
|
||||
.cancel
|
||||
right 148px
|
||||
color #888
|
||||
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||
border solid 1px #e2e2e2
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
|
||||
border-color #dcdcdc
|
||||
|
||||
&:active
|
||||
background #ececec
|
||||
border-color #dcdcdc
|
||||
|
||||
</style>
|
||||
<script>
|
||||
const q = (new URL(location)).searchParams;
|
||||
this.multiple = q.get('multiple') == 'true' ? true : false;
|
||||
|
||||
this.on('mount', () => {
|
||||
document.documentElement.style.background = '#fff';
|
||||
|
||||
this.refs.browser.on('selected', file => {
|
||||
this.files = [file];
|
||||
this.ok();
|
||||
});
|
||||
|
||||
this.refs.browser.on('change-selection', files => {
|
||||
this.update({
|
||||
files: files
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.upload = () => {
|
||||
this.refs.browser.selectLocalFile();
|
||||
};
|
||||
|
||||
this.close = () => {
|
||||
window.close();
|
||||
};
|
||||
|
||||
this.ok = () => {
|
||||
window.opener.cb(this.multiple ? this.files : this.files[0]);
|
||||
window.close();
|
||||
};
|
||||
</script>
|
||||
</mk-selectdrive-page>
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
this.refs.ui.refs.user.on('user-fetched', user => {
|
||||
Progress.set(0.5);
|
||||
document.title = user.name + ' | Misskey'
|
||||
document.title = user.name + ' | Misskey';
|
||||
});
|
||||
|
||||
this.refs.ui.refs.user.on('loaded', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<mk-post-detail title={ title }>
|
||||
<div class="main">
|
||||
<button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
|
||||
<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
|
||||
<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
|
||||
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
||||
</button>
|
||||
|
@ -9,8 +9,8 @@
|
|||
<mk-post-detail-sub post={ post }/>
|
||||
</virtual>
|
||||
</div>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-post-detail-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-post-detail-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
|
@ -329,7 +329,7 @@
|
|||
|
||||
// Fetch context
|
||||
this.api('posts/context', {
|
||||
post_id: this.p.reply_to_id
|
||||
post_id: this.p.reply_id
|
||||
}).then(context => {
|
||||
this.update({
|
||||
contextFetching: false,
|
||||
|
|
|
@ -475,7 +475,7 @@
|
|||
this.api('posts/create', {
|
||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||
media_ids: files,
|
||||
reply_to_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
|
||||
reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
|
||||
repost_id: this.repost ? this.repost.id : undefined,
|
||||
poll: this.poll ? this.refs.poll.get() : undefined
|
||||
}).then(data => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<mk-sub-post-content>
|
||||
<div class="body">
|
||||
<a class="reply" if={ post.reply_to_id }>
|
||||
<a class="reply" if={ post.reply_id }>
|
||||
<i class="fa fa-reply"></i>
|
||||
</a>
|
||||
<span ref="text"></span>
|
||||
|
|
|
@ -82,8 +82,8 @@
|
|||
</mk-timeline>
|
||||
|
||||
<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-timeline-post-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-timeline-post-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
|
@ -112,7 +112,8 @@
|
|||
</header>
|
||||
<div class="body">
|
||||
<div class="text" ref="text">
|
||||
<a class="reply" if={ p.reply_to }>
|
||||
<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
|
||||
<a class="reply" if={ p.reply }>
|
||||
<i class="fa fa-reply"></i>
|
||||
</a>
|
||||
<p class="dummy"></p>
|
||||
|
@ -333,6 +334,9 @@
|
|||
font-weight 400
|
||||
font-style normal
|
||||
|
||||
> .channel
|
||||
margin 0
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color #717171
|
||||
|
|
|
@ -319,7 +319,8 @@
|
|||
</mk-ui-header-notifications>
|
||||
|
||||
<mk-ui-header-nav>
|
||||
<ul if={ SIGNIN }>
|
||||
<ul>
|
||||
<virtual if={ SIGNIN }>
|
||||
<li class="home { active: page == 'home' }">
|
||||
<a href={ CONFIG.url }>
|
||||
<i class="fa fa-home"></i>
|
||||
|
@ -333,6 +334,13 @@
|
|||
<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
|
||||
</a>
|
||||
</li>
|
||||
</virtual>
|
||||
<li class="ch">
|
||||
<a href={ CONFIG.chUrl } target="_blank">
|
||||
<i class="fa fa-television"></i>
|
||||
<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="info">
|
||||
<a href="https://twitter.com/misskey_xyz" target="_blank">
|
||||
<i class="fa fa-info"></i>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
background-color #fff
|
||||
|
|
|
@ -21,6 +21,11 @@ require('./common/tags');
|
|||
|
||||
console.info(`Misskey v${VERSION} (葵 aoi)`);
|
||||
|
||||
{ // Set lang attr
|
||||
const html = document.documentElement;
|
||||
html.setAttribute('lang', LANG);
|
||||
}
|
||||
|
||||
{ // Set description meta tag
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const meta = document.createElement('meta');
|
||||
|
|
|
@ -8,6 +8,7 @@ let page = null;
|
|||
|
||||
export default me => {
|
||||
route('/', index);
|
||||
route('/selectdrive', selectDrive);
|
||||
route('/i/notifications', notifications);
|
||||
route('/i/messaging', messaging);
|
||||
route('/i/messaging/:username', messaging);
|
||||
|
@ -122,6 +123,10 @@ export default me => {
|
|||
mount(el);
|
||||
}
|
||||
|
||||
function selectDrive() {
|
||||
mount(document.createElement('mk-selectdrive-page'));
|
||||
}
|
||||
|
||||
function notFound() {
|
||||
mount(document.createElement('mk-not-found'));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
#wait
|
||||
top auto
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<mk-drive>
|
||||
<nav>
|
||||
<nav ref="nav">
|
||||
<p onclick={ goRoot }><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive.drive%</p>
|
||||
<virtual each={ folder in hierarchyFolders }>
|
||||
<span><i class="fa fa-angle-right"></i></span>
|
||||
|
@ -56,10 +56,6 @@
|
|||
display block
|
||||
background #fff
|
||||
|
||||
&[data-is-naked]
|
||||
> nav
|
||||
top 48px
|
||||
|
||||
> nav
|
||||
display block
|
||||
position sticky
|
||||
|
@ -205,6 +201,10 @@
|
|||
} else {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
if (this.opts.isNaked) {
|
||||
this.refs.nav.style.top = `${this.opts.top}px`;
|
||||
}
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
|
@ -483,7 +483,7 @@
|
|||
if (fn == null || fn == '') return;
|
||||
switch (fn) {
|
||||
case '1':
|
||||
this.refs.file.click();
|
||||
this.selectLocalFile();
|
||||
break;
|
||||
case '2':
|
||||
this.urlUpload();
|
||||
|
@ -503,6 +503,10 @@
|
|||
}
|
||||
};
|
||||
|
||||
this.selectLocalFile = () => {
|
||||
this.refs.file.click();
|
||||
};
|
||||
|
||||
this.createFolder = () => {
|
||||
const name = window.prompt('フォルダー名');
|
||||
if (name == null || name == '') return;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
require('./ui.tag');
|
||||
require('./ui-header.tag');
|
||||
require('./ui-nav.tag');
|
||||
require('./page/entrance.tag');
|
||||
require('./page/entrance/signin.tag');
|
||||
require('./page/entrance/signup.tag');
|
||||
|
@ -21,6 +19,7 @@ require('./page/settings/authorized-apps.tag');
|
|||
require('./page/settings/twitter.tag');
|
||||
require('./page/messaging.tag');
|
||||
require('./page/messaging-room.tag');
|
||||
require('./page/selectdrive.tag');
|
||||
require('./home.tag');
|
||||
require('./home-timeline.tag');
|
||||
require('./timeline.tag');
|
||||
|
|
|
@ -123,6 +123,12 @@
|
|||
});
|
||||
|
||||
this.onNotification = notification => {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.stream.send({
|
||||
type: 'read_notification',
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.notifications.unshift(notification);
|
||||
this.update();
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<mk-drive-page>
|
||||
<mk-ui ref="ui">
|
||||
<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } data-is-naked="true"/>
|
||||
<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } is-naked={ true } top={ 48 }/>
|
||||
</mk-ui>
|
||||
<style>
|
||||
:scope
|
||||
|
|
|
@ -10,16 +10,30 @@
|
|||
import ui from '../../scripts/ui-event';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.on('mount', () => {
|
||||
document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%';
|
||||
ui.trigger('title', '<i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-notifications-page.notifications%');
|
||||
document.documentElement.style.background = '#313a42';
|
||||
|
||||
ui.trigger('func', () => {
|
||||
this.readAll();
|
||||
}, 'check');
|
||||
|
||||
Progress.start();
|
||||
|
||||
this.refs.ui.refs.notifications.on('fetched', () => {
|
||||
Progress.done();
|
||||
});
|
||||
});
|
||||
|
||||
this.readAll = () => {
|
||||
const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
this.api('notifications/mark_as_read_all');
|
||||
};
|
||||
</script>
|
||||
</mk-notifications-page>
|
||||
|
|
87
src/web/app/mobile/tags/page/selectdrive.tag
Normal file
87
src/web/app/mobile/tags/page/selectdrive.tag
Normal file
|
@ -0,0 +1,87 @@
|
|||
<mk-selectdrive-page>
|
||||
<header>
|
||||
<h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" if={ files.length > 0 }>({ files.length })</span></h1>
|
||||
<button class="upload" onclick={ upload }><i class="fa fa-upload"></i></button>
|
||||
<button if={ multiple } class="ok" onclick={ ok }><i class="fa fa-check"></i></button>
|
||||
</header>
|
||||
<mk-drive ref="browser" select-file={ true } multiple={ multiple } is-naked={ true } top={ 42 }/>
|
||||
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
width 100%
|
||||
height 100%
|
||||
background #fff
|
||||
|
||||
> header
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
z-index 1000
|
||||
background #fff
|
||||
box-shadow 0 1px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
padding 0
|
||||
text-align center
|
||||
line-height 42px
|
||||
font-size 1em
|
||||
font-weight normal
|
||||
|
||||
> .count
|
||||
margin-left 4px
|
||||
opacity 0.5
|
||||
|
||||
> .upload
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
line-height 42px
|
||||
width 42px
|
||||
|
||||
> .ok
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
line-height 42px
|
||||
width 42px
|
||||
|
||||
> mk-drive
|
||||
top 42px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
const q = (new URL(location)).searchParams;
|
||||
this.multiple = q.get('multiple') == 'true' ? true : false;
|
||||
|
||||
this.on('mount', () => {
|
||||
document.documentElement.style.background = '#fff';
|
||||
|
||||
this.refs.browser.on('selected', file => {
|
||||
this.files = [file];
|
||||
this.ok();
|
||||
});
|
||||
|
||||
this.refs.browser.on('change-selection', files => {
|
||||
this.update({
|
||||
files: files
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.upload = () => {
|
||||
this.refs.browser.selectLocalFile();
|
||||
};
|
||||
|
||||
this.close = () => {
|
||||
window.close();
|
||||
};
|
||||
|
||||
this.ok = () => {
|
||||
window.opener.cb(this.multiple ? this.files : this.files[0]);
|
||||
window.close();
|
||||
};
|
||||
</script>
|
||||
</mk-selectdrive-page>
|
|
@ -1,5 +1,5 @@
|
|||
<mk-post-detail>
|
||||
<button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
|
||||
<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
|
||||
<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
|
||||
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
||||
</button>
|
||||
|
@ -8,8 +8,8 @@
|
|||
<mk-post-detail-sub post={ post }/>
|
||||
</virtual>
|
||||
</div>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-post-detail-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-post-detail-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
|
@ -348,7 +348,7 @@
|
|||
|
||||
// Fetch context
|
||||
this.api('posts/context', {
|
||||
post_id: this.p.reply_to_id
|
||||
post_id: this.p.reply_id
|
||||
}).then(context => {
|
||||
this.update({
|
||||
contextFetching: false,
|
||||
|
|
|
@ -267,7 +267,7 @@
|
|||
this.api('posts/create', {
|
||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||
media_ids: files,
|
||||
reply_to_id: opts.reply ? opts.reply.id : undefined,
|
||||
reply_id: opts.reply ? opts.reply.id : undefined,
|
||||
poll: this.poll ? this.refs.poll.get() : undefined
|
||||
}).then(data => {
|
||||
this.trigger('post');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<mk-sub-post-content>
|
||||
<div class="body"><a class="reply" if={ post.reply_to_id }><i class="fa fa-reply"></i></a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
|
||||
<div class="body"><a class="reply" if={ post.reply_id }><i class="fa fa-reply"></i></a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
|
||||
<details if={ post.media }>
|
||||
<summary>({ post.media.length }個のメディア)</summary>
|
||||
<mk-images-viewer images={ post.media }/>
|
||||
|
|
|
@ -137,8 +137,8 @@
|
|||
</mk-timeline>
|
||||
|
||||
<mk-timeline-post class={ repost: isRepost }>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-timeline-post-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-timeline-post-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
|
@ -164,7 +164,8 @@
|
|||
</header>
|
||||
<div class="body">
|
||||
<div class="text" ref="text">
|
||||
<a class="reply" if={ p.reply_to }>
|
||||
<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
|
||||
<a class="reply" if={ p.reply }>
|
||||
<i class="fa fa-reply"></i>
|
||||
</a>
|
||||
<p class="dummy"></p>
|
||||
|
@ -373,6 +374,9 @@
|
|||
mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .channel
|
||||
margin 0
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color #717171
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
<mk-ui-header>
|
||||
<mk-special-message/>
|
||||
<div class="main">
|
||||
<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>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
$height = 48px
|
||||
|
||||
display block
|
||||
position fixed
|
||||
top 0
|
||||
z-index 1024
|
||||
width 100%
|
||||
box-shadow 0 1px 0 rgba(#000, 0.075)
|
||||
|
||||
> .main
|
||||
color rgba(#fff, 0.9)
|
||||
|
||||
> .backdrop
|
||||
position absolute
|
||||
top 0
|
||||
z-index 1023
|
||||
width 100%
|
||||
height $height
|
||||
-webkit-backdrop-filter blur(12px)
|
||||
backdrop-filter blur(12px)
|
||||
background-color rgba(#1b2023, 0.75)
|
||||
|
||||
> .content
|
||||
z-index 1024
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 0
|
||||
width 100%
|
||||
max-width calc(100% - 112px)
|
||||
text-align center
|
||||
font-size 1.1em
|
||||
font-weight normal
|
||||
line-height $height
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
> i
|
||||
> .icon
|
||||
margin-right 8px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
width ($height - 16px)
|
||||
height ($height - 16px)
|
||||
margin 8px
|
||||
border-radius 6px
|
||||
|
||||
> .nav
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width $height
|
||||
font-size 1.4em
|
||||
line-height $height
|
||||
border-right solid 1px rgba(#000, 0.1)
|
||||
|
||||
> 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
|
||||
top 0
|
||||
right 0
|
||||
width $height
|
||||
text-align center
|
||||
font-size 1.4em
|
||||
color inherit
|
||||
line-height $height
|
||||
border-left solid 1px rgba(#000, 0.1)
|
||||
|
||||
</style>
|
||||
<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;
|
||||
};
|
||||
|
||||
this.setFunc = (fn, icon) => {
|
||||
this.update({
|
||||
func: fn,
|
||||
funcIcon: icon
|
||||
});
|
||||
};
|
||||
|
||||
ui.on('title', this.setTitle);
|
||||
ui.on('func', this.setFunc);
|
||||
</script>
|
||||
</mk-ui-header>
|
|
@ -1,170 +0,0 @@
|
|||
<mk-ui-nav>
|
||||
<div class="backdrop" onclick={ parent.toggleDrawer }></div>
|
||||
<div class="body">
|
||||
<a class="me" if={ SIGNIN } href={ '/' + I.username }>
|
||||
<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
|
||||
<p class="name">{ I.name }</p>
|
||||
</a>
|
||||
<div class="links">
|
||||
<ul>
|
||||
<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><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><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><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>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display none
|
||||
|
||||
.backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1025
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.2)
|
||||
|
||||
.body
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1026
|
||||
width 240px
|
||||
height 100%
|
||||
overflow auto
|
||||
-webkit-overflow-scrolling touch
|
||||
color #777
|
||||
background #fff
|
||||
|
||||
.me
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
|
||||
.avatar
|
||||
display inline
|
||||
max-width 64px
|
||||
border-radius 32px
|
||||
vertical-align middle
|
||||
|
||||
.name
|
||||
display block
|
||||
margin 0 16px
|
||||
position absolute
|
||||
top 0
|
||||
left 80px
|
||||
padding 0
|
||||
width calc(100% - 112px)
|
||||
color #777
|
||||
line-height 96px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
|
||||
ul
|
||||
display block
|
||||
margin 16px 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
li
|
||||
display block
|
||||
font-size 1em
|
||||
line-height 1em
|
||||
|
||||
a
|
||||
display block
|
||||
padding 0 20px
|
||||
line-height 3rem
|
||||
line-height calc(1rem + 30px)
|
||||
color #777
|
||||
text-decoration none
|
||||
|
||||
> i:first-child
|
||||
margin-right 0.5em
|
||||
|
||||
> .i
|
||||
margin-left 6px
|
||||
vertical-align super
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> i:last-child
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 0 20px
|
||||
font-size 1.2em
|
||||
line-height calc(1rem + 30px)
|
||||
color #ccc
|
||||
|
||||
.about
|
||||
margin 0
|
||||
padding 1em 0
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
opacity 0.5
|
||||
|
||||
a
|
||||
color #777
|
||||
|
||||
</style>
|
||||
<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%');
|
||||
if (query == null || query == '') return;
|
||||
this.page('/search:' + query);
|
||||
};
|
||||
</script>
|
||||
</mk-ui-nav>
|
|
@ -30,9 +30,378 @@
|
|||
};
|
||||
|
||||
this.onStreamNotification = notification => {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.stream.send({
|
||||
type: 'read_notification',
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
riot.mount(document.body.appendChild(document.createElement('mk-notify')), {
|
||||
notification: notification
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-ui>
|
||||
|
||||
<mk-ui-header>
|
||||
<mk-special-message/>
|
||||
<div class="main">
|
||||
<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={ hasUnreadNotifications || hasUnreadMessagingMessages }></i>
|
||||
<h1 ref="title">Misskey</h1>
|
||||
<button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
$height = 48px
|
||||
|
||||
display block
|
||||
position fixed
|
||||
top 0
|
||||
z-index 1024
|
||||
width 100%
|
||||
box-shadow 0 1px 0 rgba(#000, 0.075)
|
||||
|
||||
> .main
|
||||
color rgba(#fff, 0.9)
|
||||
|
||||
> .backdrop
|
||||
position absolute
|
||||
top 0
|
||||
z-index 1023
|
||||
width 100%
|
||||
height $height
|
||||
-webkit-backdrop-filter blur(12px)
|
||||
backdrop-filter blur(12px)
|
||||
background-color rgba(#1b2023, 0.75)
|
||||
|
||||
> .content
|
||||
z-index 1024
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 0
|
||||
width 100%
|
||||
max-width calc(100% - 112px)
|
||||
text-align center
|
||||
font-size 1.1em
|
||||
font-weight normal
|
||||
line-height $height
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
> i
|
||||
> .icon
|
||||
margin-right 8px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
width ($height - 16px)
|
||||
height ($height - 16px)
|
||||
margin 8px
|
||||
border-radius 6px
|
||||
|
||||
> .nav
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width $height
|
||||
font-size 1.4em
|
||||
line-height $height
|
||||
border-right solid 1px rgba(#000, 0.1)
|
||||
|
||||
> 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
|
||||
top 0
|
||||
right 0
|
||||
width $height
|
||||
text-align center
|
||||
font-size 1.4em
|
||||
color inherit
|
||||
line-height $height
|
||||
border-left solid 1px rgba(#000, 0.1)
|
||||
|
||||
</style>
|
||||
<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_notifications', this.onReadAllNotifications);
|
||||
this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread notifications
|
||||
this.api('notifications/get_unread_count').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadNotifications: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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_notifications', this.onReadAllNotifications);
|
||||
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.onReadAllNotifications = () => {
|
||||
this.update({
|
||||
hasUnreadNotifications: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onReadAllMessagingMessages = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onUnreadMessagingMessage = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
};
|
||||
|
||||
this.setTitle = title => {
|
||||
this.refs.title.innerHTML = title;
|
||||
};
|
||||
|
||||
this.setFunc = (fn, icon) => {
|
||||
this.update({
|
||||
func: fn,
|
||||
funcIcon: icon
|
||||
});
|
||||
};
|
||||
|
||||
ui.on('title', this.setTitle);
|
||||
ui.on('func', this.setFunc);
|
||||
</script>
|
||||
</mk-ui-header>
|
||||
|
||||
<mk-ui-nav>
|
||||
<div class="backdrop" onclick={ parent.toggleDrawer }></div>
|
||||
<div class="body">
|
||||
<a class="me" if={ SIGNIN } href={ '/' + I.username }>
|
||||
<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
|
||||
<p class="name">{ I.name }</p>
|
||||
</a>
|
||||
<div class="links">
|
||||
<ul>
|
||||
<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="i fa fa-circle" if={ hasUnreadNotifications }></i><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><a href={ CONFIG.chUrl } target="_blank"><i class="fa fa-television"></i>%i18n:mobile.tags.mk-ui-nav.ch%<i class="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><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><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>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display none
|
||||
|
||||
.backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1025
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.2)
|
||||
|
||||
.body
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1026
|
||||
width 240px
|
||||
height 100%
|
||||
overflow auto
|
||||
-webkit-overflow-scrolling touch
|
||||
color #777
|
||||
background #fff
|
||||
|
||||
.me
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
|
||||
.avatar
|
||||
display inline
|
||||
max-width 64px
|
||||
border-radius 32px
|
||||
vertical-align middle
|
||||
|
||||
.name
|
||||
display block
|
||||
margin 0 16px
|
||||
position absolute
|
||||
top 0
|
||||
left 80px
|
||||
padding 0
|
||||
width calc(100% - 112px)
|
||||
color #777
|
||||
line-height 96px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
|
||||
ul
|
||||
display block
|
||||
margin 16px 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
li
|
||||
display block
|
||||
font-size 1em
|
||||
line-height 1em
|
||||
|
||||
a
|
||||
display block
|
||||
padding 0 20px
|
||||
line-height 3rem
|
||||
line-height calc(1rem + 30px)
|
||||
color #777
|
||||
text-decoration none
|
||||
|
||||
> i:first-child
|
||||
margin-right 0.5em
|
||||
|
||||
> .i
|
||||
margin-left 6px
|
||||
vertical-align super
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> i:last-child
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 0 20px
|
||||
font-size 1.2em
|
||||
line-height calc(1rem + 30px)
|
||||
color #ccc
|
||||
|
||||
.about
|
||||
margin 0
|
||||
padding 1em 0
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
opacity 0.5
|
||||
|
||||
a
|
||||
color #777
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
this.mixin('page');
|
||||
this.mixin('api');
|
||||
this.mixin('stream');
|
||||
|
||||
this.on('mount', () => {
|
||||
this.stream.on('read_all_notifications', this.onReadAllNotifications);
|
||||
this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread notifications
|
||||
this.api('notifications/get_unread_count').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadNotifications: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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_notifications', this.onReadAllNotifications);
|
||||
this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
});
|
||||
|
||||
this.onReadAllNotifications = () => {
|
||||
this.update({
|
||||
hasUnreadNotifications: false
|
||||
});
|
||||
};
|
||||
|
||||
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%');
|
||||
if (query == null || query == '') return;
|
||||
this.page('/search:' + query);
|
||||
};
|
||||
</script>
|
||||
</mk-ui-nav>
|
||||
|
|
|
@ -1,16 +1,3 @@
|
|||
*
|
||||
position relative
|
||||
box-sizing border-box
|
||||
background-clip padding-box !important
|
||||
|
||||
html
|
||||
body
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
body
|
||||
overflow-wrap break-word
|
||||
|
||||
input:not([type])
|
||||
input[type='text']
|
||||
input[type='password']
|
||||
|
|
|
@ -7,5 +7,8 @@
|
|||
if (!('fetch' in window)) {
|
||||
alert(
|
||||
'お使いのブラウザが古いためMisskeyを動作させることができません。' +
|
||||
'バージョンを最新のものに更新するか、別のブラウザをお試しください。');
|
||||
'バージョンを最新のものに更新するか、別のブラウザをお試しください。' +
|
||||
'\n\n' +
|
||||
'Your browser seems outdated.' +
|
||||
'To run Misskey, please update your browser to latest version or try other browsers.');
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
color #456267
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue