mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-19 08:27:35 -07:00
Merge branch 'master' of https://github.com/syuilo/misskey
This commit is contained in:
commit
f5a5456253
88 changed files with 2167 additions and 558 deletions
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -2,6 +2,50 @@ ChangeLog (Release Notes)
|
||||||
=========================
|
=========================
|
||||||
主に notable な changes を書いていきます
|
主に 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)
|
2747 (2017/10/25)
|
||||||
-----------------
|
-----------------
|
||||||
* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
|
* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
|
||||||
|
|
|
@ -25,6 +25,7 @@ Note that Misskey uses following subdomains:
|
||||||
* **api**.*{primary domain}*
|
* **api**.*{primary domain}*
|
||||||
* **auth**.*{primary domain}*
|
* **auth**.*{primary domain}*
|
||||||
* **about**.*{primary domain}*
|
* **about**.*{primary domain}*
|
||||||
|
* **ch**.*{primary domain}*
|
||||||
* **stats**.*{primary domain}*
|
* **stats**.*{primary domain}*
|
||||||
* **status**.*{primary domain}*
|
* **status**.*{primary domain}*
|
||||||
* **dev**.*{primary domain}*
|
* **dev**.*{primary domain}*
|
||||||
|
|
|
@ -26,6 +26,7 @@ Misskeyは以下のサブドメインを使います:
|
||||||
* **api**.*{primary domain}*
|
* **api**.*{primary domain}*
|
||||||
* **auth**.*{primary domain}*
|
* **auth**.*{primary domain}*
|
||||||
* **about**.*{primary domain}*
|
* **about**.*{primary domain}*
|
||||||
|
* **ch**.*{primary domain}*
|
||||||
* **stats**.*{primary domain}*
|
* **stats**.*{primary domain}*
|
||||||
* **status**.*{primary domain}*
|
* **status**.*{primary domain}*
|
||||||
* **dev**.*{primary domain}*
|
* **dev**.*{primary domain}*
|
||||||
|
|
|
@ -164,6 +164,19 @@ common:
|
||||||
mk-uploader:
|
mk-uploader:
|
||||||
waiting: "Waiting"
|
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:
|
desktop:
|
||||||
tags:
|
tags:
|
||||||
mk-api-info:
|
mk-api-info:
|
||||||
|
@ -241,6 +254,7 @@ desktop:
|
||||||
mk-ui-header-nav:
|
mk-ui-header-nav:
|
||||||
home: "Home"
|
home: "Home"
|
||||||
messaging: "Messages"
|
messaging: "Messages"
|
||||||
|
ch: "Channels"
|
||||||
info: "News"
|
info: "News"
|
||||||
|
|
||||||
mk-ui-header-search:
|
mk-ui-header-search:
|
||||||
|
@ -353,6 +367,9 @@ desktop:
|
||||||
|
|
||||||
mobile:
|
mobile:
|
||||||
tags:
|
tags:
|
||||||
|
mk-selectdrive-page:
|
||||||
|
select-file: "Select file(s)"
|
||||||
|
|
||||||
mk-drive-file-viewer:
|
mk-drive-file-viewer:
|
||||||
download: "Download"
|
download: "Download"
|
||||||
rename: "Rename"
|
rename: "Rename"
|
||||||
|
@ -389,6 +406,7 @@ mobile:
|
||||||
|
|
||||||
mk-notifications-page:
|
mk-notifications-page:
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
|
read-all: "Are you sure you want to mark all unread notifications as read?"
|
||||||
|
|
||||||
mk-post-page:
|
mk-post-page:
|
||||||
title: "Post"
|
title: "Post"
|
||||||
|
@ -490,6 +508,7 @@ mobile:
|
||||||
home: "Home"
|
home: "Home"
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
messaging: "Messages"
|
messaging: "Messages"
|
||||||
|
ch: "Channels"
|
||||||
drive: "Drive"
|
drive: "Drive"
|
||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
about: "About Misskey"
|
about: "About Misskey"
|
||||||
|
|
|
@ -164,6 +164,19 @@ common:
|
||||||
mk-uploader:
|
mk-uploader:
|
||||||
waiting: "待機中"
|
waiting: "待機中"
|
||||||
|
|
||||||
|
ch:
|
||||||
|
tags:
|
||||||
|
mk-index:
|
||||||
|
new: "チャンネルを作成"
|
||||||
|
channel-title: "チャンネルのタイトル"
|
||||||
|
|
||||||
|
mk-channel-form:
|
||||||
|
textarea: "書いて"
|
||||||
|
upload: "アップロード"
|
||||||
|
drive: "ドライブ"
|
||||||
|
post: "やる"
|
||||||
|
posting: "やってます"
|
||||||
|
|
||||||
desktop:
|
desktop:
|
||||||
tags:
|
tags:
|
||||||
mk-api-info:
|
mk-api-info:
|
||||||
|
@ -241,6 +254,7 @@ desktop:
|
||||||
mk-ui-header-nav:
|
mk-ui-header-nav:
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
messaging: "メッセージ"
|
messaging: "メッセージ"
|
||||||
|
ch: "チャンネル"
|
||||||
info: "お知らせ"
|
info: "お知らせ"
|
||||||
|
|
||||||
mk-ui-header-search:
|
mk-ui-header-search:
|
||||||
|
@ -353,6 +367,9 @@ desktop:
|
||||||
|
|
||||||
mobile:
|
mobile:
|
||||||
tags:
|
tags:
|
||||||
|
mk-selectdrive-page:
|
||||||
|
select-file: "ファイルを選択"
|
||||||
|
|
||||||
mk-drive-file-viewer:
|
mk-drive-file-viewer:
|
||||||
download: "ダウンロード"
|
download: "ダウンロード"
|
||||||
rename: "名前を変更"
|
rename: "名前を変更"
|
||||||
|
@ -389,6 +406,7 @@ mobile:
|
||||||
|
|
||||||
mk-notifications-page:
|
mk-notifications-page:
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
|
read-all: "すべての通知を既読にしますか?"
|
||||||
|
|
||||||
mk-post-page:
|
mk-post-page:
|
||||||
title: "投稿"
|
title: "投稿"
|
||||||
|
@ -490,6 +508,7 @@ mobile:
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
messaging: "メッセージ"
|
messaging: "メッセージ"
|
||||||
|
ch: "チャンネル"
|
||||||
search: "検索"
|
search: "検索"
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "0.0.2747",
|
"version": "0.0.2807",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "A miniblog-based SNS",
|
"description": "A miniblog-based SNS",
|
||||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||||
|
|
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,
|
withCredential: true,
|
||||||
kind: 'notification-read'
|
kind: 'notification-read'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'notifications/get_unread_count',
|
||||||
|
withCredential: true,
|
||||||
|
kind: 'notification-read'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'notifications/delete',
|
name: 'notifications/delete',
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
|
@ -205,11 +210,6 @@ const endpoints: Endpoint[] = [
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
kind: 'notification-write'
|
kind: 'notification-write'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'notifications/mark_as_read',
|
|
||||||
withCredential: true,
|
|
||||||
kind: 'notification-write'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'notifications/mark_as_read_all',
|
name: 'notifications/mark_as_read_all',
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
|
@ -474,8 +474,33 @@ const endpoints: Endpoint[] = [
|
||||||
name: 'messaging/messages/create',
|
name: 'messaging/messages/create',
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
kind: 'messaging-write'
|
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;
|
export default endpoints;
|
||||||
|
|
|
@ -19,7 +19,7 @@ module.exports = params => new Promise(async (res, rej) => {
|
||||||
.aggregate([
|
.aggregate([
|
||||||
{ $project: {
|
{ $project: {
|
||||||
repost_id: '$repost_id',
|
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
|
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||||
}},
|
}},
|
||||||
{ $project: {
|
{ $project: {
|
||||||
|
@ -34,7 +34,7 @@ module.exports = params => new Promise(async (res, rej) => {
|
||||||
then: 'repost',
|
then: 'repost',
|
||||||
else: {
|
else: {
|
||||||
$cond: {
|
$cond: {
|
||||||
if: { $ne: ['$reply_to_id', null] },
|
if: { $ne: ['$reply_id', null] },
|
||||||
then: 'reply',
|
then: 'reply',
|
||||||
else: 'post'
|
else: 'post'
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
const datas = await Post
|
const datas = await Post
|
||||||
.aggregate([
|
.aggregate([
|
||||||
{ $match: { reply_to: post._id } },
|
{ $match: { reply: post._id } },
|
||||||
{ $project: {
|
{ $project: {
|
||||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
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 } },
|
{ $match: { user_id: user._id } },
|
||||||
{ $project: {
|
{ $project: {
|
||||||
repost_id: '$repost_id',
|
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
|
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||||
}},
|
}},
|
||||||
{ $project: {
|
{ $project: {
|
||||||
|
@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||||
then: 'repost',
|
then: 'repost',
|
||||||
else: {
|
else: {
|
||||||
$cond: {
|
$cond: {
|
||||||
if: { $ne: ['$reply_to_id', null] },
|
if: { $ne: ['$reply_id', null] },
|
||||||
then: 'reply',
|
then: 'reply',
|
||||||
else: 'post'
|
else: 'post'
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||||
{ $match: { user_id: user._id } },
|
{ $match: { user_id: user._id } },
|
||||||
{ $project: {
|
{ $project: {
|
||||||
repost_id: '$repost_id',
|
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
|
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||||
}},
|
}},
|
||||||
{ $project: {
|
{ $project: {
|
||||||
|
@ -49,7 +49,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||||
then: 'repost',
|
then: 'repost',
|
||||||
else: {
|
else: {
|
||||||
$cond: {
|
$cond: {
|
||||||
if: { $ne: ['$reply_to_id', null] },
|
if: { $ne: ['$reply_id', null] },
|
||||||
then: 'reply',
|
then: 'reply',
|
||||||
else: 'post'
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,6 +5,7 @@ import $ from 'cafy';
|
||||||
import Notification from '../../models/notification';
|
import Notification from '../../models/notification';
|
||||||
import serialize from '../../serializers/notification';
|
import serialize from '../../serializers/notification';
|
||||||
import getFriends from '../../common/get-friends';
|
import getFriends from '../../common/get-friends';
|
||||||
|
import read from '../../common/read-notification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get notifications
|
* Get notifications
|
||||||
|
@ -91,17 +92,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
// Mark as read all
|
// Mark as read all
|
||||||
if (notifications.length > 0 && markAsRead) {
|
if (notifications.length > 0 && markAsRead) {
|
||||||
const ids = notifications
|
read(user._id, 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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
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) {
|
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) {
|
if (repost != undefined) {
|
||||||
|
|
|
@ -49,13 +49,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p.reply_to_id) {
|
if (p.reply_id) {
|
||||||
await get(p.reply_to_id);
|
await get(p.reply_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post.reply_to_id) {
|
if (post.reply_id) {
|
||||||
await get(post.reply_to_id);
|
await get(post.reply_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
|
|
|
@ -4,16 +4,17 @@
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import deepEqual = require('deep-equal');
|
import deepEqual = require('deep-equal');
|
||||||
import parse from '../../common/text';
|
import parse from '../../common/text';
|
||||||
import Post from '../../models/post';
|
import { default as Post, IPost, isValidText } from '../../models/post';
|
||||||
import { isValidText } from '../../models/post';
|
|
||||||
import { default as User, IUser } from '../../models/user';
|
import { default as User, IUser } from '../../models/user';
|
||||||
|
import { default as Channel, IChannel } from '../../models/channel';
|
||||||
import Following from '../../models/following';
|
import Following from '../../models/following';
|
||||||
import DriveFile from '../../models/drive-file';
|
import DriveFile from '../../models/drive-file';
|
||||||
import Watching from '../../models/post-watching';
|
import Watching from '../../models/post-watching';
|
||||||
|
import ChannelWatching from '../../models/channel-watching';
|
||||||
import serialize from '../../serializers/post';
|
import serialize from '../../serializers/post';
|
||||||
import notify from '../../common/notify';
|
import notify from '../../common/notify';
|
||||||
import watch from '../../common/watch-post';
|
import watch from '../../common/watch-post';
|
||||||
import event from '../../event';
|
import { default as event, publishChannelStream } from '../../event';
|
||||||
import config from '../../../conf';
|
import config from '../../../conf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +63,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
|
const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
|
||||||
if (repostIdErr) return rej('invalid repost_id');
|
if (repostIdErr) return rej('invalid repost_id');
|
||||||
|
|
||||||
let repost = null;
|
let repost: IPost = null;
|
||||||
|
let isQuote = false;
|
||||||
if (repostId !== undefined) {
|
if (repostId !== undefined) {
|
||||||
// Fetch repost to post
|
// Fetch repost to post
|
||||||
repost = await Post.findOne({
|
repost = await Post.findOne({
|
||||||
|
@ -84,43 +86,86 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
isQuote = text != null || files != null;
|
||||||
|
|
||||||
// 直近と同じRepost対象かつ引用じゃなかったらエラー
|
// 直近と同じRepost対象かつ引用じゃなかったらエラー
|
||||||
if (latestPost &&
|
if (latestPost &&
|
||||||
latestPost.repost_id &&
|
latestPost.repost_id &&
|
||||||
latestPost.repost_id.equals(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');
|
return rej('cannot repost same post that already reposted in your latest post');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直近がRepost対象かつ引用じゃなかったらエラー
|
// 直近がRepost対象かつ引用じゃなかったらエラー
|
||||||
if (latestPost &&
|
if (latestPost &&
|
||||||
latestPost._id.equals(repost._id) &&
|
latestPost._id.equals(repost._id) &&
|
||||||
text === undefined && files === null) {
|
!isQuote) {
|
||||||
return rej('cannot repost your latest post');
|
return rej('cannot repost your latest post');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 'in_reply_to_post_id' parameter
|
// Get 'reply_id' parameter
|
||||||
const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$;
|
const [replyId, replyIdErr] = $(params.reply_id).optional.id().$;
|
||||||
if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id');
|
if (replyIdErr) return rej('invalid reply_id');
|
||||||
|
|
||||||
let inReplyToPost = null;
|
let reply: IPost = null;
|
||||||
if (inReplyToPostId !== undefined) {
|
if (replyId !== undefined) {
|
||||||
// Fetch reply
|
// Fetch reply
|
||||||
inReplyToPost = await Post.findOne({
|
reply = await Post.findOne({
|
||||||
_id: inReplyToPostId
|
_id: replyId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inReplyToPost === null) {
|
if (reply === null) {
|
||||||
return rej('in reply to post is not found');
|
return rej('in reply to post is not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返信対象が引用でないRepostだったらエラー
|
// 返信対象が引用でない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');
|
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
|
// Get 'poll' parameter
|
||||||
const [poll, pollErr] = $(params.poll).optional.strict.object()
|
const [poll, pollErr] = $(params.poll).optional.strict.object()
|
||||||
.have('choices', $().array('string')
|
.have('choices', $().array('string')
|
||||||
|
@ -148,15 +193,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
if (user.latest_post) {
|
if (user.latest_post) {
|
||||||
if (deepEqual({
|
if (deepEqual({
|
||||||
text: user.latest_post.text,
|
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,
|
repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null,
|
||||||
media_ids: (user.latest_post.media_ids || []).map(id => id.toString())
|
media_ids: (user.latest_post.media_ids || []).map(id => id.toString())
|
||||||
}, {
|
}, {
|
||||||
text: text,
|
text: text,
|
||||||
reply: inReplyToPost ? inReplyToPost._id.toString() : null,
|
reply: reply ? reply._id.toString() : null,
|
||||||
repost: repost ? repost._id.toString() : null,
|
repost: repost ? repost._id.toString() : null,
|
||||||
media_ids: (files || []).map(file => file._id.toString())
|
media_ids: (files || []).map(file => file._id.toString())
|
||||||
})) {
|
})) {
|
||||||
return rej('duplicate');
|
return rej('duplicate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,8 +209,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
// 投稿を作成
|
// 投稿を作成
|
||||||
const post = await Post.insert({
|
const post = await Post.insert({
|
||||||
created_at: new Date(),
|
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,
|
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,
|
repost_id: repost ? repost._id : undefined,
|
||||||
poll: poll,
|
poll: poll,
|
||||||
text: text,
|
text: text,
|
||||||
|
@ -179,8 +226,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
// Reponse
|
// Reponse
|
||||||
res(postObj);
|
res(postObj);
|
||||||
|
|
||||||
// -----------------------------------------------------------
|
//#region Post processes
|
||||||
// Post processes
|
|
||||||
|
|
||||||
User.update({ _id: user._id }, {
|
User.update({ _id: user._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
|
@ -203,23 +249,51 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish event to myself's stream
|
// タイムラインへの投稿
|
||||||
event(user._id, 'post', postObj);
|
if (!channel) {
|
||||||
|
// Publish event to myself's stream
|
||||||
|
event(user._id, 'post', postObj);
|
||||||
|
|
||||||
// Fetch all followers
|
// Fetch all followers
|
||||||
const followers = await Following
|
const followers = await Following
|
||||||
.find({
|
.find({
|
||||||
followee_id: user._id,
|
followee_id: user._id,
|
||||||
// 削除されたドキュメントは除く
|
// 削除されたドキュメントは除く
|
||||||
deleted_at: { $exists: false }
|
deleted_at: { $exists: false }
|
||||||
}, {
|
}, {
|
||||||
follower_id: true,
|
follower_id: true,
|
||||||
_id: false
|
_id: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 followers stream
|
// Publish event to channel
|
||||||
followers.forEach(following =>
|
publishChannelStream(channel._id, 'post', postObj);
|
||||||
event(following.follower_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
|
// Increment my posts count
|
||||||
User.update({ _id: user._id }, {
|
User.update({ _id: user._id }, {
|
||||||
|
@ -229,23 +303,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// If has in reply to post
|
// If has in reply to post
|
||||||
if (inReplyToPost) {
|
if (reply) {
|
||||||
// Increment replies count
|
// Increment replies count
|
||||||
Post.update({ _id: inReplyToPost._id }, {
|
Post.update({ _id: reply._id }, {
|
||||||
$inc: {
|
$inc: {
|
||||||
replies_count: 1
|
replies_count: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自分自身へのリプライでない限りは通知を作成
|
// 自分自身へのリプライでない限りは通知を作成
|
||||||
notify(inReplyToPost.user_id, user._id, 'reply', {
|
notify(reply.user_id, user._id, 'reply', {
|
||||||
post_id: post._id
|
post_id: post._id
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch watchers
|
// Fetch watchers
|
||||||
Watching
|
Watching
|
||||||
.find({
|
.find({
|
||||||
post_id: inReplyToPost._id,
|
post_id: reply._id,
|
||||||
user_id: { $ne: user._id },
|
user_id: { $ne: user._id },
|
||||||
// 削除されたドキュメントは除く
|
// 削除されたドキュメントは除く
|
||||||
deleted_at: { $exists: false }
|
deleted_at: { $exists: false }
|
||||||
|
@ -265,10 +339,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
// この投稿をWatchする
|
// この投稿をWatchする
|
||||||
// TODO: ユーザーが「返信したときに自動でWatchする」設定を
|
// TODO: ユーザーが「返信したときに自動でWatchする」設定を
|
||||||
// オフにしていた場合はしない
|
// オフにしていた場合はしない
|
||||||
watch(user._id, inReplyToPost);
|
watch(user._id, reply);
|
||||||
|
|
||||||
// Add mention
|
// Add mention
|
||||||
addMention(inReplyToPost.user_id, 'reply');
|
addMention(reply.user_id, 'reply');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it is repost
|
// If it is repost
|
||||||
|
@ -369,7 +443,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
if (mentionee == null) return;
|
if (mentionee == null) return;
|
||||||
|
|
||||||
// 既に言及されたユーザーに対する返信や引用repostの場合も無視
|
// 既に言及されたユーザーに対する返信や引用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;
|
if (repost && repost.user_id.equals(mentionee._id)) return;
|
||||||
|
|
||||||
// Add mention
|
// Add mention
|
||||||
|
@ -406,4 +480,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
|
// Issue query
|
||||||
const replies = await Post
|
const replies = await Post
|
||||||
.find({ reply_to_id: post._id }, {
|
.find({ reply_id: post._id }, {
|
||||||
limit: limit,
|
limit: limit,
|
||||||
skip: offset,
|
skip: offset,
|
||||||
sort: {
|
sort: {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import Post from '../../models/post';
|
import Post from '../../models/post';
|
||||||
|
import ChannelWatching from '../../models/channel-watching';
|
||||||
import getFriends from '../../common/get-friends';
|
import getFriends from '../../common/get-friends';
|
||||||
import serialize from '../../serializers/post';
|
import serialize from '../../serializers/post';
|
||||||
|
|
||||||
|
@ -32,18 +33,43 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
return rej('cannot set since_id and max_id');
|
return rej('cannot set since_id and max_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID list of the user $self and other users who the user follows
|
// ID list of the user itself and other users who the user follows
|
||||||
const followingIds = await getFriends(user._id);
|
const followingIds = await getFriends(user._id);
|
||||||
|
|
||||||
// Construct query
|
// Watchしているチャンネルを取得
|
||||||
|
const watches = await ChannelWatching.find({
|
||||||
|
user_id: user._id,
|
||||||
|
// 削除されたドキュメントは除く
|
||||||
|
deleted_at: { $exists: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
//#region Construct query
|
||||||
const sort = {
|
const sort = {
|
||||||
_id: -1
|
_id: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
user_id: {
|
$or: [{
|
||||||
$in: followingIds
|
// フォローしている人のタイムラインへの投稿
|
||||||
}
|
user_id: {
|
||||||
|
$in: followingIds
|
||||||
|
},
|
||||||
|
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
|
||||||
|
$or: [{
|
||||||
|
channel_id: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
channel_id: null
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
// Watchしているチャンネルへの投稿
|
||||||
|
channel_id: {
|
||||||
|
$in: watches.map(w => w.channel_id)
|
||||||
|
}
|
||||||
|
}]
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
if (sinceId) {
|
if (sinceId) {
|
||||||
sort._id = 1;
|
sort._id = 1;
|
||||||
query._id = {
|
query._id = {
|
||||||
|
@ -54,6 +80,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
$lt: maxId
|
$lt: maxId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// Issue query
|
// Issue query
|
||||||
const timeline = await Post
|
const timeline = await Post
|
||||||
|
|
|
@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
if (reply != undefined) {
|
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) {
|
if (repost != undefined) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
// Fetch recent posts
|
// Fetch recent posts
|
||||||
const recentPosts = await Post.find({
|
const recentPosts = await Post.find({
|
||||||
user_id: user._id,
|
user_id: user._id,
|
||||||
reply_to_id: {
|
reply_id: {
|
||||||
$exists: true,
|
$exists: true,
|
||||||
$ne: null
|
$ne: null
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
fields: {
|
fields: {
|
||||||
_id: false,
|
_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({
|
const replyTargetPosts = await Post.find({
|
||||||
_id: {
|
_id: {
|
||||||
$in: recentPosts.map(p => p.reply_to_id)
|
$in: recentPosts.map(p => p.reply_id)
|
||||||
},
|
},
|
||||||
user_id: {
|
user_id: {
|
||||||
$ne: user._id
|
$ne: user._id
|
||||||
|
|
|
@ -85,7 +85,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!includeReplies) {
|
if (!includeReplies) {
|
||||||
query.reply_to_id = null;
|
query.reply_id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withMedia) {
|
if (withMedia) {
|
||||||
|
|
|
@ -25,6 +25,10 @@ class MisskeyEvent {
|
||||||
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
|
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 {
|
private publish(channel: string, type: string, value?: any): void {
|
||||||
const message = value == null ?
|
const message = value == null ?
|
||||||
{ type: type } :
|
{ type: type } :
|
||||||
|
@ -41,3 +45,5 @@ export default ev.publishUserStream.bind(ev);
|
||||||
export const publishPostStream = ev.publishPostStream.bind(ev);
|
export const publishPostStream = ev.publishPostStream.bind(ev);
|
||||||
|
|
||||||
export const publishMessagingStream = ev.publishMessagingStream.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,3 +1,8 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
import db from '../../db/mongodb';
|
import db from '../../db/mongodb';
|
||||||
|
|
||||||
export default db.get('notifications') as any; // fuck type definition
|
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 = {
|
export type IPost = {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
|
channel_id: mongo.ObjectID;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
media_ids: mongo.ObjectID[];
|
media_ids: mongo.ObjectID[];
|
||||||
reply_to_id: mongo.ObjectID;
|
reply_id: mongo.ObjectID;
|
||||||
repost_id: mongo.ObjectID;
|
repost_id: mongo.ObjectID;
|
||||||
poll: {}; // todo
|
poll: {}; // todo
|
||||||
text: string;
|
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);
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ import Reaction from '../models/post-reaction';
|
||||||
import { IUser } from '../models/user';
|
import { IUser } from '../models/user';
|
||||||
import Vote from '../models/poll-vote';
|
import Vote from '../models/poll-vote';
|
||||||
import serializeApp from './app';
|
import serializeApp from './app';
|
||||||
|
import serializeChannel from './channel';
|
||||||
import serializeUser from './user';
|
import serializeUser from './user';
|
||||||
import serializeDriveFile from './drive-file';
|
import serializeDriveFile from './drive-file';
|
||||||
import parse from '../common/text';
|
import parse from '../common/text';
|
||||||
|
@ -76,8 +77,13 @@ const self = (
|
||||||
_post.app = await serializeApp(_post.app_id);
|
_post.app = await serializeApp(_post.app_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate channel
|
||||||
|
if (_post.channel_id) {
|
||||||
|
_post.channel = await serializeChannel(_post.channel_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate media
|
||||||
if (_post.media_ids) {
|
if (_post.media_ids) {
|
||||||
// Populate media
|
|
||||||
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
||||||
await serializeDriveFile(fileId)
|
await serializeDriveFile(fileId)
|
||||||
));
|
));
|
||||||
|
@ -117,9 +123,9 @@ const self = (
|
||||||
});
|
});
|
||||||
_post.next = next ? next._id : null;
|
_post.next = next ? next._id : null;
|
||||||
|
|
||||||
if (_post.reply_to_id) {
|
if (_post.reply_id) {
|
||||||
// Populate reply to post
|
// Populate reply to post
|
||||||
_post.reply_to = await self(_post.reply_to_id, meId, {
|
_post.reply = await self(_post.reply_id, meId, {
|
||||||
detail: false
|
detail: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
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 User from '../models/user';
|
||||||
import serializePost from '../serializers/post';
|
import serializePost from '../serializers/post';
|
||||||
|
import readNotification from '../common/read-notification';
|
||||||
|
|
||||||
const log = debug('misskey');
|
const log = debug('misskey');
|
||||||
|
|
||||||
|
@ -45,6 +46,11 @@ export default function homeStream(request: websocket.request, connection: webso
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'read_notification':
|
||||||
|
if (!msg.id) return;
|
||||||
|
readNotification(user._id, msg.id);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'capture':
|
case 'capture':
|
||||||
if (!msg.id) return;
|
if (!msg.id) return;
|
||||||
const postId = msg.id;
|
const postId = msg.id;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import isNativeToken from './common/is-native-token';
|
||||||
import homeStream from './stream/home';
|
import homeStream from './stream/home';
|
||||||
import messagingStream from './stream/messaging';
|
import messagingStream from './stream/messaging';
|
||||||
import serverStream from './stream/server';
|
import serverStream from './stream/server';
|
||||||
|
import channelStream from './stream/channel';
|
||||||
|
|
||||||
module.exports = (server: http.Server) => {
|
module.exports = (server: http.Server) => {
|
||||||
/**
|
/**
|
||||||
|
@ -26,14 +27,6 @@ module.exports = (server: http.Server) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await authenticate(request.resourceURL.query.i);
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
connection.send('authentication-failed');
|
|
||||||
connection.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to Redis
|
// Connect to Redis
|
||||||
const subscriber = redis.createClient(
|
const subscriber = redis.createClient(
|
||||||
config.redis.port, config.redis.host);
|
config.redis.port, config.redis.host);
|
||||||
|
@ -43,6 +36,19 @@ module.exports = (server: http.Server) => {
|
||||||
subscriber.quit();
|
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 =
|
const channel =
|
||||||
request.resourceURL.pathname === '/' ? homeStream :
|
request.resourceURL.pathname === '/' ? homeStream :
|
||||||
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
||||||
|
|
|
@ -3,7 +3,13 @@
|
||||||
* @param {*} post 投稿
|
* @param {*} post 投稿
|
||||||
*/
|
*/
|
||||||
const summarize = (post: any): string => {
|
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) {
|
if (post.media) {
|
||||||
|
@ -16,9 +22,9 @@ const summarize = (post: any): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返信のとき
|
// 返信のとき
|
||||||
if (post.reply_to_id) {
|
if (post.reply_id) {
|
||||||
if (post.reply_to) {
|
if (post.reply) {
|
||||||
summary += ` RE: ${summarize(post.reply_to)}`;
|
summary += ` RE: ${summarize(post.reply)}`;
|
||||||
} else {
|
} else {
|
||||||
summary += ' RE: ...';
|
summary += ' RE: ...';
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ type Mixin = {
|
||||||
api_url: string;
|
api_url: string;
|
||||||
auth_url: string;
|
auth_url: string;
|
||||||
about_url: string;
|
about_url: string;
|
||||||
|
ch_url: stirng;
|
||||||
stats_url: string;
|
stats_url: string;
|
||||||
status_url: string;
|
status_url: string;
|
||||||
dev_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.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
|
||||||
mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
|
mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
|
||||||
mixin.auth_url = `${mixin.scheme}://auth.${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.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
|
||||||
mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
|
mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
|
||||||
mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
|
mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
|
||||||
|
|
|
@ -52,11 +52,11 @@ block content
|
||||||
td Number
|
td Number
|
||||||
td 返信数
|
td 返信数
|
||||||
tr.optional
|
tr.optional
|
||||||
td reply_to
|
td reply
|
||||||
td: a(href='./post', target='_blank') Post
|
td: a(href='./post', target='_blank') Post
|
||||||
td 返信先の投稿
|
td 返信先の投稿
|
||||||
tr.nullable
|
tr.nullable
|
||||||
td reply_to_id
|
td reply_id
|
||||||
td ID
|
td ID
|
||||||
td 返信先の投稿のID
|
td 返信先の投稿のID
|
||||||
tr.optional
|
tr.optional
|
||||||
|
@ -90,7 +90,7 @@ block content
|
||||||
{
|
{
|
||||||
"created_at": "2016-12-10T00:28:50.114Z",
|
"created_at": "2016-12-10T00:28:50.114Z",
|
||||||
"media_ids": null,
|
"media_ids": null,
|
||||||
"reply_to_id": "584a16b15860fc52320137e3",
|
"reply_id": "584a16b15860fc52320137e3",
|
||||||
"repost_id": null,
|
"repost_id": null,
|
||||||
"text": "小日向美穂だぞ!",
|
"text": "小日向美穂だぞ!",
|
||||||
"user_id": "5848bf7764e572683f4402f8",
|
"user_id": "5848bf7764e572683f4402f8",
|
||||||
|
@ -117,10 +117,10 @@ block content
|
||||||
"is_following": true,
|
"is_following": true,
|
||||||
"is_followed": true
|
"is_followed": true
|
||||||
},
|
},
|
||||||
"reply_to": {
|
"reply": {
|
||||||
"created_at": "2016-12-09T02:28:01.563Z",
|
"created_at": "2016-12-09T02:28:01.563Z",
|
||||||
"media_ids": null,
|
"media_ids": null,
|
||||||
"reply_to_id": "5849d35e547e4249be329884",
|
"reply_id": "5849d35e547e4249be329884",
|
||||||
"repost_id": null,
|
"repost_id": null,
|
||||||
"text": "アイコン小日向美穂?",
|
"text": "アイコン小日向美穂?",
|
||||||
"user_id": "57d01a501fdf2d07be417afe",
|
"user_id": "57d01a501fdf2d07be417afe",
|
||||||
|
|
|
@ -5,8 +5,6 @@ json('../../const.json')
|
||||||
$theme-color = themeColor
|
$theme-color = themeColor
|
||||||
$theme-color-foreground = themeColorForeground
|
$theme-color-foreground = themeColorForeground
|
||||||
|
|
||||||
@import './reset'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
::selection
|
::selection
|
||||||
background $theme-color
|
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)
|
tap-highlight-color rgba($theme-color, 0.7)
|
||||||
-webkit-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
|
cursor progress !important
|
||||||
|
|
||||||
|
body
|
||||||
|
overflow-wrap break-word
|
||||||
|
|
||||||
#error
|
#error
|
||||||
padding 32px
|
padding 32px
|
||||||
color #fff
|
color #fff
|
|
@ -1,4 +1,5 @@
|
||||||
@import "../base"
|
@import "../app"
|
||||||
|
@import "../reset"
|
||||||
|
|
||||||
html
|
html
|
||||||
background #eee
|
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 scheme = Url.protocol;
|
||||||
const url = `${scheme}//${host}`;
|
const url = `${scheme}//${host}`;
|
||||||
const apiUrl = `${scheme}//api.${host}`;
|
const apiUrl = `${scheme}//api.${host}`;
|
||||||
|
const chUrl = `${scheme}//ch.${host}`;
|
||||||
const devUrl = `${scheme}//dev.${host}`;
|
const devUrl = `${scheme}//dev.${host}`;
|
||||||
const aboutUrl = `${scheme}//about.${host}`;
|
const aboutUrl = `${scheme}//about.${host}`;
|
||||||
const statsUrl = `${scheme}//stats.${host}`;
|
const statsUrl = `${scheme}//stats.${host}`;
|
||||||
|
@ -16,6 +17,7 @@ export default {
|
||||||
scheme,
|
scheme,
|
||||||
url,
|
url,
|
||||||
apiUrl,
|
apiUrl,
|
||||||
|
chUrl,
|
||||||
devUrl,
|
devUrl,
|
||||||
aboutUrl,
|
aboutUrl,
|
||||||
statsUrl,
|
statsUrl,
|
||||||
|
|
|
@ -7,14 +7,15 @@ const route = require('page');
|
||||||
let page = null;
|
let page = null;
|
||||||
|
|
||||||
export default me => {
|
export default me => {
|
||||||
route('/', index);
|
route('/', index);
|
||||||
route('/i>mentions', mentions);
|
route('/selectdrive', selectDrive);
|
||||||
route('/post::post', post);
|
route('/i>mentions', mentions);
|
||||||
route('/search::query', search);
|
route('/post::post', post);
|
||||||
route('/:user', user.bind(null, 'home'));
|
route('/search::query', search);
|
||||||
route('/:user/graphs', user.bind(null, 'graphs'));
|
route('/:user', user.bind(null, 'home'));
|
||||||
route('/:user/:post', post);
|
route('/:user/graphs', user.bind(null, 'graphs'));
|
||||||
route('*', notFound);
|
route('/:user/:post', post);
|
||||||
|
route('*', notFound);
|
||||||
|
|
||||||
function index() {
|
function index() {
|
||||||
me ? home() : entrance();
|
me ? home() : entrance();
|
||||||
|
@ -54,6 +55,10 @@ export default me => {
|
||||||
mount(el);
|
mount(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectDrive() {
|
||||||
|
mount(document.createElement('mk-selectdrive-page'));
|
||||||
|
}
|
||||||
|
|
||||||
function notFound() {
|
function notFound() {
|
||||||
mount(document.createElement('mk-not-found'));
|
mount(document.createElement('mk-not-found'));
|
||||||
}
|
}
|
||||||
|
@ -67,6 +72,7 @@ export default me => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function mount(content) {
|
function mount(content) {
|
||||||
|
document.documentElement.style.background = '#313a42';
|
||||||
document.documentElement.removeAttribute('data-page');
|
document.documentElement.removeAttribute('data-page');
|
||||||
if (page) page.unmount();
|
if (page) page.unmount();
|
||||||
const body = document.getElementById('app');
|
const body = document.getElementById('app');
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import "../base"
|
@import "../app"
|
||||||
|
@import "../reset"
|
||||||
@import "../../../../node_modules/cropperjs/dist/cropper.css"
|
@import "../../../../node_modules/cropperjs/dist/cropper.css"
|
||||||
|
|
||||||
*::input-placeholder
|
*::input-placeholder
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="feed" if={ !initializing }>
|
<div class="feed" if={ !initializing }>
|
||||||
<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
|
<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
|
||||||
</div>
|
</div>
|
||||||
<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p>
|
<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p>
|
||||||
<style>
|
<style>
|
||||||
:scope
|
:scope
|
||||||
display block
|
display block
|
||||||
|
|
|
@ -61,6 +61,7 @@ require('./pages/user.tag');
|
||||||
require('./pages/post.tag');
|
require('./pages/post.tag');
|
||||||
require('./pages/search.tag');
|
require('./pages/search.tag');
|
||||||
require('./pages/not-found.tag');
|
require('./pages/not-found.tag');
|
||||||
|
require('./pages/selectdrive.tag');
|
||||||
require('./autocomplete-suggestion.tag');
|
require('./autocomplete-suggestion.tag');
|
||||||
require('./progress-dialog.tag');
|
require('./progress-dialog.tag');
|
||||||
require('./user-preview.tag');
|
require('./user-preview.tag');
|
||||||
|
|
|
@ -252,6 +252,12 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onNotification = notification => {
|
this.onNotification = notification => {
|
||||||
|
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||||
|
this.stream.send({
|
||||||
|
type: 'read_notification',
|
||||||
|
id: notification.id
|
||||||
|
});
|
||||||
|
|
||||||
this.notifications.unshift(notification);
|
this.notifications.unshift(notification);
|
||||||
this.update();
|
this.update();
|
||||||
};
|
};
|
||||||
|
|
159
src/web/app/desktop/tags/pages/selectdrive.tag
Normal file
159
src/web/app/desktop/tags/pages/selectdrive.tag
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<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
|
||||||
|
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 => {
|
this.refs.ui.refs.user.on('user-fetched', user => {
|
||||||
Progress.set(0.5);
|
Progress.set(0.5);
|
||||||
document.title = user.name + ' | Misskey'
|
document.title = user.name + ' | Misskey';
|
||||||
});
|
});
|
||||||
|
|
||||||
this.refs.ui.refs.user.on('loaded', () => {
|
this.refs.ui.refs.user.on('loaded', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<mk-post-detail title={ title }>
|
<mk-post-detail title={ title }>
|
||||||
<div class="main">
|
<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-ellipsis-v" if={ !contextFetching }></i>
|
||||||
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -9,8 +9,8 @@
|
||||||
<mk-post-detail-sub post={ post }/>
|
<mk-post-detail-sub post={ post }/>
|
||||||
</virtual>
|
</virtual>
|
||||||
</div>
|
</div>
|
||||||
<div class="reply-to" if={ p.reply_to }>
|
<div class="reply-to" if={ p.reply }>
|
||||||
<mk-post-detail-sub post={ p.reply_to }/>
|
<mk-post-detail-sub post={ p.reply }/>
|
||||||
</div>
|
</div>
|
||||||
<div class="repost" if={ isRepost }>
|
<div class="repost" if={ isRepost }>
|
||||||
<p>
|
<p>
|
||||||
|
@ -329,7 +329,7 @@
|
||||||
|
|
||||||
// Fetch context
|
// Fetch context
|
||||||
this.api('posts/context', {
|
this.api('posts/context', {
|
||||||
post_id: this.p.reply_to_id
|
post_id: this.p.reply_id
|
||||||
}).then(context => {
|
}).then(context => {
|
||||||
this.update({
|
this.update({
|
||||||
contextFetching: false,
|
contextFetching: false,
|
||||||
|
|
|
@ -475,7 +475,7 @@
|
||||||
this.api('posts/create', {
|
this.api('posts/create', {
|
||||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||||
media_ids: files,
|
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,
|
repost_id: this.repost ? this.repost.id : undefined,
|
||||||
poll: this.poll ? this.refs.poll.get() : undefined
|
poll: this.poll ? this.refs.poll.get() : undefined
|
||||||
}).then(data => {
|
}).then(data => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<mk-sub-post-content>
|
<mk-sub-post-content>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<a class="reply" if={ post.reply_to_id }>
|
<a class="reply" if={ post.reply_id }>
|
||||||
<i class="fa fa-reply"></i>
|
<i class="fa fa-reply"></i>
|
||||||
</a>
|
</a>
|
||||||
<span ref="text"></span>
|
<span ref="text"></span>
|
||||||
|
|
|
@ -82,8 +82,8 @@
|
||||||
</mk-timeline>
|
</mk-timeline>
|
||||||
|
|
||||||
<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
|
<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
|
||||||
<div class="reply-to" if={ p.reply_to }>
|
<div class="reply-to" if={ p.reply }>
|
||||||
<mk-timeline-post-sub post={ p.reply_to }/>
|
<mk-timeline-post-sub post={ p.reply }/>
|
||||||
</div>
|
</div>
|
||||||
<div class="repost" if={ isRepost }>
|
<div class="repost" if={ isRepost }>
|
||||||
<p>
|
<p>
|
||||||
|
@ -112,7 +112,8 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="text" ref="text">
|
<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>
|
<i class="fa fa-reply"></i>
|
||||||
</a>
|
</a>
|
||||||
<p class="dummy"></p>
|
<p class="dummy"></p>
|
||||||
|
@ -333,6 +334,9 @@
|
||||||
font-weight 400
|
font-weight 400
|
||||||
font-style normal
|
font-style normal
|
||||||
|
|
||||||
|
> .channel
|
||||||
|
margin 0
|
||||||
|
|
||||||
> .reply
|
> .reply
|
||||||
margin-right 8px
|
margin-right 8px
|
||||||
color #717171
|
color #717171
|
||||||
|
|
|
@ -319,18 +319,26 @@
|
||||||
</mk-ui-header-notifications>
|
</mk-ui-header-notifications>
|
||||||
|
|
||||||
<mk-ui-header-nav>
|
<mk-ui-header-nav>
|
||||||
<ul if={ SIGNIN }>
|
<ul>
|
||||||
<li class="home { active: page == 'home' }">
|
<virtual if={ SIGNIN }>
|
||||||
<a href={ CONFIG.url }>
|
<li class="home { active: page == 'home' }">
|
||||||
<i class="fa fa-home"></i>
|
<a href={ CONFIG.url }>
|
||||||
<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
|
<i class="fa fa-home"></i>
|
||||||
</a>
|
<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
|
||||||
</li>
|
</a>
|
||||||
<li class="messaging">
|
</li>
|
||||||
<a onclick={ messaging }>
|
<li class="messaging">
|
||||||
<i class="fa fa-comments"></i>
|
<a onclick={ messaging }>
|
||||||
<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
|
<i class="fa fa-comments"></i>
|
||||||
<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
|
<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
|
||||||
|
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="info">
|
<li class="info">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import "../base"
|
@import "../app"
|
||||||
|
@import "../reset"
|
||||||
|
|
||||||
html
|
html
|
||||||
background-color #fff
|
background-color #fff
|
||||||
|
|
|
@ -8,6 +8,7 @@ let page = null;
|
||||||
|
|
||||||
export default me => {
|
export default me => {
|
||||||
route('/', index);
|
route('/', index);
|
||||||
|
route('/selectdrive', selectDrive);
|
||||||
route('/i/notifications', notifications);
|
route('/i/notifications', notifications);
|
||||||
route('/i/messaging', messaging);
|
route('/i/messaging', messaging);
|
||||||
route('/i/messaging/:username', messaging);
|
route('/i/messaging/:username', messaging);
|
||||||
|
@ -122,6 +123,10 @@ export default me => {
|
||||||
mount(el);
|
mount(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectDrive() {
|
||||||
|
mount(document.createElement('mk-selectdrive-page'));
|
||||||
|
}
|
||||||
|
|
||||||
function notFound() {
|
function notFound() {
|
||||||
mount(document.createElement('mk-not-found'));
|
mount(document.createElement('mk-not-found'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import "../base"
|
@import "../app"
|
||||||
|
@import "../reset"
|
||||||
|
|
||||||
#wait
|
#wait
|
||||||
top auto
|
top auto
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<mk-drive>
|
<mk-drive>
|
||||||
<nav>
|
<nav ref="nav">
|
||||||
<p onclick={ goRoot }><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive.drive%</p>
|
<p onclick={ goRoot }><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive.drive%</p>
|
||||||
<virtual each={ folder in hierarchyFolders }>
|
<virtual each={ folder in hierarchyFolders }>
|
||||||
<span><i class="fa fa-angle-right"></i></span>
|
<span><i class="fa fa-angle-right"></i></span>
|
||||||
|
@ -56,10 +56,6 @@
|
||||||
display block
|
display block
|
||||||
background #fff
|
background #fff
|
||||||
|
|
||||||
&[data-is-naked]
|
|
||||||
> nav
|
|
||||||
top 48px
|
|
||||||
|
|
||||||
> nav
|
> nav
|
||||||
display block
|
display block
|
||||||
position sticky
|
position sticky
|
||||||
|
@ -205,6 +201,10 @@
|
||||||
} else {
|
} else {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.opts.isNaked) {
|
||||||
|
this.refs.nav.style.top = `${this.opts.top}px`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('unmount', () => {
|
this.on('unmount', () => {
|
||||||
|
@ -483,7 +483,7 @@
|
||||||
if (fn == null || fn == '') return;
|
if (fn == null || fn == '') return;
|
||||||
switch (fn) {
|
switch (fn) {
|
||||||
case '1':
|
case '1':
|
||||||
this.refs.file.click();
|
this.selectLocalFile();
|
||||||
break;
|
break;
|
||||||
case '2':
|
case '2':
|
||||||
this.urlUpload();
|
this.urlUpload();
|
||||||
|
@ -503,6 +503,10 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.selectLocalFile = () => {
|
||||||
|
this.refs.file.click();
|
||||||
|
};
|
||||||
|
|
||||||
this.createFolder = () => {
|
this.createFolder = () => {
|
||||||
const name = window.prompt('フォルダー名');
|
const name = window.prompt('フォルダー名');
|
||||||
if (name == null || name == '') return;
|
if (name == null || name == '') return;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
require('./ui.tag');
|
require('./ui.tag');
|
||||||
require('./ui-header.tag');
|
|
||||||
require('./ui-nav.tag');
|
|
||||||
require('./page/entrance.tag');
|
require('./page/entrance.tag');
|
||||||
require('./page/entrance/signin.tag');
|
require('./page/entrance/signin.tag');
|
||||||
require('./page/entrance/signup.tag');
|
require('./page/entrance/signup.tag');
|
||||||
|
@ -21,6 +19,7 @@ require('./page/settings/authorized-apps.tag');
|
||||||
require('./page/settings/twitter.tag');
|
require('./page/settings/twitter.tag');
|
||||||
require('./page/messaging.tag');
|
require('./page/messaging.tag');
|
||||||
require('./page/messaging-room.tag');
|
require('./page/messaging-room.tag');
|
||||||
|
require('./page/selectdrive.tag');
|
||||||
require('./home.tag');
|
require('./home.tag');
|
||||||
require('./home-timeline.tag');
|
require('./home-timeline.tag');
|
||||||
require('./timeline.tag');
|
require('./timeline.tag');
|
||||||
|
|
|
@ -123,6 +123,12 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onNotification = notification => {
|
this.onNotification = notification => {
|
||||||
|
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||||
|
this.stream.send({
|
||||||
|
type: 'read_notification',
|
||||||
|
id: notification.id
|
||||||
|
});
|
||||||
|
|
||||||
this.notifications.unshift(notification);
|
this.notifications.unshift(notification);
|
||||||
this.update();
|
this.update();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<mk-drive-page>
|
<mk-drive-page>
|
||||||
<mk-ui ref="ui">
|
<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>
|
</mk-ui>
|
||||||
<style>
|
<style>
|
||||||
:scope
|
:scope
|
||||||
|
|
|
@ -10,16 +10,30 @@
|
||||||
import ui from '../../scripts/ui-event';
|
import ui from '../../scripts/ui-event';
|
||||||
import Progress from '../../../common/scripts/loading';
|
import Progress from '../../../common/scripts/loading';
|
||||||
|
|
||||||
|
this.mixin('api');
|
||||||
|
|
||||||
this.on('mount', () => {
|
this.on('mount', () => {
|
||||||
document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%';
|
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%');
|
ui.trigger('title', '<i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-notifications-page.notifications%');
|
||||||
document.documentElement.style.background = '#313a42';
|
document.documentElement.style.background = '#313a42';
|
||||||
|
|
||||||
|
ui.trigger('func', () => {
|
||||||
|
this.readAll();
|
||||||
|
}, 'check');
|
||||||
|
|
||||||
Progress.start();
|
Progress.start();
|
||||||
|
|
||||||
this.refs.ui.refs.notifications.on('fetched', () => {
|
this.refs.ui.refs.notifications.on('fetched', () => {
|
||||||
Progress.done();
|
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>
|
</script>
|
||||||
</mk-notifications-page>
|
</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>
|
<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-ellipsis-v" if={ !contextFetching }></i>
|
||||||
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -8,8 +8,8 @@
|
||||||
<mk-post-detail-sub post={ post }/>
|
<mk-post-detail-sub post={ post }/>
|
||||||
</virtual>
|
</virtual>
|
||||||
</div>
|
</div>
|
||||||
<div class="reply-to" if={ p.reply_to }>
|
<div class="reply-to" if={ p.reply }>
|
||||||
<mk-post-detail-sub post={ p.reply_to }/>
|
<mk-post-detail-sub post={ p.reply }/>
|
||||||
</div>
|
</div>
|
||||||
<div class="repost" if={ isRepost }>
|
<div class="repost" if={ isRepost }>
|
||||||
<p>
|
<p>
|
||||||
|
@ -348,7 +348,7 @@
|
||||||
|
|
||||||
// Fetch context
|
// Fetch context
|
||||||
this.api('posts/context', {
|
this.api('posts/context', {
|
||||||
post_id: this.p.reply_to_id
|
post_id: this.p.reply_id
|
||||||
}).then(context => {
|
}).then(context => {
|
||||||
this.update({
|
this.update({
|
||||||
contextFetching: false,
|
contextFetching: false,
|
||||||
|
|
|
@ -267,7 +267,7 @@
|
||||||
this.api('posts/create', {
|
this.api('posts/create', {
|
||||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||||
media_ids: files,
|
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
|
poll: this.poll ? this.refs.poll.get() : undefined
|
||||||
}).then(data => {
|
}).then(data => {
|
||||||
this.trigger('post');
|
this.trigger('post');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<mk-sub-post-content>
|
<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 }>
|
<details if={ post.media }>
|
||||||
<summary>({ post.media.length }個のメディア)</summary>
|
<summary>({ post.media.length }個のメディア)</summary>
|
||||||
<mk-images-viewer images={ post.media }/>
|
<mk-images-viewer images={ post.media }/>
|
||||||
|
|
|
@ -137,8 +137,8 @@
|
||||||
</mk-timeline>
|
</mk-timeline>
|
||||||
|
|
||||||
<mk-timeline-post class={ repost: isRepost }>
|
<mk-timeline-post class={ repost: isRepost }>
|
||||||
<div class="reply-to" if={ p.reply_to }>
|
<div class="reply-to" if={ p.reply }>
|
||||||
<mk-timeline-post-sub post={ p.reply_to }/>
|
<mk-timeline-post-sub post={ p.reply }/>
|
||||||
</div>
|
</div>
|
||||||
<div class="repost" if={ isRepost }>
|
<div class="repost" if={ isRepost }>
|
||||||
<p>
|
<p>
|
||||||
|
@ -164,7 +164,8 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="text" ref="text">
|
<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>
|
<i class="fa fa-reply"></i>
|
||||||
</a>
|
</a>
|
||||||
<p class="dummy"></p>
|
<p class="dummy"></p>
|
||||||
|
@ -373,6 +374,9 @@
|
||||||
mk-url-preview
|
mk-url-preview
|
||||||
margin-top 8px
|
margin-top 8px
|
||||||
|
|
||||||
|
> .channel
|
||||||
|
margin 0
|
||||||
|
|
||||||
> .reply
|
> .reply
|
||||||
margin-right 8px
|
margin-right 8px
|
||||||
color #717171
|
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 => {
|
this.onStreamNotification = notification => {
|
||||||
|
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||||
|
this.stream.send({
|
||||||
|
type: 'read_notification',
|
||||||
|
id: notification.id
|
||||||
|
});
|
||||||
|
|
||||||
riot.mount(document.body.appendChild(document.createElement('mk-notify')), {
|
riot.mount(document.body.appendChild(document.createElement('mk-notify')), {
|
||||||
notification: notification
|
notification: notification
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
</mk-ui>
|
</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:not([type])
|
||||||
input[type='text']
|
input[type='text']
|
||||||
input[type='password']
|
input[type='password']
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import "../base"
|
@import "../app"
|
||||||
|
@import "../reset"
|
||||||
|
|
||||||
html
|
html
|
||||||
color #456267
|
color #456267
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import "../base"
|
@import "../app"
|
||||||
|
@import "../reset"
|
||||||
|
|
||||||
html
|
html
|
||||||
color #456267
|
color #456267
|
||||||
|
|
12
test/api.js
12
test/api.js
|
@ -277,15 +277,15 @@ describe('API', () => {
|
||||||
const me = await insertSakurako();
|
const me = await insertSakurako();
|
||||||
const post = {
|
const post = {
|
||||||
text: 'さく',
|
text: 'さく',
|
||||||
reply_to_id: himaPost._id.toString()
|
reply_id: himaPost._id.toString()
|
||||||
};
|
};
|
||||||
const res = await request('/posts/create', post, me);
|
const res = await request('/posts/create', post, me);
|
||||||
res.should.have.status(200);
|
res.should.have.status(200);
|
||||||
res.body.should.be.a('object');
|
res.body.should.be.a('object');
|
||||||
res.body.should.have.property('text').eql(post.text);
|
res.body.should.have.property('text').eql(post.text);
|
||||||
res.body.should.have.property('reply_to_id').eql(post.reply_to_id);
|
res.body.should.have.property('reply_id').eql(post.reply_id);
|
||||||
res.body.should.have.property('reply_to');
|
res.body.should.have.property('reply');
|
||||||
res.body.reply_to.should.have.property('text').eql(himaPost.text);
|
res.body.reply.should.have.property('text').eql(himaPost.text);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('repostできる', async(async () => {
|
it('repostできる', async(async () => {
|
||||||
|
@ -350,7 +350,7 @@ describe('API', () => {
|
||||||
const me = await insertSakurako();
|
const me = await insertSakurako();
|
||||||
const post = {
|
const post = {
|
||||||
text: 'さく',
|
text: 'さく',
|
||||||
reply_to_id: '000000000000000000000000'
|
reply_id: '000000000000000000000000'
|
||||||
};
|
};
|
||||||
const res = await request('/posts/create', post, me);
|
const res = await request('/posts/create', post, me);
|
||||||
res.should.have.status(400);
|
res.should.have.status(400);
|
||||||
|
@ -369,7 +369,7 @@ describe('API', () => {
|
||||||
const me = await insertSakurako();
|
const me = await insertSakurako();
|
||||||
const post = {
|
const post = {
|
||||||
text: 'さく',
|
text: 'さく',
|
||||||
reply_to_id: 'kyoppie'
|
reply_id: 'kyoppie'
|
||||||
};
|
};
|
||||||
const res = await request('/posts/create', post, me);
|
const res = await request('/posts/create', post, me);
|
||||||
res.should.have.status(400);
|
res.should.have.status(400);
|
||||||
|
|
5
tools/migration/reply_to-to-reply.js
Normal file
5
tools/migration/reply_to-to-reply.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
db.posts.update({}, {
|
||||||
|
$rename: {
|
||||||
|
reply_to_id: 'reply_id'
|
||||||
|
}
|
||||||
|
}, false, true);
|
|
@ -16,6 +16,7 @@ module.exports = langs.map(([lang, locale]) => {
|
||||||
const entry = {
|
const entry = {
|
||||||
desktop: './src/web/app/desktop/script.js',
|
desktop: './src/web/app/desktop/script.js',
|
||||||
mobile: './src/web/app/mobile/script.js',
|
mobile: './src/web/app/mobile/script.js',
|
||||||
|
ch: './src/web/app/ch/script.js',
|
||||||
stats: './src/web/app/stats/script.js',
|
stats: './src/web/app/stats/script.js',
|
||||||
status: './src/web/app/status/script.js',
|
status: './src/web/app/status/script.js',
|
||||||
dev: './src/web/app/dev/script.js',
|
dev: './src/web/app/dev/script.js',
|
||||||
|
|
Loading…
Reference in a new issue