mirror of
https://github.com/misskey-dev/misskey
synced 2024-12-27 04:59:10 +09:00
Merge branch 'master' of https://github.com/syuilo/misskey
This commit is contained in:
commit
4e83106853
44
CHANGELOG.md
44
CHANGELOG.md
@ -2,6 +2,50 @@ ChangeLog (Release Notes)
|
||||
=========================
|
||||
主に notable な changes を書いていきます
|
||||
|
||||
2807 (2017/11/02)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2805 (2017/11/02)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2801 (2017/11/01)
|
||||
-----------------
|
||||
* チャンネルのWatch実装
|
||||
|
||||
2799 (2017/11/01)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2795 (2017/11/01)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
||||
2793 (2017/11/01)
|
||||
-----------------
|
||||
* なんか
|
||||
|
||||
2783 (2017/11/01)
|
||||
-----------------
|
||||
* なんか
|
||||
|
||||
2777 (2017/11/01)
|
||||
-----------------
|
||||
* 細かいブラッシュアップ
|
||||
|
||||
2775 (2017/11/01)
|
||||
-----------------
|
||||
* Fix: バグ修正
|
||||
|
||||
2769 (2017/11/01)
|
||||
-----------------
|
||||
* New: チャンネルシステム
|
||||
|
||||
2752 (2017/10/30)
|
||||
-----------------
|
||||
* New: 未読の通知がある場合アイコンを表示するように
|
||||
|
||||
2747 (2017/10/25)
|
||||
-----------------
|
||||
* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
|
||||
|
@ -25,6 +25,7 @@ Note that Misskey uses following subdomains:
|
||||
* **api**.*{primary domain}*
|
||||
* **auth**.*{primary domain}*
|
||||
* **about**.*{primary domain}*
|
||||
* **ch**.*{primary domain}*
|
||||
* **stats**.*{primary domain}*
|
||||
* **status**.*{primary domain}*
|
||||
* **dev**.*{primary domain}*
|
||||
|
@ -26,6 +26,7 @@ Misskeyは以下のサブドメインを使います:
|
||||
* **api**.*{primary domain}*
|
||||
* **auth**.*{primary domain}*
|
||||
* **about**.*{primary domain}*
|
||||
* **ch**.*{primary domain}*
|
||||
* **stats**.*{primary domain}*
|
||||
* **status**.*{primary domain}*
|
||||
* **dev**.*{primary domain}*
|
||||
|
@ -164,6 +164,19 @@ common:
|
||||
mk-uploader:
|
||||
waiting: "Waiting"
|
||||
|
||||
ch:
|
||||
tags:
|
||||
mk-index:
|
||||
new: "Create new channel"
|
||||
channel-title: "Channel title"
|
||||
|
||||
mk-channel-form:
|
||||
textarea: "Write here"
|
||||
upload: "Upload"
|
||||
drive: "Drive"
|
||||
post: "Do"
|
||||
posting: "Doing"
|
||||
|
||||
desktop:
|
||||
tags:
|
||||
mk-api-info:
|
||||
@ -241,6 +254,7 @@ desktop:
|
||||
mk-ui-header-nav:
|
||||
home: "Home"
|
||||
messaging: "Messages"
|
||||
ch: "Channels"
|
||||
info: "News"
|
||||
|
||||
mk-ui-header-search:
|
||||
@ -353,6 +367,9 @@ desktop:
|
||||
|
||||
mobile:
|
||||
tags:
|
||||
mk-selectdrive-page:
|
||||
select-file: "Select file(s)"
|
||||
|
||||
mk-drive-file-viewer:
|
||||
download: "Download"
|
||||
rename: "Rename"
|
||||
@ -389,6 +406,7 @@ mobile:
|
||||
|
||||
mk-notifications-page:
|
||||
notifications: "Notifications"
|
||||
read-all: "Are you sure you want to mark all unread notifications as read?"
|
||||
|
||||
mk-post-page:
|
||||
title: "Post"
|
||||
@ -490,6 +508,7 @@ mobile:
|
||||
home: "Home"
|
||||
notifications: "Notifications"
|
||||
messaging: "Messages"
|
||||
ch: "Channels"
|
||||
drive: "Drive"
|
||||
settings: "Settings"
|
||||
about: "About Misskey"
|
||||
|
@ -164,6 +164,19 @@ common:
|
||||
mk-uploader:
|
||||
waiting: "待機中"
|
||||
|
||||
ch:
|
||||
tags:
|
||||
mk-index:
|
||||
new: "チャンネルを作成"
|
||||
channel-title: "チャンネルのタイトル"
|
||||
|
||||
mk-channel-form:
|
||||
textarea: "書いて"
|
||||
upload: "アップロード"
|
||||
drive: "ドライブ"
|
||||
post: "やる"
|
||||
posting: "やってます"
|
||||
|
||||
desktop:
|
||||
tags:
|
||||
mk-api-info:
|
||||
@ -241,6 +254,7 @@ desktop:
|
||||
mk-ui-header-nav:
|
||||
home: "ホーム"
|
||||
messaging: "メッセージ"
|
||||
ch: "チャンネル"
|
||||
info: "お知らせ"
|
||||
|
||||
mk-ui-header-search:
|
||||
@ -353,6 +367,9 @@ desktop:
|
||||
|
||||
mobile:
|
||||
tags:
|
||||
mk-selectdrive-page:
|
||||
select-file: "ファイルを選択"
|
||||
|
||||
mk-drive-file-viewer:
|
||||
download: "ダウンロード"
|
||||
rename: "名前を変更"
|
||||
@ -389,6 +406,7 @@ mobile:
|
||||
|
||||
mk-notifications-page:
|
||||
notifications: "通知"
|
||||
read-all: "すべての通知を既読にしますか?"
|
||||
|
||||
mk-post-page:
|
||||
title: "投稿"
|
||||
@ -490,6 +508,7 @@ mobile:
|
||||
home: "ホーム"
|
||||
notifications: "通知"
|
||||
messaging: "メッセージ"
|
||||
ch: "チャンネル"
|
||||
search: "検索"
|
||||
drive: "ドライブ"
|
||||
settings: "設定"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "0.0.2747",
|
||||
"version": "0.0.2807",
|
||||
"license": "MIT",
|
||||
"description": "A miniblog-based SNS",
|
||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||
|
52
src/api/common/read-notification.ts
Normal file
52
src/api/common/read-notification.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import { default as Notification, INotification } from '../models/notification';
|
||||
import publishUserStream from '../event';
|
||||
|
||||
/**
|
||||
* Mark as read notification(s)
|
||||
*/
|
||||
export default (
|
||||
user: string | mongo.ObjectID,
|
||||
message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[]
|
||||
) => new Promise<any>(async (resolve, reject) => {
|
||||
|
||||
const userId = mongo.ObjectID.prototype.isPrototypeOf(user)
|
||||
? user
|
||||
: new mongo.ObjectID(user);
|
||||
|
||||
const ids: mongo.ObjectID[] = Array.isArray(message)
|
||||
? mongo.ObjectID.prototype.isPrototypeOf(message[0])
|
||||
? (message as mongo.ObjectID[])
|
||||
: typeof message[0] === 'string'
|
||||
? (message as string[]).map(m => new mongo.ObjectID(m))
|
||||
: (message as INotification[]).map(m => m._id)
|
||||
: mongo.ObjectID.prototype.isPrototypeOf(message)
|
||||
? [(message as mongo.ObjectID)]
|
||||
: typeof message === 'string'
|
||||
? [new mongo.ObjectID(message)]
|
||||
: [(message as INotification)._id];
|
||||
|
||||
// Update documents
|
||||
await Notification.update({
|
||||
_id: { $in: ids },
|
||||
is_read: false
|
||||
}, {
|
||||
$set: {
|
||||
is_read: true
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Calc count of my unread notifications
|
||||
const count = await Notification
|
||||
.count({
|
||||
notifiee_id: userId,
|
||||
is_read: false
|
||||
});
|
||||
|
||||
if (count == 0) {
|
||||
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
|
||||
publishUserStream(userId, 'read_all_notifications');
|
||||
}
|
||||
});
|
@ -195,6 +195,11 @@ const endpoints: Endpoint[] = [
|
||||
withCredential: true,
|
||||
kind: 'notification-read'
|
||||
},
|
||||
{
|
||||
name: 'notifications/get_unread_count',
|
||||
withCredential: true,
|
||||
kind: 'notification-read'
|
||||
},
|
||||
{
|
||||
name: 'notifications/delete',
|
||||
withCredential: true,
|
||||
@ -205,11 +210,6 @@ const endpoints: Endpoint[] = [
|
||||
withCredential: true,
|
||||
kind: 'notification-write'
|
||||
},
|
||||
{
|
||||
name: 'notifications/mark_as_read',
|
||||
withCredential: true,
|
||||
kind: 'notification-write'
|
||||
},
|
||||
{
|
||||
name: 'notifications/mark_as_read_all',
|
||||
withCredential: true,
|
||||
@ -474,8 +474,33 @@ const endpoints: Endpoint[] = [
|
||||
name: 'messaging/messages/create',
|
||||
withCredential: true,
|
||||
kind: 'messaging-write'
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
name: 'channels/create',
|
||||
withCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 3,
|
||||
minInterval: ms('10seconds')
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'channels/show'
|
||||
},
|
||||
{
|
||||
name: 'channels/posts'
|
||||
},
|
||||
{
|
||||
name: 'channels/watch',
|
||||
withCredential: true
|
||||
},
|
||||
{
|
||||
name: 'channels/unwatch',
|
||||
withCredential: true
|
||||
},
|
||||
{
|
||||
name: 'channels'
|
||||
},
|
||||
];
|
||||
|
||||
export default endpoints;
|
||||
|
@ -19,7 +19,7 @@ module.exports = params => new Promise(async (res, rej) => {
|
||||
.aggregate([
|
||||
{ $project: {
|
||||
repost_id: '$repost_id',
|
||||
reply_to_id: '$reply_to_id',
|
||||
reply_id: '$reply_id',
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
{ $project: {
|
||||
@ -34,7 +34,7 @@ module.exports = params => new Promise(async (res, rej) => {
|
||||
then: 'repost',
|
||||
else: {
|
||||
$cond: {
|
||||
if: { $ne: ['$reply_to_id', null] },
|
||||
if: { $ne: ['$reply_id', null] },
|
||||
then: 'reply',
|
||||
else: 'post'
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||
|
||||
const datas = await Post
|
||||
.aggregate([
|
||||
{ $match: { reply_to: post._id } },
|
||||
{ $match: { reply: post._id } },
|
||||
{ $project: {
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
|
@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||
{ $match: { user_id: user._id } },
|
||||
{ $project: {
|
||||
repost_id: '$repost_id',
|
||||
reply_to_id: '$reply_to_id',
|
||||
reply_id: '$reply_id',
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
{ $project: {
|
||||
@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||
then: 'repost',
|
||||
else: {
|
||||
$cond: {
|
||||
if: { $ne: ['$reply_to_id', null] },
|
||||
if: { $ne: ['$reply_id', null] },
|
||||
then: 'reply',
|
||||
else: 'post'
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||
{ $match: { user_id: user._id } },
|
||||
{ $project: {
|
||||
repost_id: '$repost_id',
|
||||
reply_to_id: '$reply_to_id',
|
||||
reply_id: '$reply_id',
|
||||
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}},
|
||||
{ $project: {
|
||||
@ -49,7 +49,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||
then: 'repost',
|
||||
else: {
|
||||
$cond: {
|
||||
if: { $ne: ['$reply_to_id', null] },
|
||||
if: { $ne: ['$reply_id', null] },
|
||||
then: 'reply',
|
||||
else: 'post'
|
||||
}
|
||||
|
59
src/api/endpoints/channels.ts
Normal file
59
src/api/endpoints/channels.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../models/channel';
|
||||
import serialize from '../serializers/channel';
|
||||
|
||||
/**
|
||||
* Get all channels
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} me
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
// Get 'since_id' parameter
|
||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||
if (sinceIdErr) return rej('invalid since_id param');
|
||||
|
||||
// Get 'max_id' parameter
|
||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||
if (maxIdErr) return rej('invalid max_id param');
|
||||
|
||||
// Check if both of since_id and max_id is specified
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
}
|
||||
|
||||
// Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
const query = {} as any;
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
$gt: sinceId
|
||||
};
|
||||
} else if (maxId) {
|
||||
query._id = {
|
||||
$lt: maxId
|
||||
};
|
||||
}
|
||||
|
||||
// Issue query
|
||||
const channels = await Channel
|
||||
.find(query, {
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(channels.map(async channel =>
|
||||
await serialize(channel, me))));
|
||||
});
|
39
src/api/endpoints/channels/create.ts
Normal file
39
src/api/endpoints/channels/create.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../../models/channel';
|
||||
import Watching from '../../models/channel-watching';
|
||||
import serialize from '../../serializers/channel';
|
||||
|
||||
/**
|
||||
* Create a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = async (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'title' parameter
|
||||
const [title, titleErr] = $(params.title).string().range(1, 100).$;
|
||||
if (titleErr) return rej('invalid title param');
|
||||
|
||||
// Create a channel
|
||||
const channel = await Channel.insert({
|
||||
created_at: new Date(),
|
||||
user_id: user._id,
|
||||
title: title,
|
||||
index: 0,
|
||||
watching_count: 1
|
||||
});
|
||||
|
||||
// Response
|
||||
res(await serialize(channel));
|
||||
|
||||
// Create Watching
|
||||
await Watching.insert({
|
||||
created_at: new Date(),
|
||||
user_id: user._id,
|
||||
channel_id: channel._id
|
||||
});
|
||||
});
|
79
src/api/endpoints/channels/posts.ts
Normal file
79
src/api/endpoints/channels/posts.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import { default as Channel, IChannel } from '../../models/channel';
|
||||
import { default as Post, IPost } from '../../models/post';
|
||||
import serialize from '../../serializers/post';
|
||||
|
||||
/**
|
||||
* Show a posts of a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'limit' parameter
|
||||
const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$;
|
||||
if (limitErr) return rej('invalid limit param');
|
||||
|
||||
// Get 'since_id' parameter
|
||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||
if (sinceIdErr) return rej('invalid since_id param');
|
||||
|
||||
// Get 'max_id' parameter
|
||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||
if (maxIdErr) return rej('invalid max_id param');
|
||||
|
||||
// Check if both of since_id and max_id is specified
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
}
|
||||
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
// Fetch channel
|
||||
const channel: IChannel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
|
||||
const query = {
|
||||
channel_id: channel._id
|
||||
} as any;
|
||||
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
$gt: sinceId
|
||||
};
|
||||
} else if (maxId) {
|
||||
query._id = {
|
||||
$lt: maxId
|
||||
};
|
||||
}
|
||||
//#endregion Construct query
|
||||
|
||||
// Issue query
|
||||
const posts = await Post
|
||||
.find(query, {
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(posts.map(async (post) =>
|
||||
await serialize(post, user)
|
||||
)));
|
||||
});
|
31
src/api/endpoints/channels/show.ts
Normal file
31
src/api/endpoints/channels/show.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import { default as Channel, IChannel } from '../../models/channel';
|
||||
import serialize from '../../serializers/channel';
|
||||
|
||||
/**
|
||||
* Show a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
// Fetch channel
|
||||
const channel: IChannel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
|
||||
// Serialize
|
||||
res(await serialize(channel, user));
|
||||
});
|
60
src/api/endpoints/channels/unwatch.ts
Normal file
60
src/api/endpoints/channels/unwatch.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../../models/channel';
|
||||
import Watching from '../../models/channel-watching';
|
||||
|
||||
/**
|
||||
* Unwatch a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
//#region Fetch channel
|
||||
const channel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Check whether not watching
|
||||
const exist = await Watching.findOne({
|
||||
user_id: user._id,
|
||||
channel_id: channel._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
if (exist === null) {
|
||||
return rej('already not watching');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Delete watching
|
||||
await Watching.update({
|
||||
_id: exist._id
|
||||
}, {
|
||||
$set: {
|
||||
deleted_at: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Send response
|
||||
res();
|
||||
|
||||
// Decrement watching count
|
||||
Channel.update(channel._id, {
|
||||
$inc: {
|
||||
watching_count: -1
|
||||
}
|
||||
});
|
||||
});
|
58
src/api/endpoints/channels/watch.ts
Normal file
58
src/api/endpoints/channels/watch.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Channel from '../../models/channel';
|
||||
import Watching from '../../models/channel-watching';
|
||||
|
||||
/**
|
||||
* Watch a channel
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id param');
|
||||
|
||||
//#region Fetch channel
|
||||
const channel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Check whether already watching
|
||||
const exist = await Watching.findOne({
|
||||
user_id: user._id,
|
||||
channel_id: channel._id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
if (exist !== null) {
|
||||
return rej('already watching');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Create Watching
|
||||
await Watching.insert({
|
||||
created_at: new Date(),
|
||||
user_id: user._id,
|
||||
channel_id: channel._id
|
||||
});
|
||||
|
||||
// Send response
|
||||
res();
|
||||
|
||||
// Increment watching count
|
||||
Channel.update(channel._id, {
|
||||
$inc: {
|
||||
watching_count: 1
|
||||
}
|
||||
});
|
||||
});
|
@ -5,6 +5,7 @@ import $ from 'cafy';
|
||||
import Notification from '../../models/notification';
|
||||
import serialize from '../../serializers/notification';
|
||||
import getFriends from '../../common/get-friends';
|
||||
import read from '../../common/read-notification';
|
||||
|
||||
/**
|
||||
* Get notifications
|
||||
@ -91,17 +92,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
|
||||
// Mark as read all
|
||||
if (notifications.length > 0 && markAsRead) {
|
||||
const ids = notifications
|
||||
.filter(x => x.is_read == false)
|
||||
.map(x => x._id);
|
||||
|
||||
// Update documents
|
||||
await Notification.update({
|
||||
_id: { $in: ids }
|
||||
}, {
|
||||
$set: { is_read: true }
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
read(user._id, notifications);
|
||||
}
|
||||
});
|
||||
|
23
src/api/endpoints/notifications/get_unread_count.ts
Normal file
23
src/api/endpoints/notifications/get_unread_count.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import Notification from '../../models/notification';
|
||||
|
||||
/**
|
||||
* Get count of unread notifications
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const count = await Notification
|
||||
.count({
|
||||
notifiee_id: user._id,
|
||||
is_read: false
|
||||
});
|
||||
|
||||
res({
|
||||
count: count
|
||||
});
|
||||
});
|
@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Notification from '../../models/notification';
|
||||
import serialize from '../../serializers/notification';
|
||||
import event from '../../event';
|
||||
|
||||
/**
|
||||
* Mark as read a notification
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
const [notificationId, notificationIdErr] = $(params.notification_id).id().$;
|
||||
if (notificationIdErr) return rej('invalid notification_id param');
|
||||
|
||||
// Get notification
|
||||
const notification = await Notification
|
||||
.findOne({
|
||||
_id: notificationId,
|
||||
i: user._id
|
||||
});
|
||||
|
||||
if (notification === null) {
|
||||
return rej('notification-not-found');
|
||||
}
|
||||
|
||||
// Update
|
||||
notification.is_read = true;
|
||||
Notification.update({ _id: notification._id }, {
|
||||
$set: {
|
||||
is_read: true
|
||||
}
|
||||
});
|
||||
|
||||
// Response
|
||||
res();
|
||||
|
||||
// Serialize
|
||||
const notificationObj = await serialize(notification);
|
||||
|
||||
// Publish read_notification event
|
||||
event(user._id, 'read_notification', notificationObj);
|
||||
});
|
32
src/api/endpoints/notifications/mark_as_read_all.ts
Normal file
32
src/api/endpoints/notifications/mark_as_read_all.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import Notification from '../../models/notification';
|
||||
import event from '../../event';
|
||||
|
||||
/**
|
||||
* Mark as read all notifications
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
// Update documents
|
||||
await Notification.update({
|
||||
notifiee_id: user._id,
|
||||
is_read: false
|
||||
}, {
|
||||
$set: {
|
||||
is_read: true
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Response
|
||||
res();
|
||||
|
||||
// 全ての通知を読みましたよというイベントを発行
|
||||
event(user._id, 'read_all_notifications');
|
||||
});
|
@ -62,7 +62,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||
}
|
||||
|
||||
if (reply != undefined) {
|
||||
query.reply_to_id = reply ? { $exists: true, $ne: null } : null;
|
||||
query.reply_id = reply ? { $exists: true, $ne: null } : null;
|
||||
}
|
||||
|
||||
if (repost != undefined) {
|
||||
|
@ -49,13 +49,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.reply_to_id) {
|
||||
await get(p.reply_to_id);
|
||||
if (p.reply_id) {
|
||||
await get(p.reply_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (post.reply_to_id) {
|
||||
await get(post.reply_to_id);
|
||||
if (post.reply_id) {
|
||||
await get(post.reply_id);
|
||||
}
|
||||
|
||||
// Serialize
|
||||
|
@ -4,16 +4,17 @@
|
||||
import $ from 'cafy';
|
||||
import deepEqual = require('deep-equal');
|
||||
import parse from '../../common/text';
|
||||
import Post from '../../models/post';
|
||||
import { isValidText } from '../../models/post';
|
||||
import { default as Post, IPost, isValidText } from '../../models/post';
|
||||
import { default as User, IUser } from '../../models/user';
|
||||
import { default as Channel, IChannel } from '../../models/channel';
|
||||
import Following from '../../models/following';
|
||||
import DriveFile from '../../models/drive-file';
|
||||
import Watching from '../../models/post-watching';
|
||||
import ChannelWatching from '../../models/channel-watching';
|
||||
import serialize from '../../serializers/post';
|
||||
import notify from '../../common/notify';
|
||||
import watch from '../../common/watch-post';
|
||||
import event from '../../event';
|
||||
import { default as event, publishChannelStream } from '../../event';
|
||||
import config from '../../../conf';
|
||||
|
||||
/**
|
||||
@ -62,7 +63,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
|
||||
if (repostIdErr) return rej('invalid repost_id');
|
||||
|
||||
let repost = null;
|
||||
let repost: IPost = null;
|
||||
let isQuote = false;
|
||||
if (repostId !== undefined) {
|
||||
// Fetch repost to post
|
||||
repost = await Post.findOne({
|
||||
@ -84,43 +86,86 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
}
|
||||
});
|
||||
|
||||
isQuote = text != null || files != null;
|
||||
|
||||
// 直近と同じRepost対象かつ引用じゃなかったらエラー
|
||||
if (latestPost &&
|
||||
latestPost.repost_id &&
|
||||
latestPost.repost_id.equals(repost._id) &&
|
||||
text === undefined && files === null) {
|
||||
!isQuote) {
|
||||
return rej('cannot repost same post that already reposted in your latest post');
|
||||
}
|
||||
|
||||
// 直近がRepost対象かつ引用じゃなかったらエラー
|
||||
if (latestPost &&
|
||||
latestPost._id.equals(repost._id) &&
|
||||
text === undefined && files === null) {
|
||||
!isQuote) {
|
||||
return rej('cannot repost your latest post');
|
||||
}
|
||||
}
|
||||
|
||||
// Get 'in_reply_to_post_id' parameter
|
||||
const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$;
|
||||
if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id');
|
||||
// Get 'reply_id' parameter
|
||||
const [replyId, replyIdErr] = $(params.reply_id).optional.id().$;
|
||||
if (replyIdErr) return rej('invalid reply_id');
|
||||
|
||||
let inReplyToPost = null;
|
||||
if (inReplyToPostId !== undefined) {
|
||||
let reply: IPost = null;
|
||||
if (replyId !== undefined) {
|
||||
// Fetch reply
|
||||
inReplyToPost = await Post.findOne({
|
||||
_id: inReplyToPostId
|
||||
reply = await Post.findOne({
|
||||
_id: replyId
|
||||
});
|
||||
|
||||
if (inReplyToPost === null) {
|
||||
if (reply === null) {
|
||||
return rej('in reply to post is not found');
|
||||
}
|
||||
|
||||
// 返信対象が引用でないRepostだったらエラー
|
||||
if (inReplyToPost.repost_id && !inReplyToPost.text && !inReplyToPost.media_ids) {
|
||||
if (reply.repost_id && !reply.text && !reply.media_ids) {
|
||||
return rej('cannot reply to repost');
|
||||
}
|
||||
}
|
||||
|
||||
// Get 'channel_id' parameter
|
||||
const [channelId, channelIdErr] = $(params.channel_id).optional.id().$;
|
||||
if (channelIdErr) return rej('invalid channel_id');
|
||||
|
||||
let channel: IChannel = null;
|
||||
if (channelId !== undefined) {
|
||||
// Fetch channel
|
||||
channel = await Channel.findOne({
|
||||
_id: channelId
|
||||
});
|
||||
|
||||
if (channel === null) {
|
||||
return rej('channel not found');
|
||||
}
|
||||
|
||||
// 返信対象の投稿がこのチャンネルじゃなかったらダメ
|
||||
if (reply && !channelId.equals(reply.channel_id)) {
|
||||
return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
|
||||
}
|
||||
|
||||
// Repost対象の投稿がこのチャンネルじゃなかったらダメ
|
||||
if (repost && !channelId.equals(repost.channel_id)) {
|
||||
return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません');
|
||||
}
|
||||
|
||||
// 引用ではないRepostはダメ
|
||||
if (repost && !isQuote) {
|
||||
return rej('チャンネル内部では引用ではないRepostをすることはできません');
|
||||
}
|
||||
} else {
|
||||
// 返信対象の投稿がチャンネルへの投稿だったらダメ
|
||||
if (reply && reply.channel_id != null) {
|
||||
return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
|
||||
}
|
||||
|
||||
// Repost対象の投稿がチャンネルへの投稿だったらダメ
|
||||
if (repost && repost.channel_id != null) {
|
||||
return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません');
|
||||
}
|
||||
}
|
||||
|
||||
// Get 'poll' parameter
|
||||
const [poll, pollErr] = $(params.poll).optional.strict.object()
|
||||
.have('choices', $().array('string')
|
||||
@ -148,15 +193,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
if (user.latest_post) {
|
||||
if (deepEqual({
|
||||
text: user.latest_post.text,
|
||||
reply: user.latest_post.reply_to_id ? user.latest_post.reply_to_id.toString() : null,
|
||||
reply: user.latest_post.reply_id ? user.latest_post.reply_id.toString() : null,
|
||||
repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null,
|
||||
media_ids: (user.latest_post.media_ids || []).map(id => id.toString())
|
||||
}, {
|
||||
text: text,
|
||||
reply: inReplyToPost ? inReplyToPost._id.toString() : null,
|
||||
repost: repost ? repost._id.toString() : null,
|
||||
media_ids: (files || []).map(file => file._id.toString())
|
||||
})) {
|
||||
text: text,
|
||||
reply: reply ? reply._id.toString() : null,
|
||||
repost: repost ? repost._id.toString() : null,
|
||||
media_ids: (files || []).map(file => file._id.toString())
|
||||
})) {
|
||||
return rej('duplicate');
|
||||
}
|
||||
}
|
||||
@ -164,8 +209,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
// 投稿を作成
|
||||
const post = await Post.insert({
|
||||
created_at: new Date(),
|
||||
channel_id: channel ? channel._id : undefined,
|
||||
index: channel ? channel.index + 1 : undefined,
|
||||
media_ids: files ? files.map(file => file._id) : undefined,
|
||||
reply_to_id: inReplyToPost ? inReplyToPost._id : undefined,
|
||||
reply_id: reply ? reply._id : undefined,
|
||||
repost_id: repost ? repost._id : undefined,
|
||||
poll: poll,
|
||||
text: text,
|
||||
@ -179,8 +226,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
// Reponse
|
||||
res(postObj);
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// Post processes
|
||||
//#region Post processes
|
||||
|
||||
User.update({ _id: user._id }, {
|
||||
$set: {
|
||||
@ -203,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
|
||||
const followers = await Following
|
||||
.find({
|
||||
followee_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
}, {
|
||||
follower_id: true,
|
||||
_id: false
|
||||
// Fetch all followers
|
||||
const followers = await Following
|
||||
.find({
|
||||
followee_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
}, {
|
||||
follower_id: true,
|
||||
_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
|
||||
followers.forEach(following =>
|
||||
event(following.follower_id, 'post', postObj));
|
||||
// Publish event to channel
|
||||
publishChannelStream(channel._id, 'post', postObj);
|
||||
|
||||
// Get channel watchers
|
||||
const watches = await ChannelWatching.find({
|
||||
channel_id: channel._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
// チャンネルの視聴者(のタイムライン)に配信
|
||||
watches.forEach(w => {
|
||||
event(w.user_id, 'post', postObj);
|
||||
});
|
||||
}
|
||||
|
||||
// Increment my posts count
|
||||
User.update({ _id: user._id }, {
|
||||
@ -229,23 +303,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
});
|
||||
|
||||
// If has in reply to post
|
||||
if (inReplyToPost) {
|
||||
if (reply) {
|
||||
// Increment replies count
|
||||
Post.update({ _id: inReplyToPost._id }, {
|
||||
Post.update({ _id: reply._id }, {
|
||||
$inc: {
|
||||
replies_count: 1
|
||||
}
|
||||
});
|
||||
|
||||
// 自分自身へのリプライでない限りは通知を作成
|
||||
notify(inReplyToPost.user_id, user._id, 'reply', {
|
||||
notify(reply.user_id, user._id, 'reply', {
|
||||
post_id: post._id
|
||||
});
|
||||
|
||||
// Fetch watchers
|
||||
Watching
|
||||
.find({
|
||||
post_id: inReplyToPost._id,
|
||||
post_id: reply._id,
|
||||
user_id: { $ne: user._id },
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
@ -265,10 +339,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
// この投稿をWatchする
|
||||
// TODO: ユーザーが「返信したときに自動でWatchする」設定を
|
||||
// オフにしていた場合はしない
|
||||
watch(user._id, inReplyToPost);
|
||||
watch(user._id, reply);
|
||||
|
||||
// Add mention
|
||||
addMention(inReplyToPost.user_id, 'reply');
|
||||
addMention(reply.user_id, 'reply');
|
||||
}
|
||||
|
||||
// If it is repost
|
||||
@ -369,7 +443,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||
if (mentionee == null) return;
|
||||
|
||||
// 既に言及されたユーザーに対する返信や引用repostの場合も無視
|
||||
if (inReplyToPost && inReplyToPost.user_id.equals(mentionee._id)) return;
|
||||
if (reply && reply.user_id.equals(mentionee._id)) return;
|
||||
if (repost && repost.user_id.equals(mentionee._id)) return;
|
||||
|
||||
// Add mention
|
||||
@ -406,4 +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
|
||||
const replies = await Post
|
||||
.find({ reply_to_id: post._id }, {
|
||||
.find({ reply_id: post._id }, {
|
||||
limit: limit,
|
||||
skip: offset,
|
||||
sort: {
|
||||
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import Post from '../../models/post';
|
||||
import ChannelWatching from '../../models/channel-watching';
|
||||
import getFriends from '../../common/get-friends';
|
||||
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');
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Construct query
|
||||
// Watchしているチャンネルを取得
|
||||
const watches = await ChannelWatching.find({
|
||||
user_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
//#region Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
|
||||
const query = {
|
||||
user_id: {
|
||||
$in: followingIds
|
||||
}
|
||||
$or: [{
|
||||
// フォローしている人のタイムラインへの投稿
|
||||
user_id: {
|
||||
$in: followingIds
|
||||
},
|
||||
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
|
||||
$or: [{
|
||||
channel_id: {
|
||||
$exists: false
|
||||
}
|
||||
}, {
|
||||
channel_id: null
|
||||
}]
|
||||
}, {
|
||||
// Watchしているチャンネルへの投稿
|
||||
channel_id: {
|
||||
$in: watches.map(w => w.channel_id)
|
||||
}
|
||||
}]
|
||||
} as any;
|
||||
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
@ -54,6 +80,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||
$lt: maxId
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Issue query
|
||||
const timeline = await Post
|
||||
|
@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
} as any;
|
||||
|
||||
if (reply != undefined) {
|
||||
query.reply_to_id = reply ? { $exists: true, $ne: null } : null;
|
||||
query.reply_id = reply ? { $exists: true, $ne: null } : null;
|
||||
}
|
||||
|
||||
if (repost != undefined) {
|
||||
|
@ -27,7 +27,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
// Fetch recent posts
|
||||
const recentPosts = await Post.find({
|
||||
user_id: user._id,
|
||||
reply_to_id: {
|
||||
reply_id: {
|
||||
$exists: true,
|
||||
$ne: null
|
||||
}
|
||||
@ -38,7 +38,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
limit: 1000,
|
||||
fields: {
|
||||
_id: false,
|
||||
reply_to_id: true
|
||||
reply_id: true
|
||||
}
|
||||
});
|
||||
|
||||
@ -49,7 +49,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
|
||||
const replyTargetPosts = await Post.find({
|
||||
_id: {
|
||||
$in: recentPosts.map(p => p.reply_to_id)
|
||||
$in: recentPosts.map(p => p.reply_id)
|
||||
},
|
||||
user_id: {
|
||||
$ne: user._id
|
||||
|
@ -85,7 +85,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||
}
|
||||
|
||||
if (!includeReplies) {
|
||||
query.reply_to_id = null;
|
||||
query.reply_id = null;
|
||||
}
|
||||
|
||||
if (withMedia) {
|
||||
|
@ -25,6 +25,10 @@ class MisskeyEvent {
|
||||
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
public publishChannelStream(channelId: ID, type: string, value?: any): void {
|
||||
this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
private publish(channel: string, type: string, value?: any): void {
|
||||
const message = value == null ?
|
||||
{ type: type } :
|
||||
@ -41,3 +45,5 @@ export default ev.publishUserStream.bind(ev);
|
||||
export const publishPostStream = ev.publishPostStream.bind(ev);
|
||||
|
||||
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
|
||||
|
||||
export const publishChannelStream = ev.publishChannelStream.bind(ev);
|
||||
|
3
src/api/models/channel-watching.ts
Normal file
3
src/api/models/channel-watching.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import db from '../../db/mongodb';
|
||||
|
||||
export default db.get('channel_watching') as any; // fuck type definition
|
14
src/api/models/channel.ts
Normal file
14
src/api/models/channel.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import db from '../../db/mongodb';
|
||||
|
||||
const collection = db.get('channels');
|
||||
|
||||
export default collection as any; // fuck type definition
|
||||
|
||||
export type IChannel = {
|
||||
_id: mongo.ObjectID;
|
||||
created_at: Date;
|
||||
title: string;
|
||||
user_id: mongo.ObjectID;
|
||||
index: number;
|
||||
};
|
@ -1,3 +1,8 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import db from '../../db/mongodb';
|
||||
|
||||
export default db.get('notifications') as any; // fuck type definition
|
||||
|
||||
export interface INotification {
|
||||
_id: mongo.ObjectID;
|
||||
}
|
||||
|
@ -10,9 +10,10 @@ export function isValidText(text: string): boolean {
|
||||
|
||||
export type IPost = {
|
||||
_id: mongo.ObjectID;
|
||||
channel_id: mongo.ObjectID;
|
||||
created_at: Date;
|
||||
media_ids: mongo.ObjectID[];
|
||||
reply_to_id: mongo.ObjectID;
|
||||
reply_id: mongo.ObjectID;
|
||||
repost_id: mongo.ObjectID;
|
||||
poll: {}; // todo
|
||||
text: string;
|
||||
|
66
src/api/serializers/channel.ts
Normal file
66
src/api/serializers/channel.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import * as mongo from 'mongodb';
|
||||
import deepcopy = require('deepcopy');
|
||||
import { IUser } from '../models/user';
|
||||
import { default as Channel, IChannel } from '../models/channel';
|
||||
import Watching from '../models/channel-watching';
|
||||
|
||||
/**
|
||||
* Serialize a channel
|
||||
*
|
||||
* @param channel target
|
||||
* @param me? serializee
|
||||
* @return response
|
||||
*/
|
||||
export default (
|
||||
channel: string | mongo.ObjectID | IChannel,
|
||||
me?: string | mongo.ObjectID | IUser
|
||||
) => new Promise<any>(async (resolve, reject) => {
|
||||
|
||||
let _channel: any;
|
||||
|
||||
// Populate the channel if 'channel' is ID
|
||||
if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
|
||||
_channel = await Channel.findOne({
|
||||
_id: channel
|
||||
});
|
||||
} else if (typeof channel === 'string') {
|
||||
_channel = await Channel.findOne({
|
||||
_id: new mongo.ObjectID(channel)
|
||||
});
|
||||
} else {
|
||||
_channel = deepcopy(channel);
|
||||
}
|
||||
|
||||
// Rename _id to id
|
||||
_channel.id = _channel._id;
|
||||
delete _channel._id;
|
||||
|
||||
// Remove needless properties
|
||||
delete _channel.user_id;
|
||||
|
||||
// Me
|
||||
const meId: mongo.ObjectID = me
|
||||
? mongo.ObjectID.prototype.isPrototypeOf(me)
|
||||
? me as mongo.ObjectID
|
||||
: typeof me === 'string'
|
||||
? new mongo.ObjectID(me)
|
||||
: (me as IUser)._id
|
||||
: null;
|
||||
|
||||
if (me) {
|
||||
//#region Watchしているかどうか
|
||||
const watch = await Watching.findOne({
|
||||
user_id: meId,
|
||||
channel_id: _channel.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
_channel.is_watching = watch !== null;
|
||||
//#endregion
|
||||
}
|
||||
|
||||
resolve(_channel);
|
||||
});
|
@ -8,6 +8,7 @@ import Reaction from '../models/post-reaction';
|
||||
import { IUser } from '../models/user';
|
||||
import Vote from '../models/poll-vote';
|
||||
import serializeApp from './app';
|
||||
import serializeChannel from './channel';
|
||||
import serializeUser from './user';
|
||||
import serializeDriveFile from './drive-file';
|
||||
import parse from '../common/text';
|
||||
@ -76,8 +77,13 @@ const self = (
|
||||
_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) {
|
||||
// Populate media
|
||||
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
||||
await serializeDriveFile(fileId)
|
||||
));
|
||||
@ -117,9 +123,9 @@ const self = (
|
||||
});
|
||||
_post.next = next ? next._id : null;
|
||||
|
||||
if (_post.reply_to_id) {
|
||||
if (_post.reply_id) {
|
||||
// Populate reply to post
|
||||
_post.reply_to = await self(_post.reply_to_id, meId, {
|
||||
_post.reply = await self(_post.reply_id, meId, {
|
||||
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 serializePost from '../serializers/post';
|
||||
import readNotification from '../common/read-notification';
|
||||
|
||||
const log = debug('misskey');
|
||||
|
||||
@ -45,6 +46,11 @@ export default function homeStream(request: websocket.request, connection: webso
|
||||
});
|
||||
break;
|
||||
|
||||
case 'read_notification':
|
||||
if (!msg.id) return;
|
||||
readNotification(user._id, msg.id);
|
||||
break;
|
||||
|
||||
case 'capture':
|
||||
if (!msg.id) return;
|
||||
const postId = msg.id;
|
||||
|
@ -9,6 +9,7 @@ import isNativeToken from './common/is-native-token';
|
||||
import homeStream from './stream/home';
|
||||
import messagingStream from './stream/messaging';
|
||||
import serverStream from './stream/server';
|
||||
import channelStream from './stream/channel';
|
||||
|
||||
module.exports = (server: http.Server) => {
|
||||
/**
|
||||
@ -26,14 +27,6 @@ module.exports = (server: http.Server) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await authenticate(request.resourceURL.query.i);
|
||||
|
||||
if (user == null) {
|
||||
connection.send('authentication-failed');
|
||||
connection.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Redis
|
||||
const subscriber = redis.createClient(
|
||||
config.redis.port, config.redis.host);
|
||||
@ -43,6 +36,19 @@ module.exports = (server: http.Server) => {
|
||||
subscriber.quit();
|
||||
});
|
||||
|
||||
if (request.resourceURL.pathname === '/channel') {
|
||||
channelStream(request, connection, subscriber);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await authenticate(request.resourceURL.query.i);
|
||||
|
||||
if (user == null) {
|
||||
connection.send('authentication-failed');
|
||||
connection.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const channel =
|
||||
request.resourceURL.pathname === '/' ? homeStream :
|
||||
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
||||
|
@ -3,7 +3,13 @@
|
||||
* @param {*} post 投稿
|
||||
*/
|
||||
const summarize = (post: any): string => {
|
||||
let summary = post.text ? post.text : '';
|
||||
let summary = '';
|
||||
|
||||
// チャンネル
|
||||
summary += post.channel ? `${post.channel.title}:` : '';
|
||||
|
||||
// 本文
|
||||
summary += post.text ? post.text : '';
|
||||
|
||||
// メディアが添付されているとき
|
||||
if (post.media) {
|
||||
@ -16,9 +22,9 @@ const summarize = (post: any): string => {
|
||||
}
|
||||
|
||||
// 返信のとき
|
||||
if (post.reply_to_id) {
|
||||
if (post.reply_to) {
|
||||
summary += ` RE: ${summarize(post.reply_to)}`;
|
||||
if (post.reply_id) {
|
||||
if (post.reply) {
|
||||
summary += ` RE: ${summarize(post.reply)}`;
|
||||
} else {
|
||||
summary += ' RE: ...';
|
||||
}
|
||||
|
@ -88,6 +88,7 @@ type Mixin = {
|
||||
api_url: string;
|
||||
auth_url: string;
|
||||
about_url: string;
|
||||
ch_url: stirng;
|
||||
stats_url: string;
|
||||
status_url: string;
|
||||
dev_url: string;
|
||||
@ -122,6 +123,7 @@ export default function load() {
|
||||
mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
|
||||
mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
|
||||
mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
|
||||
mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
|
||||
mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
|
||||
mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
|
||||
mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
|
||||
|
@ -52,11 +52,11 @@ block content
|
||||
td Number
|
||||
td 返信数
|
||||
tr.optional
|
||||
td reply_to
|
||||
td reply
|
||||
td: a(href='./post', target='_blank') Post
|
||||
td 返信先の投稿
|
||||
tr.nullable
|
||||
td reply_to_id
|
||||
td reply_id
|
||||
td ID
|
||||
td 返信先の投稿のID
|
||||
tr.optional
|
||||
@ -90,7 +90,7 @@ block content
|
||||
{
|
||||
"created_at": "2016-12-10T00:28:50.114Z",
|
||||
"media_ids": null,
|
||||
"reply_to_id": "584a16b15860fc52320137e3",
|
||||
"reply_id": "584a16b15860fc52320137e3",
|
||||
"repost_id": null,
|
||||
"text": "小日向美穂だぞ!",
|
||||
"user_id": "5848bf7764e572683f4402f8",
|
||||
@ -117,10 +117,10 @@ block content
|
||||
"is_following": true,
|
||||
"is_followed": true
|
||||
},
|
||||
"reply_to": {
|
||||
"reply": {
|
||||
"created_at": "2016-12-09T02:28:01.563Z",
|
||||
"media_ids": null,
|
||||
"reply_to_id": "5849d35e547e4249be329884",
|
||||
"reply_id": "5849d35e547e4249be329884",
|
||||
"repost_id": null,
|
||||
"text": "アイコン小日向美穂?",
|
||||
"user_id": "57d01a501fdf2d07be417afe",
|
||||
|
@ -5,8 +5,6 @@ json('../../const.json')
|
||||
$theme-color = themeColor
|
||||
$theme-color-foreground = themeColorForeground
|
||||
|
||||
@import './reset'
|
||||
|
||||
/*
|
||||
::selection
|
||||
background $theme-color
|
||||
@ -14,6 +12,9 @@ $theme-color-foreground = themeColorForeground
|
||||
*/
|
||||
|
||||
*
|
||||
position relative
|
||||
box-sizing border-box
|
||||
background-clip padding-box !important
|
||||
tap-highlight-color rgba($theme-color, 0.7)
|
||||
-webkit-tap-highlight-color rgba($theme-color, 0.7)
|
||||
|
||||
@ -29,6 +30,9 @@ html
|
||||
&, *
|
||||
cursor progress !important
|
||||
|
||||
body
|
||||
overflow-wrap break-word
|
||||
|
||||
#error
|
||||
padding 32px
|
||||
color #fff
|
@ -1,4 +1,5 @@
|
||||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
background #eee
|
||||
|
32
src/web/app/ch/router.js
Normal file
32
src/web/app/ch/router.js
Normal file
@ -0,0 +1,32 @@
|
||||
import * as riot from 'riot';
|
||||
const route = require('page');
|
||||
let page = null;
|
||||
|
||||
export default me => {
|
||||
route('/', index);
|
||||
route('/:channel', channel);
|
||||
route('*', notFound);
|
||||
|
||||
function index() {
|
||||
mount(document.createElement('mk-index'));
|
||||
}
|
||||
|
||||
function channel(ctx) {
|
||||
const el = document.createElement('mk-channel');
|
||||
el.setAttribute('id', ctx.params.channel);
|
||||
mount(el);
|
||||
}
|
||||
|
||||
function notFound() {
|
||||
mount(document.createElement('mk-not-found'));
|
||||
}
|
||||
|
||||
// EXEC
|
||||
route();
|
||||
};
|
||||
|
||||
function mount(content) {
|
||||
if (page) page.unmount();
|
||||
const body = document.getElementById('app');
|
||||
page = riot.mount(body.appendChild(content))[0];
|
||||
}
|
18
src/web/app/ch/script.js
Normal file
18
src/web/app/ch/script.js
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Channels
|
||||
*/
|
||||
|
||||
// Style
|
||||
import './style.styl';
|
||||
|
||||
require('./tags');
|
||||
import init from '../init';
|
||||
import route from './router';
|
||||
|
||||
/**
|
||||
* init
|
||||
*/
|
||||
init(me => {
|
||||
// Start routing
|
||||
route(me);
|
||||
});
|
10
src/web/app/ch/style.styl
Normal file
10
src/web/app/ch/style.styl
Normal file
@ -0,0 +1,10 @@
|
||||
@import "../app"
|
||||
|
||||
html
|
||||
padding 8px
|
||||
background #efefef
|
||||
|
||||
#wait
|
||||
top auto
|
||||
bottom 15px
|
||||
left 15px
|
403
src/web/app/ch/tags/channel.tag
Normal file
403
src/web/app/ch/tags/channel.tag
Normal file
@ -0,0 +1,403 @@
|
||||
<mk-channel>
|
||||
<mk-header/>
|
||||
<hr>
|
||||
<main if={ !fetching }>
|
||||
<h1>{ channel.title }</h1>
|
||||
|
||||
<div if={ SIGNIN }>
|
||||
<p if={ channel.is_watching }>このチャンネルをウォッチしています <a onclick={ unwatch }>ウォッチ解除</a></p>
|
||||
<p if={ !channel.is_watching }><a onclick={ watch }>このチャンネルをウォッチする</a></p>
|
||||
</div>
|
||||
|
||||
<div class="share">
|
||||
<mk-twitter-button/>
|
||||
<mk-line-button/>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
|
||||
<div if={ !postsFetching }>
|
||||
<p if={ posts == null || posts.length == 0 }>まだ投稿がありません</p>
|
||||
<virtual if={ posts != null }>
|
||||
<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
|
||||
</virtual>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
|
||||
<div if={ !SIGNIN }>
|
||||
<p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p>
|
||||
</div>
|
||||
<hr>
|
||||
<footer>
|
||||
<small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small>
|
||||
</footer>
|
||||
</main>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> main
|
||||
> h1
|
||||
font-size 1.5em
|
||||
color #f00
|
||||
|
||||
> .share
|
||||
> *
|
||||
margin-right 4px
|
||||
|
||||
> .body
|
||||
margin 8px 0 0 0
|
||||
|
||||
> mk-channel-form
|
||||
max-width 500px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import Progress from '../../common/scripts/loading';
|
||||
import ChannelStream from '../../common/scripts/channel-stream';
|
||||
|
||||
this.mixin('i');
|
||||
this.mixin('api');
|
||||
|
||||
this.id = this.opts.id;
|
||||
this.fetching = true;
|
||||
this.postsFetching = true;
|
||||
this.channel = null;
|
||||
this.posts = null;
|
||||
this.connection = new ChannelStream(this.id);
|
||||
this.version = VERSION;
|
||||
this.unreadCount = 0;
|
||||
|
||||
this.on('mount', () => {
|
||||
document.documentElement.style.background = '#efefef';
|
||||
|
||||
Progress.start();
|
||||
|
||||
let fetched = false;
|
||||
|
||||
// チャンネル概要読み込み
|
||||
this.api('channels/show', {
|
||||
channel_id: this.id
|
||||
}).then(channel => {
|
||||
if (fetched) {
|
||||
Progress.done();
|
||||
} else {
|
||||
Progress.set(0.5);
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
this.update({
|
||||
fetching: false,
|
||||
channel: channel
|
||||
});
|
||||
|
||||
document.title = channel.title + ' | Misskey'
|
||||
});
|
||||
|
||||
// 投稿読み込み
|
||||
this.api('channels/posts', {
|
||||
channel_id: this.id
|
||||
}).then(posts => {
|
||||
if (fetched) {
|
||||
Progress.done();
|
||||
} else {
|
||||
Progress.set(0.5);
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
this.update({
|
||||
postsFetching: false,
|
||||
posts: posts
|
||||
});
|
||||
});
|
||||
|
||||
this.connection.on('post', this.onPost);
|
||||
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.connection.off('post', this.onPost);
|
||||
this.connection.close();
|
||||
document.removeEventListener('visibilitychange', this.onVisibilitychange);
|
||||
});
|
||||
|
||||
this.onPost = post => {
|
||||
this.posts.unshift(post);
|
||||
this.update();
|
||||
|
||||
if (document.hidden && this.SIGNIN && post.user_id !== this.I.id) {
|
||||
this.unreadCount++;
|
||||
document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
|
||||
}
|
||||
};
|
||||
|
||||
this.onVisibilitychange = () => {
|
||||
if (!document.hidden) {
|
||||
this.unreadCount = 0;
|
||||
document.title = this.channel.title + ' | Misskey'
|
||||
}
|
||||
};
|
||||
|
||||
this.watch = () => {
|
||||
this.api('channels/watch', {
|
||||
channel_id: this.id
|
||||
}).then(() => {
|
||||
this.channel.is_watching = true;
|
||||
this.update();
|
||||
}, e => {
|
||||
alert('error');
|
||||
});
|
||||
};
|
||||
|
||||
this.unwatch = () => {
|
||||
this.api('channels/unwatch', {
|
||||
channel_id: this.id
|
||||
}).then(() => {
|
||||
this.channel.is_watching = false;
|
||||
this.update();
|
||||
}, e => {
|
||||
alert('error');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-channel>
|
||||
|
||||
<mk-channel-post>
|
||||
<header>
|
||||
<a class="index" onclick={ reply }>{ post.index }:</a>
|
||||
<a class="name" href={ CONFIG.url + '/' + post.user.username }><b>{ post.user.name }</b></a>
|
||||
<mk-time time={ post.created_at }/>
|
||||
<mk-time time={ post.created_at } mode="detail"/>
|
||||
<span>ID:<i>{ post.user.username }</i></span>
|
||||
</header>
|
||||
<div>
|
||||
<a if={ post.reply }>>>{ post.reply.index }</a>
|
||||
{ post.text }
|
||||
<div class="media" if={ post.media }>
|
||||
<virtual each={ file in post.media }>
|
||||
<a href={ file.url } target="_blank">
|
||||
<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
|
||||
</a>
|
||||
</virtual>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
> header
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
z-index 1
|
||||
top 0
|
||||
background rgba(239, 239, 239, 0.9)
|
||||
|
||||
> .index
|
||||
margin-right 0.25em
|
||||
color #000
|
||||
|
||||
> .name
|
||||
margin-right 0.5em
|
||||
color #008000
|
||||
|
||||
> mk-time
|
||||
margin-right 0.5em
|
||||
|
||||
&:first-of-type
|
||||
display none
|
||||
|
||||
@media (max-width 600px)
|
||||
> mk-time
|
||||
&:first-of-type
|
||||
display initial
|
||||
|
||||
&:last-of-type
|
||||
display none
|
||||
|
||||
> div
|
||||
padding 0 0 1em 2em
|
||||
|
||||
> .media
|
||||
> a
|
||||
display inline-block
|
||||
|
||||
> img
|
||||
max-width 100%
|
||||
vertical-align bottom
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.post = this.opts.post;
|
||||
this.form = this.opts.form;
|
||||
|
||||
this.reply = () => {
|
||||
this.form.update({
|
||||
reply: this.post
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-channel-post>
|
||||
|
||||
<mk-channel-form>
|
||||
<p if={ reply }><b>>>{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
|
||||
<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
|
||||
<div class="actions">
|
||||
<button onclick={ selectFile }><i class="fa fa-upload"></i>%i18n:ch.tags.mk-channel-form.upload%</button>
|
||||
<button onclick={ drive }><i class="fa fa-cloud"></i>%i18n:ch.tags.mk-channel-form.drive%</button>
|
||||
<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
|
||||
<i class="fa fa-paper-plane" if={ !wait }></i>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis if={ wait }/>
|
||||
</button>
|
||||
</div>
|
||||
<mk-uploader ref="uploader"/>
|
||||
<ol if={ files }>
|
||||
<li each={ files }>{ name }</li>
|
||||
</ol>
|
||||
<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> textarea
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
min-height 5em
|
||||
|
||||
> .actions
|
||||
display flex
|
||||
|
||||
> button
|
||||
> i
|
||||
margin-right 0.25em
|
||||
|
||||
&:last-child
|
||||
margin-left auto
|
||||
|
||||
&.wait
|
||||
cursor wait
|
||||
|
||||
> input[type='file']
|
||||
display none
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import CONFIG from '../../common/scripts/config';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.channel = this.opts.channel;
|
||||
this.files = null;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.refs.uploader.on('uploaded', file => {
|
||||
this.update({
|
||||
files: [file]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.upload = file => {
|
||||
this.refs.uploader.upload(file);
|
||||
};
|
||||
|
||||
this.clearReply = () => {
|
||||
this.update({
|
||||
reply: null
|
||||
});
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
this.clearReply();
|
||||
this.update({
|
||||
files: null
|
||||
});
|
||||
this.refs.text.value = '';
|
||||
};
|
||||
|
||||
this.post = () => {
|
||||
this.update({
|
||||
wait: true
|
||||
});
|
||||
|
||||
const files = this.files && this.files.length > 0
|
||||
? this.files.map(f => f.id)
|
||||
: undefined;
|
||||
|
||||
this.api('posts/create', {
|
||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||
media_ids: files,
|
||||
reply_id: this.reply ? this.reply.id : undefined,
|
||||
channel_id: this.channel.id
|
||||
}).then(data => {
|
||||
this.clear();
|
||||
}).catch(err => {
|
||||
alert('失敗した');
|
||||
}).then(() => {
|
||||
this.update({
|
||||
wait: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.changeFile = () => {
|
||||
this.refs.file.files.forEach(this.upload);
|
||||
};
|
||||
|
||||
this.selectFile = () => {
|
||||
this.refs.file.click();
|
||||
};
|
||||
|
||||
this.drive = () => {
|
||||
window['cb'] = files => {
|
||||
this.update({
|
||||
files: files
|
||||
});
|
||||
};
|
||||
|
||||
window.open(CONFIG.url + '/selectdrive?multiple=true',
|
||||
'drive_window',
|
||||
'height=500,width=800');
|
||||
};
|
||||
|
||||
this.onkeydown = e => {
|
||||
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
|
||||
};
|
||||
|
||||
this.onpaste = e => {
|
||||
e.clipboardData.items.forEach(item => {
|
||||
if (item.kind == 'file') {
|
||||
this.upload(item.getAsFile());
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-channel-form>
|
||||
|
||||
<mk-twitter-button>
|
||||
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
|
||||
<script>
|
||||
this.on('mount', () => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
|
||||
script.setAttribute('async', 'async');
|
||||
head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</mk-twitter-button>
|
||||
|
||||
<mk-line-button>
|
||||
<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ CONFIG.chUrl } style="display: none;"></div>
|
||||
<script>
|
||||
this.on('mount', () => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://d.line-scdn.net/r/web/social-plugin/js/thirdparty/loader.min.js');
|
||||
script.setAttribute('async', 'async');
|
||||
head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</mk-line-button>
|
20
src/web/app/ch/tags/header.tag
Normal file
20
src/web/app/ch/tags/header.tag
Normal file
@ -0,0 +1,20 @@
|
||||
<mk-header>
|
||||
<div>
|
||||
<a href={ CONFIG.chUrl }>Index</a> | <a href={ CONFIG.url }>Misskey</a>
|
||||
</div>
|
||||
<div>
|
||||
<a if={ !SIGNIN } href={ CONFIG.url }>ログイン(新規登録)</a>
|
||||
<a if={ SIGNIN } href={ CONFIG.url + '/' + I.username }>{ I.username }</a>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display flex
|
||||
|
||||
> div:last-child
|
||||
margin-left auto
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
</script>
|
||||
</mk-header>
|
3
src/web/app/ch/tags/index.js
Normal file
3
src/web/app/ch/tags/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
require('./index.tag');
|
||||
require('./channel.tag');
|
||||
require('./header.tag');
|
35
src/web/app/ch/tags/index.tag
Normal file
35
src/web/app/ch/tags/index.tag
Normal file
@ -0,0 +1,35 @@
|
||||
<mk-index>
|
||||
<mk-header/>
|
||||
<hr>
|
||||
<button onclick={ n }>%i18n:ch.tags.mk-index.new%</button>
|
||||
<hr>
|
||||
<ul if={ channels }>
|
||||
<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
|
||||
</ul>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('channels').then(channels => {
|
||||
this.update({
|
||||
channels: channels
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.n = () => {
|
||||
const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%');
|
||||
|
||||
this.api('channels/create', {
|
||||
title: title
|
||||
}).then(channel => {
|
||||
location.href = '/' + channel.id;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-index>
|
16
src/web/app/common/scripts/channel-stream.js
Normal file
16
src/web/app/common/scripts/channel-stream.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
import Stream from './stream';
|
||||
|
||||
/**
|
||||
* Channel stream connection
|
||||
*/
|
||||
class Connection extends Stream {
|
||||
constructor(channelId) {
|
||||
super('channel', {
|
||||
channel: channelId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Connection;
|
@ -6,6 +6,7 @@ const host = isRoot ? Url.host : Url.host.substring(Url.host.indexOf('.') + 1, U
|
||||
const scheme = Url.protocol;
|
||||
const url = `${scheme}//${host}`;
|
||||
const apiUrl = `${scheme}//api.${host}`;
|
||||
const chUrl = `${scheme}//ch.${host}`;
|
||||
const devUrl = `${scheme}//dev.${host}`;
|
||||
const aboutUrl = `${scheme}//about.${host}`;
|
||||
const statsUrl = `${scheme}//stats.${host}`;
|
||||
@ -16,6 +17,7 @@ export default {
|
||||
scheme,
|
||||
url,
|
||||
apiUrl,
|
||||
chUrl,
|
||||
devUrl,
|
||||
aboutUrl,
|
||||
statsUrl,
|
||||
|
@ -7,14 +7,15 @@ const route = require('page');
|
||||
let page = null;
|
||||
|
||||
export default me => {
|
||||
route('/', index);
|
||||
route('/i>mentions', mentions);
|
||||
route('/post::post', post);
|
||||
route('/search::query', search);
|
||||
route('/:user', user.bind(null, 'home'));
|
||||
route('/:user/graphs', user.bind(null, 'graphs'));
|
||||
route('/:user/:post', post);
|
||||
route('*', notFound);
|
||||
route('/', index);
|
||||
route('/selectdrive', selectDrive);
|
||||
route('/i>mentions', mentions);
|
||||
route('/post::post', post);
|
||||
route('/search::query', search);
|
||||
route('/:user', user.bind(null, 'home'));
|
||||
route('/:user/graphs', user.bind(null, 'graphs'));
|
||||
route('/:user/:post', post);
|
||||
route('*', notFound);
|
||||
|
||||
function index() {
|
||||
me ? home() : entrance();
|
||||
@ -54,6 +55,10 @@ export default me => {
|
||||
mount(el);
|
||||
}
|
||||
|
||||
function selectDrive() {
|
||||
mount(document.createElement('mk-selectdrive-page'));
|
||||
}
|
||||
|
||||
function notFound() {
|
||||
mount(document.createElement('mk-not-found'));
|
||||
}
|
||||
@ -67,6 +72,7 @@ export default me => {
|
||||
};
|
||||
|
||||
function mount(content) {
|
||||
document.documentElement.style.background = '#313a42';
|
||||
document.documentElement.removeAttribute('data-page');
|
||||
if (page) page.unmount();
|
||||
const body = document.getElementById('app');
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
@import "../../../../node_modules/cropperjs/dist/cropper.css"
|
||||
|
||||
*::input-placeholder
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="feed" if={ !initializing }>
|
||||
<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
|
||||
</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>
|
||||
:scope
|
||||
display block
|
||||
|
@ -61,6 +61,7 @@ require('./pages/user.tag');
|
||||
require('./pages/post.tag');
|
||||
require('./pages/search.tag');
|
||||
require('./pages/not-found.tag');
|
||||
require('./pages/selectdrive.tag');
|
||||
require('./autocomplete-suggestion.tag');
|
||||
require('./progress-dialog.tag');
|
||||
require('./user-preview.tag');
|
||||
|
@ -252,6 +252,12 @@
|
||||
});
|
||||
|
||||
this.onNotification = notification => {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.stream.send({
|
||||
type: 'read_notification',
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.notifications.unshift(notification);
|
||||
this.update();
|
||||
};
|
||||
|
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 => {
|
||||
Progress.set(0.5);
|
||||
document.title = user.name + ' | Misskey'
|
||||
document.title = user.name + ' | Misskey';
|
||||
});
|
||||
|
||||
this.refs.ui.refs.user.on('loaded', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<mk-post-detail title={ title }>
|
||||
<div class="main">
|
||||
<button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
|
||||
<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
|
||||
<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
|
||||
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
||||
</button>
|
||||
@ -9,8 +9,8 @@
|
||||
<mk-post-detail-sub post={ post }/>
|
||||
</virtual>
|
||||
</div>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-post-detail-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-post-detail-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
@ -329,7 +329,7 @@
|
||||
|
||||
// Fetch context
|
||||
this.api('posts/context', {
|
||||
post_id: this.p.reply_to_id
|
||||
post_id: this.p.reply_id
|
||||
}).then(context => {
|
||||
this.update({
|
||||
contextFetching: false,
|
||||
|
@ -475,7 +475,7 @@
|
||||
this.api('posts/create', {
|
||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||
media_ids: files,
|
||||
reply_to_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
|
||||
reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
|
||||
repost_id: this.repost ? this.repost.id : undefined,
|
||||
poll: this.poll ? this.refs.poll.get() : undefined
|
||||
}).then(data => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<mk-sub-post-content>
|
||||
<div class="body">
|
||||
<a class="reply" if={ post.reply_to_id }>
|
||||
<a class="reply" if={ post.reply_id }>
|
||||
<i class="fa fa-reply"></i>
|
||||
</a>
|
||||
<span ref="text"></span>
|
||||
|
@ -82,8 +82,8 @@
|
||||
</mk-timeline>
|
||||
|
||||
<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-timeline-post-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-timeline-post-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
@ -112,7 +112,8 @@
|
||||
</header>
|
||||
<div class="body">
|
||||
<div class="text" ref="text">
|
||||
<a class="reply" if={ p.reply_to }>
|
||||
<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
|
||||
<a class="reply" if={ p.reply }>
|
||||
<i class="fa fa-reply"></i>
|
||||
</a>
|
||||
<p class="dummy"></p>
|
||||
@ -333,6 +334,9 @@
|
||||
font-weight 400
|
||||
font-style normal
|
||||
|
||||
> .channel
|
||||
margin 0
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color #717171
|
||||
|
@ -319,18 +319,26 @@
|
||||
</mk-ui-header-notifications>
|
||||
|
||||
<mk-ui-header-nav>
|
||||
<ul if={ SIGNIN }>
|
||||
<li class="home { active: page == 'home' }">
|
||||
<a href={ CONFIG.url }>
|
||||
<i class="fa fa-home"></i>
|
||||
<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="messaging">
|
||||
<a onclick={ messaging }>
|
||||
<i class="fa fa-comments"></i>
|
||||
<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
|
||||
<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
|
||||
<ul>
|
||||
<virtual if={ SIGNIN }>
|
||||
<li class="home { active: page == 'home' }">
|
||||
<a href={ CONFIG.url }>
|
||||
<i class="fa fa-home"></i>
|
||||
<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="messaging">
|
||||
<a onclick={ messaging }>
|
||||
<i class="fa fa-comments"></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>
|
||||
</li>
|
||||
<li class="info">
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
background-color #fff
|
||||
|
@ -8,6 +8,7 @@ let page = null;
|
||||
|
||||
export default me => {
|
||||
route('/', index);
|
||||
route('/selectdrive', selectDrive);
|
||||
route('/i/notifications', notifications);
|
||||
route('/i/messaging', messaging);
|
||||
route('/i/messaging/:username', messaging);
|
||||
@ -122,6 +123,10 @@ export default me => {
|
||||
mount(el);
|
||||
}
|
||||
|
||||
function selectDrive() {
|
||||
mount(document.createElement('mk-selectdrive-page'));
|
||||
}
|
||||
|
||||
function notFound() {
|
||||
mount(document.createElement('mk-not-found'));
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
#wait
|
||||
top auto
|
||||
|
@ -1,5 +1,5 @@
|
||||
<mk-drive>
|
||||
<nav>
|
||||
<nav ref="nav">
|
||||
<p onclick={ goRoot }><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive.drive%</p>
|
||||
<virtual each={ folder in hierarchyFolders }>
|
||||
<span><i class="fa fa-angle-right"></i></span>
|
||||
@ -56,10 +56,6 @@
|
||||
display block
|
||||
background #fff
|
||||
|
||||
&[data-is-naked]
|
||||
> nav
|
||||
top 48px
|
||||
|
||||
> nav
|
||||
display block
|
||||
position sticky
|
||||
@ -205,6 +201,10 @@
|
||||
} else {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
if (this.opts.isNaked) {
|
||||
this.refs.nav.style.top = `${this.opts.top}px`;
|
||||
}
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
@ -483,7 +483,7 @@
|
||||
if (fn == null || fn == '') return;
|
||||
switch (fn) {
|
||||
case '1':
|
||||
this.refs.file.click();
|
||||
this.selectLocalFile();
|
||||
break;
|
||||
case '2':
|
||||
this.urlUpload();
|
||||
@ -503,6 +503,10 @@
|
||||
}
|
||||
};
|
||||
|
||||
this.selectLocalFile = () => {
|
||||
this.refs.file.click();
|
||||
};
|
||||
|
||||
this.createFolder = () => {
|
||||
const name = window.prompt('フォルダー名');
|
||||
if (name == null || name == '') return;
|
||||
|
@ -1,6 +1,4 @@
|
||||
require('./ui.tag');
|
||||
require('./ui-header.tag');
|
||||
require('./ui-nav.tag');
|
||||
require('./page/entrance.tag');
|
||||
require('./page/entrance/signin.tag');
|
||||
require('./page/entrance/signup.tag');
|
||||
@ -21,6 +19,7 @@ require('./page/settings/authorized-apps.tag');
|
||||
require('./page/settings/twitter.tag');
|
||||
require('./page/messaging.tag');
|
||||
require('./page/messaging-room.tag');
|
||||
require('./page/selectdrive.tag');
|
||||
require('./home.tag');
|
||||
require('./home-timeline.tag');
|
||||
require('./timeline.tag');
|
||||
|
@ -123,6 +123,12 @@
|
||||
});
|
||||
|
||||
this.onNotification = notification => {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.stream.send({
|
||||
type: 'read_notification',
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.notifications.unshift(notification);
|
||||
this.update();
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
<mk-drive-page>
|
||||
<mk-ui ref="ui">
|
||||
<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } data-is-naked="true"/>
|
||||
<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } is-naked={ true } top={ 48 }/>
|
||||
</mk-ui>
|
||||
<style>
|
||||
:scope
|
||||
|
@ -10,16 +10,30 @@
|
||||
import ui from '../../scripts/ui-event';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.on('mount', () => {
|
||||
document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%';
|
||||
ui.trigger('title', '<i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-notifications-page.notifications%');
|
||||
document.documentElement.style.background = '#313a42';
|
||||
|
||||
ui.trigger('func', () => {
|
||||
this.readAll();
|
||||
}, 'check');
|
||||
|
||||
Progress.start();
|
||||
|
||||
this.refs.ui.refs.notifications.on('fetched', () => {
|
||||
Progress.done();
|
||||
});
|
||||
});
|
||||
|
||||
this.readAll = () => {
|
||||
const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
this.api('notifications/mark_as_read_all');
|
||||
};
|
||||
</script>
|
||||
</mk-notifications-page>
|
||||
|
87
src/web/app/mobile/tags/page/selectdrive.tag
Normal file
87
src/web/app/mobile/tags/page/selectdrive.tag
Normal file
@ -0,0 +1,87 @@
|
||||
<mk-selectdrive-page>
|
||||
<header>
|
||||
<h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" if={ files.length > 0 }>({ files.length })</span></h1>
|
||||
<button class="upload" onclick={ upload }><i class="fa fa-upload"></i></button>
|
||||
<button if={ multiple } class="ok" onclick={ ok }><i class="fa fa-check"></i></button>
|
||||
</header>
|
||||
<mk-drive ref="browser" select-file={ true } multiple={ multiple } is-naked={ true } top={ 42 }/>
|
||||
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
width 100%
|
||||
height 100%
|
||||
background #fff
|
||||
|
||||
> header
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
z-index 1000
|
||||
background #fff
|
||||
box-shadow 0 1px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
padding 0
|
||||
text-align center
|
||||
line-height 42px
|
||||
font-size 1em
|
||||
font-weight normal
|
||||
|
||||
> .count
|
||||
margin-left 4px
|
||||
opacity 0.5
|
||||
|
||||
> .upload
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
line-height 42px
|
||||
width 42px
|
||||
|
||||
> .ok
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
line-height 42px
|
||||
width 42px
|
||||
|
||||
> mk-drive
|
||||
top 42px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
const q = (new URL(location)).searchParams;
|
||||
this.multiple = q.get('multiple') == 'true' ? true : false;
|
||||
|
||||
this.on('mount', () => {
|
||||
document.documentElement.style.background = '#fff';
|
||||
|
||||
this.refs.browser.on('selected', file => {
|
||||
this.files = [file];
|
||||
this.ok();
|
||||
});
|
||||
|
||||
this.refs.browser.on('change-selection', files => {
|
||||
this.update({
|
||||
files: files
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.upload = () => {
|
||||
this.refs.browser.selectLocalFile();
|
||||
};
|
||||
|
||||
this.close = () => {
|
||||
window.close();
|
||||
};
|
||||
|
||||
this.ok = () => {
|
||||
window.opener.cb(this.multiple ? this.files : this.files[0]);
|
||||
window.close();
|
||||
};
|
||||
</script>
|
||||
</mk-selectdrive-page>
|
@ -1,5 +1,5 @@
|
||||
<mk-post-detail>
|
||||
<button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
|
||||
<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
|
||||
<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
|
||||
<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
|
||||
</button>
|
||||
@ -8,8 +8,8 @@
|
||||
<mk-post-detail-sub post={ post }/>
|
||||
</virtual>
|
||||
</div>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-post-detail-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-post-detail-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
@ -348,7 +348,7 @@
|
||||
|
||||
// Fetch context
|
||||
this.api('posts/context', {
|
||||
post_id: this.p.reply_to_id
|
||||
post_id: this.p.reply_id
|
||||
}).then(context => {
|
||||
this.update({
|
||||
contextFetching: false,
|
||||
|
@ -267,7 +267,7 @@
|
||||
this.api('posts/create', {
|
||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||
media_ids: files,
|
||||
reply_to_id: opts.reply ? opts.reply.id : undefined,
|
||||
reply_id: opts.reply ? opts.reply.id : undefined,
|
||||
poll: this.poll ? this.refs.poll.get() : undefined
|
||||
}).then(data => {
|
||||
this.trigger('post');
|
||||
|
@ -1,5 +1,5 @@
|
||||
<mk-sub-post-content>
|
||||
<div class="body"><a class="reply" if={ post.reply_to_id }><i class="fa fa-reply"></i></a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
|
||||
<div class="body"><a class="reply" if={ post.reply_id }><i class="fa fa-reply"></i></a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
|
||||
<details if={ post.media }>
|
||||
<summary>({ post.media.length }個のメディア)</summary>
|
||||
<mk-images-viewer images={ post.media }/>
|
||||
|
@ -137,8 +137,8 @@
|
||||
</mk-timeline>
|
||||
|
||||
<mk-timeline-post class={ repost: isRepost }>
|
||||
<div class="reply-to" if={ p.reply_to }>
|
||||
<mk-timeline-post-sub post={ p.reply_to }/>
|
||||
<div class="reply-to" if={ p.reply }>
|
||||
<mk-timeline-post-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" if={ isRepost }>
|
||||
<p>
|
||||
@ -164,7 +164,8 @@
|
||||
</header>
|
||||
<div class="body">
|
||||
<div class="text" ref="text">
|
||||
<a class="reply" if={ p.reply_to }>
|
||||
<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
|
||||
<a class="reply" if={ p.reply }>
|
||||
<i class="fa fa-reply"></i>
|
||||
</a>
|
||||
<p class="dummy"></p>
|
||||
@ -373,6 +374,9 @@
|
||||
mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .channel
|
||||
margin 0
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color #717171
|
||||
|
@ -1,156 +0,0 @@
|
||||
<mk-ui-header>
|
||||
<mk-special-message/>
|
||||
<div class="main">
|
||||
<div class="backdrop"></div>
|
||||
<div class="content">
|
||||
<button class="nav" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button>
|
||||
<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
|
||||
<h1 ref="title">Misskey</h1>
|
||||
<button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
$height = 48px
|
||||
|
||||
display block
|
||||
position fixed
|
||||
top 0
|
||||
z-index 1024
|
||||
width 100%
|
||||
box-shadow 0 1px 0 rgba(#000, 0.075)
|
||||
|
||||
> .main
|
||||
color rgba(#fff, 0.9)
|
||||
|
||||
> .backdrop
|
||||
position absolute
|
||||
top 0
|
||||
z-index 1023
|
||||
width 100%
|
||||
height $height
|
||||
-webkit-backdrop-filter blur(12px)
|
||||
backdrop-filter blur(12px)
|
||||
background-color rgba(#1b2023, 0.75)
|
||||
|
||||
> .content
|
||||
z-index 1024
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 0
|
||||
width 100%
|
||||
max-width calc(100% - 112px)
|
||||
text-align center
|
||||
font-size 1.1em
|
||||
font-weight normal
|
||||
line-height $height
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
> i
|
||||
> .icon
|
||||
margin-right 8px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
width ($height - 16px)
|
||||
height ($height - 16px)
|
||||
margin 8px
|
||||
border-radius 6px
|
||||
|
||||
> .nav
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width $height
|
||||
font-size 1.4em
|
||||
line-height $height
|
||||
border-right solid 1px rgba(#000, 0.1)
|
||||
|
||||
> i
|
||||
transition all 0.2s ease
|
||||
|
||||
> i
|
||||
position absolute
|
||||
top 8px
|
||||
left 8px
|
||||
pointer-events none
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> button:last-child
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
width $height
|
||||
text-align center
|
||||
font-size 1.4em
|
||||
color inherit
|
||||
line-height $height
|
||||
border-left solid 1px rgba(#000, 0.1)
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import ui from '../scripts/ui-event';
|
||||
|
||||
this.mixin('api');
|
||||
this.mixin('stream');
|
||||
|
||||
this.func = null;
|
||||
this.funcIcon = null;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread messaging messages
|
||||
this.api('messaging/unread').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
ui.off('title', this.setTitle);
|
||||
ui.off('func', this.setFunc);
|
||||
});
|
||||
|
||||
this.onReadAllMessagingMessages = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onUnreadMessagingMessage = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
};
|
||||
|
||||
this.setTitle = title => {
|
||||
this.refs.title.innerHTML = title;
|
||||
};
|
||||
|
||||
this.setFunc = (fn, icon) => {
|
||||
this.update({
|
||||
func: fn,
|
||||
funcIcon: icon
|
||||
});
|
||||
};
|
||||
|
||||
ui.on('title', this.setTitle);
|
||||
ui.on('func', this.setFunc);
|
||||
</script>
|
||||
</mk-ui-header>
|
@ -1,170 +0,0 @@
|
||||
<mk-ui-nav>
|
||||
<div class="backdrop" onclick={ parent.toggleDrawer }></div>
|
||||
<div class="body">
|
||||
<a class="me" if={ SIGNIN } href={ '/' + I.username }>
|
||||
<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
|
||||
<p class="name">{ I.name }</p>
|
||||
</a>
|
||||
<div class="links">
|
||||
<ul>
|
||||
<li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li>
|
||||
<li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="fa fa-angle-right"></i></a></li>
|
||||
<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display none
|
||||
|
||||
.backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1025
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.2)
|
||||
|
||||
.body
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1026
|
||||
width 240px
|
||||
height 100%
|
||||
overflow auto
|
||||
-webkit-overflow-scrolling touch
|
||||
color #777
|
||||
background #fff
|
||||
|
||||
.me
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
|
||||
.avatar
|
||||
display inline
|
||||
max-width 64px
|
||||
border-radius 32px
|
||||
vertical-align middle
|
||||
|
||||
.name
|
||||
display block
|
||||
margin 0 16px
|
||||
position absolute
|
||||
top 0
|
||||
left 80px
|
||||
padding 0
|
||||
width calc(100% - 112px)
|
||||
color #777
|
||||
line-height 96px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
|
||||
ul
|
||||
display block
|
||||
margin 16px 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
li
|
||||
display block
|
||||
font-size 1em
|
||||
line-height 1em
|
||||
|
||||
a
|
||||
display block
|
||||
padding 0 20px
|
||||
line-height 3rem
|
||||
line-height calc(1rem + 30px)
|
||||
color #777
|
||||
text-decoration none
|
||||
|
||||
> i:first-child
|
||||
margin-right 0.5em
|
||||
|
||||
> .i
|
||||
margin-left 6px
|
||||
vertical-align super
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> i:last-child
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 0 20px
|
||||
font-size 1.2em
|
||||
line-height calc(1rem + 30px)
|
||||
color #ccc
|
||||
|
||||
.about
|
||||
margin 0
|
||||
padding 1em 0
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
opacity 0.5
|
||||
|
||||
a
|
||||
color #777
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
this.mixin('page');
|
||||
this.mixin('api');
|
||||
this.mixin('stream');
|
||||
|
||||
this.on('mount', () => {
|
||||
this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread messaging messages
|
||||
this.api('messaging/unread').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
});
|
||||
|
||||
this.onReadAllMessagingMessages = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onUnreadMessagingMessage = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
};
|
||||
|
||||
this.search = () => {
|
||||
const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
|
||||
if (query == null || query == '') return;
|
||||
this.page('/search:' + query);
|
||||
};
|
||||
</script>
|
||||
</mk-ui-nav>
|
@ -30,9 +30,378 @@
|
||||
};
|
||||
|
||||
this.onStreamNotification = notification => {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.stream.send({
|
||||
type: 'read_notification',
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
riot.mount(document.body.appendChild(document.createElement('mk-notify')), {
|
||||
notification: notification
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-ui>
|
||||
|
||||
<mk-ui-header>
|
||||
<mk-special-message/>
|
||||
<div class="main">
|
||||
<div class="backdrop"></div>
|
||||
<div class="content">
|
||||
<button class="nav" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button>
|
||||
<i class="fa fa-circle" if={ hasUnreadNotifications || hasUnreadMessagingMessages }></i>
|
||||
<h1 ref="title">Misskey</h1>
|
||||
<button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
$height = 48px
|
||||
|
||||
display block
|
||||
position fixed
|
||||
top 0
|
||||
z-index 1024
|
||||
width 100%
|
||||
box-shadow 0 1px 0 rgba(#000, 0.075)
|
||||
|
||||
> .main
|
||||
color rgba(#fff, 0.9)
|
||||
|
||||
> .backdrop
|
||||
position absolute
|
||||
top 0
|
||||
z-index 1023
|
||||
width 100%
|
||||
height $height
|
||||
-webkit-backdrop-filter blur(12px)
|
||||
backdrop-filter blur(12px)
|
||||
background-color rgba(#1b2023, 0.75)
|
||||
|
||||
> .content
|
||||
z-index 1024
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 0
|
||||
width 100%
|
||||
max-width calc(100% - 112px)
|
||||
text-align center
|
||||
font-size 1.1em
|
||||
font-weight normal
|
||||
line-height $height
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
> i
|
||||
> .icon
|
||||
margin-right 8px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
width ($height - 16px)
|
||||
height ($height - 16px)
|
||||
margin 8px
|
||||
border-radius 6px
|
||||
|
||||
> .nav
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width $height
|
||||
font-size 1.4em
|
||||
line-height $height
|
||||
border-right solid 1px rgba(#000, 0.1)
|
||||
|
||||
> i
|
||||
transition all 0.2s ease
|
||||
|
||||
> i
|
||||
position absolute
|
||||
top 8px
|
||||
left 8px
|
||||
pointer-events none
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> button:last-child
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
width $height
|
||||
text-align center
|
||||
font-size 1.4em
|
||||
color inherit
|
||||
line-height $height
|
||||
border-left solid 1px rgba(#000, 0.1)
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import ui from '../scripts/ui-event';
|
||||
|
||||
this.mixin('api');
|
||||
this.mixin('stream');
|
||||
|
||||
this.func = null;
|
||||
this.funcIcon = null;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.stream.on('read_all_notifications', this.onReadAllNotifications);
|
||||
this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread notifications
|
||||
this.api('notifications/get_unread_count').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadNotifications: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch count of unread messaging messages
|
||||
this.api('messaging/unread').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.stream.off('read_all_notifications', this.onReadAllNotifications);
|
||||
this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
ui.off('title', this.setTitle);
|
||||
ui.off('func', this.setFunc);
|
||||
});
|
||||
|
||||
this.onReadAllNotifications = () => {
|
||||
this.update({
|
||||
hasUnreadNotifications: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onReadAllMessagingMessages = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onUnreadMessagingMessage = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
};
|
||||
|
||||
this.setTitle = title => {
|
||||
this.refs.title.innerHTML = title;
|
||||
};
|
||||
|
||||
this.setFunc = (fn, icon) => {
|
||||
this.update({
|
||||
func: fn,
|
||||
funcIcon: icon
|
||||
});
|
||||
};
|
||||
|
||||
ui.on('title', this.setTitle);
|
||||
ui.on('func', this.setFunc);
|
||||
</script>
|
||||
</mk-ui-header>
|
||||
|
||||
<mk-ui-nav>
|
||||
<div class="backdrop" onclick={ parent.toggleDrawer }></div>
|
||||
<div class="body">
|
||||
<a class="me" if={ SIGNIN } href={ '/' + I.username }>
|
||||
<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
|
||||
<p class="name">{ I.name }</p>
|
||||
</a>
|
||||
<div class="links">
|
||||
<ul>
|
||||
<li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li>
|
||||
<li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="i fa fa-circle" if={ hasUnreadNotifications }></i><i class="fa fa-angle-right"></i></a></li>
|
||||
<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href={ CONFIG.chUrl } target="_blank"><i class="fa fa-television"></i>%i18n:mobile.tags.mk-ui-nav.ch%<i class="fa fa-angle-right"></i></a></li>
|
||||
<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display none
|
||||
|
||||
.backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1025
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.2)
|
||||
|
||||
.body
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1026
|
||||
width 240px
|
||||
height 100%
|
||||
overflow auto
|
||||
-webkit-overflow-scrolling touch
|
||||
color #777
|
||||
background #fff
|
||||
|
||||
.me
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
|
||||
.avatar
|
||||
display inline
|
||||
max-width 64px
|
||||
border-radius 32px
|
||||
vertical-align middle
|
||||
|
||||
.name
|
||||
display block
|
||||
margin 0 16px
|
||||
position absolute
|
||||
top 0
|
||||
left 80px
|
||||
padding 0
|
||||
width calc(100% - 112px)
|
||||
color #777
|
||||
line-height 96px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
|
||||
ul
|
||||
display block
|
||||
margin 16px 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
li
|
||||
display block
|
||||
font-size 1em
|
||||
line-height 1em
|
||||
|
||||
a
|
||||
display block
|
||||
padding 0 20px
|
||||
line-height 3rem
|
||||
line-height calc(1rem + 30px)
|
||||
color #777
|
||||
text-decoration none
|
||||
|
||||
> i:first-child
|
||||
margin-right 0.5em
|
||||
|
||||
> .i
|
||||
margin-left 6px
|
||||
vertical-align super
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> i:last-child
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 0 20px
|
||||
font-size 1.2em
|
||||
line-height calc(1rem + 30px)
|
||||
color #ccc
|
||||
|
||||
.about
|
||||
margin 0
|
||||
padding 1em 0
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
opacity 0.5
|
||||
|
||||
a
|
||||
color #777
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
this.mixin('page');
|
||||
this.mixin('api');
|
||||
this.mixin('stream');
|
||||
|
||||
this.on('mount', () => {
|
||||
this.stream.on('read_all_notifications', this.onReadAllNotifications);
|
||||
this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread notifications
|
||||
this.api('notifications/get_unread_count').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadNotifications: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch count of unread messaging messages
|
||||
this.api('messaging/unread').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.stream.off('read_all_notifications', this.onReadAllNotifications);
|
||||
this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
});
|
||||
|
||||
this.onReadAllNotifications = () => {
|
||||
this.update({
|
||||
hasUnreadNotifications: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onReadAllMessagingMessages = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: false
|
||||
});
|
||||
};
|
||||
|
||||
this.onUnreadMessagingMessage = () => {
|
||||
this.update({
|
||||
hasUnreadMessagingMessages: true
|
||||
});
|
||||
};
|
||||
|
||||
this.search = () => {
|
||||
const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
|
||||
if (query == null || query == '') return;
|
||||
this.page('/search:' + query);
|
||||
};
|
||||
</script>
|
||||
</mk-ui-nav>
|
||||
|
@ -1,16 +1,3 @@
|
||||
*
|
||||
position relative
|
||||
box-sizing border-box
|
||||
background-clip padding-box !important
|
||||
|
||||
html
|
||||
body
|
||||
margin 0
|
||||
padding 0
|
||||
|
||||
body
|
||||
overflow-wrap break-word
|
||||
|
||||
input:not([type])
|
||||
input[type='text']
|
||||
input[type='password']
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
color #456267
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../base"
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
color #456267
|
||||
|
12
test/api.js
12
test/api.js
@ -277,15 +277,15 @@ describe('API', () => {
|
||||
const me = await insertSakurako();
|
||||
const post = {
|
||||
text: 'さく',
|
||||
reply_to_id: himaPost._id.toString()
|
||||
reply_id: himaPost._id.toString()
|
||||
};
|
||||
const res = await request('/posts/create', post, me);
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.a('object');
|
||||
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_to');
|
||||
res.body.reply_to.should.have.property('text').eql(himaPost.text);
|
||||
res.body.should.have.property('reply_id').eql(post.reply_id);
|
||||
res.body.should.have.property('reply');
|
||||
res.body.reply.should.have.property('text').eql(himaPost.text);
|
||||
}));
|
||||
|
||||
it('repostできる', async(async () => {
|
||||
@ -350,7 +350,7 @@ describe('API', () => {
|
||||
const me = await insertSakurako();
|
||||
const post = {
|
||||
text: 'さく',
|
||||
reply_to_id: '000000000000000000000000'
|
||||
reply_id: '000000000000000000000000'
|
||||
};
|
||||
const res = await request('/posts/create', post, me);
|
||||
res.should.have.status(400);
|
||||
@ -369,7 +369,7 @@ describe('API', () => {
|
||||
const me = await insertSakurako();
|
||||
const post = {
|
||||
text: 'さく',
|
||||
reply_to_id: 'kyoppie'
|
||||
reply_id: 'kyoppie'
|
||||
};
|
||||
const res = await request('/posts/create', post, me);
|
||||
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 = {
|
||||
desktop: './src/web/app/desktop/script.js',
|
||||
mobile: './src/web/app/mobile/script.js',
|
||||
ch: './src/web/app/ch/script.js',
|
||||
stats: './src/web/app/stats/script.js',
|
||||
status: './src/web/app/status/script.js',
|
||||
dev: './src/web/app/dev/script.js',
|
||||
|
Loading…
Reference in New Issue
Block a user