From 77528f022d2e9f76298331b55303cfc42359c7af Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Mon, 30 Oct 2017 17:30:32 +0900 Subject: [PATCH 01/16] wip --- locales/en.yml | 6 ++++ locales/ja.yml | 6 ++++ src/api/endpoints/bbs/threads/create.ts | 29 ++++++++++++++++ src/api/models/bbs-thread.ts | 13 ++++++++ src/api/serializers/bbs-thread.ts | 44 +++++++++++++++++++++++++ src/web/app/desktop/tags/index.js | 1 + src/web/app/desktop/tags/pages/bbs.tag | 30 +++++++++++++++++ src/web/app/desktop/tags/ui.tag | 32 +++++++++++------- 8 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 src/api/endpoints/bbs/threads/create.ts create mode 100644 src/api/models/bbs-thread.ts create mode 100644 src/api/serializers/bbs-thread.ts create mode 100644 src/web/app/desktop/tags/pages/bbs.tag diff --git a/locales/en.yml b/locales/en.yml index 03d5306d3..6c763886d 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -241,6 +241,7 @@ desktop: mk-ui-header-nav: home: "Home" messaging: "Messages" + bbs: "BBS" info: "News" mk-ui-header-search: @@ -351,6 +352,11 @@ desktop: mk-repost-form-window: title: "Are you sure you want to repost this post?" + mk-bbs-page: + title: "Misskey BBS" + new: "Create new thread" + thread-title: "Thread title" + mobile: tags: mk-drive-file-viewer: diff --git a/locales/ja.yml b/locales/ja.yml index b640f0f24..1e243fb8d 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -241,6 +241,7 @@ desktop: mk-ui-header-nav: home: "ホーム" messaging: "メッセージ" + bbs: "掲示板" info: "お知らせ" mk-ui-header-search: @@ -351,6 +352,11 @@ desktop: mk-repost-form-window: title: "この投稿をRepostしますか?" + mk-bbs-page: + title: "Misskey掲示板" + new: "スレッドを作成" + thread-title: "スレッドのタイトル" + mobile: tags: mk-drive-file-viewer: diff --git a/src/api/endpoints/bbs/threads/create.ts b/src/api/endpoints/bbs/threads/create.ts new file mode 100644 index 000000000..71d61d871 --- /dev/null +++ b/src/api/endpoints/bbs/threads/create.ts @@ -0,0 +1,29 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Thread from '../../../models/bbs-thread'; +import serialize from '../../../serializers/bbs-thread'; + +/** + * Create a thread + * + * @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 thread + const thread = await Thread.insert({ + created_at: new Date(), + user_id: user._id, + title: title + }); + + // Response + res(await serialize(thread)); +}); diff --git a/src/api/models/bbs-thread.ts b/src/api/models/bbs-thread.ts new file mode 100644 index 000000000..a92157c6f --- /dev/null +++ b/src/api/models/bbs-thread.ts @@ -0,0 +1,13 @@ +import * as mongo from 'mongodb'; +import db from '../../db/mongodb'; + +const collection = db.get('bbs_threads'); + +export default collection as any; // fuck type definition + +export type IBbsThread = { + _id: mongo.ObjectID; + created_at: Date; + title: string; + user_id: mongo.ObjectID; +}; diff --git a/src/api/serializers/bbs-thread.ts b/src/api/serializers/bbs-thread.ts new file mode 100644 index 000000000..d9e41a846 --- /dev/null +++ b/src/api/serializers/bbs-thread.ts @@ -0,0 +1,44 @@ +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { IUser } from '../models/user'; +import { default as Thread, IBbsThread } from '../models/bbs-thread'; + +/** + * Serialize a thread + * + * @param thread target + * @param me? serializee + * @return response + */ +export default ( + thread: string | mongo.ObjectID | IBbsThread, + me?: string | mongo.ObjectID | IUser +) => new Promise<any>(async (resolve, reject) => { + + let _thread: any; + + // Populate the thread if 'thread' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(thread)) { + _thread = await Thread.findOne({ + _id: thread + }); + } else if (typeof thread === 'string') { + _thread = await Thread.findOne({ + _id: new mongo.ObjectID(thread) + }); + } else { + _thread = deepcopy(thread); + } + + // Rename _id to id + _thread.id = _thread._id; + delete _thread._id; + + // Remove needless properties + delete _thread.user_id; + + resolve(_thread); +}); diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index 4e286013a..fa7161ddf 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.js @@ -61,6 +61,7 @@ require('./pages/user.tag'); require('./pages/post.tag'); require('./pages/search.tag'); require('./pages/not-found.tag'); +require('./pages/bbs.tag'); require('./autocomplete-suggestion.tag'); require('./progress-dialog.tag'); require('./user-preview.tag'); diff --git a/src/web/app/desktop/tags/pages/bbs.tag b/src/web/app/desktop/tags/pages/bbs.tag new file mode 100644 index 000000000..cb58af193 --- /dev/null +++ b/src/web/app/desktop/tags/pages/bbs.tag @@ -0,0 +1,30 @@ +<mk-bbs-page> + <mk-ui ref="ui"> + <main> + <h1>%i18n:desktop.tags.mk-bbs-page.title%</h1> + <button onclick={ parent.new }>%i18n:desktop.tags.mk-bbs-page.new%</button> + </main> + </mk-ui> + <style> + :scope + display block + + </style> + <script> + this.mixin('api'); + + this.on('mount', () => { + document.title = '%i18n:desktop.tags.mk-bbs-page.title%'; + }); + + this.new = () => { + const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.thread-title%'); + + this.api('bbs/threads/create', { + title: title + }).then(thread => { + location.href = '/bbs/' + thread.id; + }); + }; + </script> +</mk-bbs-page> diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index e0d7393b0..452a72c00 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -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="bbs"> + <a href={ CONFIG.url + '/bbs' }> + <i class="fa fa-coffee"></i> + <p>%i18n:desktop.tags.mk-ui-header-nav.bbs%</p> </a> </li> <li class="info"> From dc9fddf839df7959a83819eb7064f402db05f200 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 31 Oct 2017 21:42:11 +0900 Subject: [PATCH 02/16] RENAME: bbs -> channel --- locales/en.yml | 7 ++- locales/ja.yml | 7 ++- src/api/endpoints/bbs/threads/create.ts | 12 ++--- src/api/models/{bbs-thread.ts => channel.ts} | 4 +- src/api/serializers/bbs-thread.ts | 44 ------------------- src/api/serializers/channel.ts | 44 +++++++++++++++++++ src/web/app/desktop/tags/index.js | 2 +- .../tags/pages/{bbs.tag => channels.tag} | 12 ++--- 8 files changed, 65 insertions(+), 67 deletions(-) rename src/api/models/{bbs-thread.ts => channel.ts} (75%) delete mode 100644 src/api/serializers/bbs-thread.ts create mode 100644 src/api/serializers/channel.ts rename src/web/app/desktop/tags/pages/{bbs.tag => channels.tag} (64%) diff --git a/locales/en.yml b/locales/en.yml index f0204b52c..da532fc78 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -352,10 +352,9 @@ desktop: mk-repost-form-window: title: "Are you sure you want to repost this post?" - mk-bbs-page: - title: "Misskey BBS" - new: "Create new thread" - thread-title: "Thread title" + mk-channels-page: + new: "Create new channel" + channel-title: "Channel title" mobile: tags: diff --git a/locales/ja.yml b/locales/ja.yml index 65d92782f..1ae94652b 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -352,10 +352,9 @@ desktop: mk-repost-form-window: title: "この投稿をRepostしますか?" - mk-bbs-page: - title: "Misskey掲示板" - new: "スレッドを作成" - thread-title: "スレッドのタイトル" + mk-channels-page: + new: "チャンネルを作成" + channel-title: "チャンネルのタイトル" mobile: tags: diff --git a/src/api/endpoints/bbs/threads/create.ts b/src/api/endpoints/bbs/threads/create.ts index 71d61d871..d9b4d34a0 100644 --- a/src/api/endpoints/bbs/threads/create.ts +++ b/src/api/endpoints/bbs/threads/create.ts @@ -2,11 +2,11 @@ * Module dependencies */ import $ from 'cafy'; -import Thread from '../../../models/bbs-thread'; -import serialize from '../../../serializers/bbs-thread'; +import Channel from '../../../models/channel'; +import serialize from '../../../serializers/channel'; /** - * Create a thread + * Create a channel * * @param {any} params * @param {any} user @@ -17,13 +17,13 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { const [title, titleErr] = $(params.title).string().range(1, 100).$; if (titleErr) return rej('invalid title param'); - // Create a thread - const thread = await Thread.insert({ + // Create a channel + const channel = await Channel.insert({ created_at: new Date(), user_id: user._id, title: title }); // Response - res(await serialize(thread)); + res(await serialize(channel)); }); diff --git a/src/api/models/bbs-thread.ts b/src/api/models/channel.ts similarity index 75% rename from src/api/models/bbs-thread.ts rename to src/api/models/channel.ts index a92157c6f..79edb7136 100644 --- a/src/api/models/bbs-thread.ts +++ b/src/api/models/channel.ts @@ -1,11 +1,11 @@ import * as mongo from 'mongodb'; import db from '../../db/mongodb'; -const collection = db.get('bbs_threads'); +const collection = db.get('channels'); export default collection as any; // fuck type definition -export type IBbsThread = { +export type IChannel = { _id: mongo.ObjectID; created_at: Date; title: string; diff --git a/src/api/serializers/bbs-thread.ts b/src/api/serializers/bbs-thread.ts deleted file mode 100644 index d9e41a846..000000000 --- a/src/api/serializers/bbs-thread.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import { IUser } from '../models/user'; -import { default as Thread, IBbsThread } from '../models/bbs-thread'; - -/** - * Serialize a thread - * - * @param thread target - * @param me? serializee - * @return response - */ -export default ( - thread: string | mongo.ObjectID | IBbsThread, - me?: string | mongo.ObjectID | IUser -) => new Promise<any>(async (resolve, reject) => { - - let _thread: any; - - // Populate the thread if 'thread' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(thread)) { - _thread = await Thread.findOne({ - _id: thread - }); - } else if (typeof thread === 'string') { - _thread = await Thread.findOne({ - _id: new mongo.ObjectID(thread) - }); - } else { - _thread = deepcopy(thread); - } - - // Rename _id to id - _thread.id = _thread._id; - delete _thread._id; - - // Remove needless properties - delete _thread.user_id; - - resolve(_thread); -}); diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts new file mode 100644 index 000000000..d4e16d6be --- /dev/null +++ b/src/api/serializers/channel.ts @@ -0,0 +1,44 @@ +/** + * Module dependencies + */ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import { IUser } from '../models/user'; +import { default as Channel, IChannel } from '../models/channel'; + +/** + * 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; + + resolve(_channel); +}); diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index fa7161ddf..6d4900652 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.js @@ -61,7 +61,7 @@ require('./pages/user.tag'); require('./pages/post.tag'); require('./pages/search.tag'); require('./pages/not-found.tag'); -require('./pages/bbs.tag'); +require('./pages/channels.tag'); require('./autocomplete-suggestion.tag'); require('./progress-dialog.tag'); require('./user-preview.tag'); diff --git a/src/web/app/desktop/tags/pages/bbs.tag b/src/web/app/desktop/tags/pages/channels.tag similarity index 64% rename from src/web/app/desktop/tags/pages/bbs.tag rename to src/web/app/desktop/tags/pages/channels.tag index cb58af193..9e47e52d2 100644 --- a/src/web/app/desktop/tags/pages/bbs.tag +++ b/src/web/app/desktop/tags/pages/channels.tag @@ -1,4 +1,4 @@ -<mk-bbs-page> +<mk-channels-page> <mk-ui ref="ui"> <main> <h1>%i18n:desktop.tags.mk-bbs-page.title%</h1> @@ -18,13 +18,13 @@ }); this.new = () => { - const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.thread-title%'); + const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.channel-title%'); - this.api('bbs/threads/create', { + this.api('bbs/channels/create', { title: title - }).then(thread => { - location.href = '/bbs/' + thread.id; + }).then(channel => { + location.href = '/bbs/' + channel.id; }); }; </script> -</mk-bbs-page> +</mk-channels-page> From b4340b1d91a6fc1679c3cb891ea800e1b491109c Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 31 Oct 2017 22:09:09 +0900 Subject: [PATCH 03/16] wip --- src/api/endpoints/posts/create.ts | 47 +++++++++++++++++++++++++++---- src/api/models/post.ts | 1 + 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index 805dba7f8..42a55f850 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -4,9 +4,9 @@ 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'; @@ -62,7 +62,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,18 +85,20 @@ 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'); } } @@ -104,7 +107,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$; if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id'); - let inReplyToPost = null; + let inReplyToPost: IPost = null; if (inReplyToPostId !== undefined) { // Fetch reply inReplyToPost = await Post.findOne({ @@ -121,6 +124,37 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { } } + // 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 (inReplyToPost && !channelId.equals(inReplyToPost.channel_id)) { + return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); + } + + // Repost対象の投稿がこのチャンネルじゃなかったらダメ + if (repost && !channelId.equals(repost.channel_id)) { + return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません'); + } + + // 引用ではないRepostはダメ + if (repost && !isQuote) { + return rej('チャンネル内部では引用ではないRepostをすることはできません'); + } + } + // Get 'poll' parameter const [poll, pollErr] = $(params.poll).optional.strict.object() .have('choices', $().array('string') @@ -164,6 +198,7 @@ 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, media_ids: files ? files.map(file => file._id) : undefined, reply_to_id: inReplyToPost ? inReplyToPost._id : undefined, repost_id: repost ? repost._id : undefined, diff --git a/src/api/models/post.ts b/src/api/models/post.ts index 8b9f7f5ef..fe07dcb0b 100644 --- a/src/api/models/post.ts +++ b/src/api/models/post.ts @@ -10,6 +10,7 @@ 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; From 30a4e839a687bed7ed839e3c17f6781bb4b76499 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 31 Oct 2017 22:14:12 +0900 Subject: [PATCH 04/16] Fix indent --- src/api/endpoints/posts/create.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index 42a55f850..e0a02fa4a 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -186,11 +186,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { 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: inReplyToPost ? inReplyToPost._id.toString() : null, + repost: repost ? repost._id.toString() : null, + media_ids: (files || []).map(file => file._id.toString()) + })) { return rej('duplicate'); } } From 5efb52b9f563ae7d6b5383d054a6c21fee676b68 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 31 Oct 2017 22:35:31 +0900 Subject: [PATCH 05/16] wip --- locales/en.yml | 2 +- locales/ja.yml | 2 +- src/api/endpoints.ts | 12 ++++++++++-- .../endpoints/{bbs/threads => channels}/create.ts | 4 ++-- src/web/app/desktop/router.js | 5 +++++ src/web/app/desktop/tags/pages/channels.tag | 8 +++----- src/web/app/desktop/tags/ui.tag | 8 ++++---- 7 files changed, 26 insertions(+), 15 deletions(-) rename src/api/endpoints/{bbs/threads => channels}/create.ts (84%) diff --git a/locales/en.yml b/locales/en.yml index da532fc78..5c7a1165b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -241,7 +241,7 @@ desktop: mk-ui-header-nav: home: "Home" messaging: "Messages" - bbs: "BBS" + channels: "Channels" info: "News" mk-ui-header-search: diff --git a/locales/ja.yml b/locales/ja.yml index 1ae94652b..dd76a2b90 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -241,7 +241,7 @@ desktop: mk-ui-header-nav: home: "ホーム" messaging: "メッセージ" - bbs: "掲示板" + channels: "チャンネル" info: "お知らせ" mk-ui-header-search: diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 29a97bcb8..26177b877 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -474,8 +474,16 @@ 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') + } + }, ]; export default endpoints; diff --git a/src/api/endpoints/bbs/threads/create.ts b/src/api/endpoints/channels/create.ts similarity index 84% rename from src/api/endpoints/bbs/threads/create.ts rename to src/api/endpoints/channels/create.ts index d9b4d34a0..74b089dfc 100644 --- a/src/api/endpoints/bbs/threads/create.ts +++ b/src/api/endpoints/channels/create.ts @@ -2,8 +2,8 @@ * Module dependencies */ import $ from 'cafy'; -import Channel from '../../../models/channel'; -import serialize from '../../../serializers/channel'; +import Channel from '../../models/channel'; +import serialize from '../../serializers/channel'; /** * Create a channel diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js index afa8a2dce..51738f3af 100644 --- a/src/web/app/desktop/router.js +++ b/src/web/app/desktop/router.js @@ -9,6 +9,7 @@ let page = null; export default me => { route('/', index); route('/i>mentions', mentions); + route('/channel', channels); route('/post::post', post); route('/search::query', search); route('/:user', user.bind(null, 'home')); @@ -54,6 +55,10 @@ export default me => { mount(el); } + function channels() { + mount(document.createElement('mk-channels-page')); + } + function notFound() { mount(document.createElement('mk-not-found')); } diff --git a/src/web/app/desktop/tags/pages/channels.tag b/src/web/app/desktop/tags/pages/channels.tag index 9e47e52d2..03fae3c8d 100644 --- a/src/web/app/desktop/tags/pages/channels.tag +++ b/src/web/app/desktop/tags/pages/channels.tag @@ -1,8 +1,7 @@ <mk-channels-page> <mk-ui ref="ui"> <main> - <h1>%i18n:desktop.tags.mk-bbs-page.title%</h1> - <button onclick={ parent.new }>%i18n:desktop.tags.mk-bbs-page.new%</button> + <button onclick={ parent.new }>%i18n:desktop.tags.mk-channels-page.new%</button> </main> </mk-ui> <style> @@ -14,16 +13,15 @@ this.mixin('api'); this.on('mount', () => { - document.title = '%i18n:desktop.tags.mk-bbs-page.title%'; }); this.new = () => { - const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.channel-title%'); + const title = window.prompt('%i18n:desktop.tags.mk-channels-page.channel-title%'); this.api('bbs/channels/create', { title: title }).then(channel => { - location.href = '/bbs/' + channel.id; + location.href = '/channel/' + channel.id; }); }; </script> diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index 452a72c00..7527358dc 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -335,10 +335,10 @@ </a> </li> </virtual> - <li class="bbs"> - <a href={ CONFIG.url + '/bbs' }> - <i class="fa fa-coffee"></i> - <p>%i18n:desktop.tags.mk-ui-header-nav.bbs%</p> + <li class="channels"> + <a href={ CONFIG.url + '/channel' }> + <i class="fa fa-television"></i> + <p>%i18n:desktop.tags.mk-ui-header-nav.channels%</p> </a> </li> <li class="info"> From f87ec61e96a8c1f070abefc6a3b5f7e68e24705d Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Tue, 31 Oct 2017 23:11:22 +0900 Subject: [PATCH 06/16] wip --- src/api/endpoints.ts | 3 ++ src/api/endpoints/channels/show.ts | 31 +++++++++++++++ src/web/app/desktop/router.js | 26 ++++++++----- src/web/app/desktop/tags/index.js | 1 + src/web/app/desktop/tags/pages/channel.tag | 43 +++++++++++++++++++++ src/web/app/desktop/tags/pages/channels.tag | 2 +- src/web/app/desktop/tags/pages/user.tag | 2 +- 7 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 src/api/endpoints/channels/show.ts create mode 100644 src/web/app/desktop/tags/pages/channel.tag diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 26177b877..45b83fc9e 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -484,6 +484,9 @@ const endpoints: Endpoint[] = [ minInterval: ms('10seconds') } }, + { + name: 'channels/show' + }, ]; export default endpoints; diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts new file mode 100644 index 000000000..8861e5459 --- /dev/null +++ b/src/api/endpoints/channels/show.ts @@ -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)); +}); diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js index 51738f3af..d9300cc69 100644 --- a/src/web/app/desktop/router.js +++ b/src/web/app/desktop/router.js @@ -7,15 +7,16 @@ const route = require('page'); let page = null; export default me => { - route('/', index); - route('/i>mentions', mentions); - route('/channel', channels); - 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('/i>mentions', mentions); + route('/channel', channels); + route('/channel/:channel', channel); + 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(); @@ -55,6 +56,12 @@ export default me => { mount(el); } + function channel(ctx) { + const el = document.createElement('mk-channel-page'); + el.setAttribute('id', ctx.params.channel); + mount(el); + } + function channels() { mount(document.createElement('mk-channels-page')); } @@ -72,6 +79,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'); diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index 6d4900652..7fdeb6884 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.js @@ -61,6 +61,7 @@ require('./pages/user.tag'); require('./pages/post.tag'); require('./pages/search.tag'); require('./pages/not-found.tag'); +require('./pages/channel.tag'); require('./pages/channels.tag'); require('./autocomplete-suggestion.tag'); require('./progress-dialog.tag'); diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag new file mode 100644 index 000000000..4fa172f99 --- /dev/null +++ b/src/web/app/desktop/tags/pages/channel.tag @@ -0,0 +1,43 @@ +<mk-channel-page> + <mk-ui ref="ui"> + <main if={ !parent.fetching }> + <h1>{ parent.channel.title }</h1> + </main> + </mk-ui> + <style> + :scope + display block + + main + > h1 + color #f00 + </style> + <script> + import Progress from '../../../common/scripts/loading'; + + this.mixin('api'); + + this.id = this.opts.id; + this.fetching = true; + this.channel = null; + + this.on('mount', () => { + document.documentElement.style.background = '#efefef'; + + Progress.start(); + + this.api('channels/show', { + channel_id: this.id + }).then(channel => { + Progress.done(); + + this.update({ + fetching: false, + channel: channel + }); + + document.title = channel.title + ' | Misskey' + }); + }); + </script> +</mk-channel-page> diff --git a/src/web/app/desktop/tags/pages/channels.tag b/src/web/app/desktop/tags/pages/channels.tag index 03fae3c8d..220f1ca50 100644 --- a/src/web/app/desktop/tags/pages/channels.tag +++ b/src/web/app/desktop/tags/pages/channels.tag @@ -18,7 +18,7 @@ this.new = () => { const title = window.prompt('%i18n:desktop.tags.mk-channels-page.channel-title%'); - this.api('bbs/channels/create', { + this.api('channels/create', { title: title }).then(channel => { location.href = '/channel/' + channel.id; diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag index 864fe2273..811ca5c0f 100644 --- a/src/web/app/desktop/tags/pages/user.tag +++ b/src/web/app/desktop/tags/pages/user.tag @@ -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', () => { From 346c2959e058fa445ebb82e71eb37ef023ba6bd4 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 00:10:30 +0900 Subject: [PATCH 07/16] wip --- src/api/endpoints.ts | 3 + src/api/endpoints/channels/posts.ts | 79 +++++++++++++++++ src/web/app/common/scripts/channel-stream.js | 14 +++ src/web/app/desktop/tags/pages/channel.tag | 87 +++++++++++++++++++ .../app/desktop/tags/pages/drive-chooser.tag | 44 ++++++++++ 5 files changed, 227 insertions(+) create mode 100644 src/api/endpoints/channels/posts.ts create mode 100644 src/web/app/common/scripts/channel-stream.js create mode 100644 src/web/app/desktop/tags/pages/drive-chooser.tag diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 45b83fc9e..88c01d4e7 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -487,6 +487,9 @@ const endpoints: Endpoint[] = [ { name: 'channels/show' }, + { + name: 'channels/posts' + }, ]; export default endpoints; diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts new file mode 100644 index 000000000..fa91fb93e --- /dev/null +++ b/src/api/endpoints/channels/posts.ts @@ -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) + ))); +}); diff --git a/src/web/app/common/scripts/channel-stream.js b/src/web/app/common/scripts/channel-stream.js new file mode 100644 index 000000000..38e7d9113 --- /dev/null +++ b/src/web/app/common/scripts/channel-stream.js @@ -0,0 +1,14 @@ +'use strict'; + +import Stream from './stream'; + +/** + * Channel stream connection + */ +class Connection extends Stream { + constructor() { + super('channel'); + } +} + +export default Connection; diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag index 4fa172f99..8a3034f40 100644 --- a/src/web/app/desktop/tags/pages/channel.tag +++ b/src/web/app/desktop/tags/pages/channel.tag @@ -2,6 +2,8 @@ <mk-ui ref="ui"> <main if={ !parent.fetching }> <h1>{ parent.channel.title }</h1> + <mk-channel-post each={ parent.posts } post={ this }/> + <mk-channel-form channel={ parent.channel }/> </main> </mk-ui> <style> @@ -14,12 +16,15 @@ </style> <script> import Progress from '../../../common/scripts/loading'; + import ChannelStream from '../../../common/scripts/channel-stream'; this.mixin('api'); this.id = this.opts.id; this.fetching = true; this.channel = null; + this.posts = null; + this.connection = new ChannelStream(); this.on('mount', () => { document.documentElement.style.background = '#efefef'; @@ -38,6 +43,88 @@ document.title = channel.title + ' | Misskey' }); + + this.api('channels/posts', { + channel_id: this.id + }).then(posts => { + this.update({ + posts: posts + }); + }); }); </script> </mk-channel-page> + +<mk-channel-post> + <header> + <b>{ post.user.name }</b> + </header> + <div> + { post.text } + </div> + <style> + :scope + display block + margin 0 + padding 0 + + > header + > b + color #008000 + + </style> + <script> + this.post = this.opts.post; + </script> +</mk-channel-post> + +<mk-channel-form> + <p if={ reply }>{ reply.user.name }への返信: (or <a onclick={ clearReply }>キャンセル</a>)</p> + <textarea ref="text" disabled={ wait }></textarea> + <button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }> + { wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/> + </button> + + <style> + :scope + display block + + </style> + <script> + this.mixin('api'); + + this.channel = this.opts.channel; + + this.clearReply = () => { + this.update({ + reply: null + }); + }; + + this.clear = () => { + this.clearReply(); + this.refs.text.value = ''; + }; + + this.post = e => { + this.update({ + wait: true + }); + + this.api('posts/create', { + text: this.refs.text.value, + reply_to_id: this.reply ? this.reply.id : undefined, + channel_id: this.channel.id + }).then(data => { + this.clear(); + }).catch(err => { + alert('失敗した'); + }).then(() => { + this.update({ + wait: false + }); + }); + }; + + </script> +</mk-channel-form> diff --git a/src/web/app/desktop/tags/pages/drive-chooser.tag b/src/web/app/desktop/tags/pages/drive-chooser.tag new file mode 100644 index 000000000..49741ad40 --- /dev/null +++ b/src/web/app/desktop/tags/pages/drive-chooser.tag @@ -0,0 +1,44 @@ +<mk-drive-chooser> + <mk-drive-browser ref="browser" multiple={ parent.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={ parent.ok }>決定</button> + </div> + + <style> + :scope + display block + height 100% + + </style> + <script> + this.multiple = this.opts.multiple != null ? this.opts.multiple : false; + + this.on('mount', () => { + 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-drive-chooser> From 71c3e11708dad327924bdcb95193d44c2b11a907 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 01:38:19 +0900 Subject: [PATCH 08/16] wip --- src/api/endpoints/channels/create.ts | 3 +- src/api/endpoints/posts/create.ts | 17 +++++++++++ src/api/models/channel.ts | 1 + src/api/serializers/post.ts | 8 ++++- src/web/app/desktop/tags/pages/channel.tag | 35 ++++++++++++++++++---- src/web/app/desktop/tags/timeline.tag | 4 +++ src/web/app/mobile/tags/timeline.tag | 4 +++ 7 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts index 74b089dfc..e0c0e0192 100644 --- a/src/api/endpoints/channels/create.ts +++ b/src/api/endpoints/channels/create.ts @@ -21,7 +21,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { const channel = await Channel.insert({ created_at: new Date(), user_id: user._id, - title: title + title: title, + index: 0 }); // Response diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index e0a02fa4a..183cabf13 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -153,6 +153,16 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { if (repost && !isQuote) { return rej('チャンネル内部では引用ではないRepostをすることはできません'); } + } else { + // 返信対象の投稿がチャンネルへの投稿だったらダメ + if (inReplyToPost && inReplyToPost.channel_id != null) { + return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); + } + + // Repost対象の投稿がチャンネルへの投稿だったらダメ + if (repost && repost.channel_id != null) { + return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません'); + } } // Get 'poll' parameter @@ -199,6 +209,7 @@ 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, repost_id: repost ? repost._id : undefined, @@ -217,6 +228,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // ----------------------------------------------------------- // Post processes + Channel.update({ _id: channel._id }, { + $inc: { + index: 1 + } + }); + User.update({ _id: user._id }, { $set: { latest_post: post diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts index 79edb7136..c80e84dbc 100644 --- a/src/api/models/channel.ts +++ b/src/api/models/channel.ts @@ -10,4 +10,5 @@ export type IChannel = { created_at: Date; title: string; user_id: mongo.ObjectID; + index: number; }; diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts index df917a859..7d40df2d6 100644 --- a/src/api/serializers/post.ts +++ b/src/api/serializers/post.ts @@ -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) )); diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag index 8a3034f40..ebd26f07b 100644 --- a/src/web/app/desktop/tags/pages/channel.tag +++ b/src/web/app/desktop/tags/pages/channel.tag @@ -2,8 +2,9 @@ <mk-ui ref="ui"> <main if={ !parent.fetching }> <h1>{ parent.channel.title }</h1> - <mk-channel-post each={ parent.posts } post={ this }/> - <mk-channel-form channel={ parent.channel }/> + <mk-channel-post if={ parent.posts } each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/> + <hr> + <mk-channel-form channel={ parent.channel } ref="form"/> </main> </mk-ui> <style> @@ -11,6 +12,8 @@ display block main + padding 8px + > h1 color #f00 </style> @@ -57,9 +60,13 @@ <mk-channel-post> <header> - <b>{ post.user.name }</b> + <a class="index" onclick={ reply }>{ post.index }:</a> + <a class="name" href={ '/' + post.user.username }><b>{ post.user.name }</b></a> + <mk-time time={ post.created_at } mode="detail"/> + <span>ID:<i>{ post.user.username }</i></span> </header> <div> + <a if={ post.reply_to }>>>{ post.reply_to.index }</a> { post.text } </div> <style> @@ -69,17 +76,35 @@ padding 0 > header - > b + > .index + margin-right 0.25em + color #000 + + > .name + margin-right 0.5em color #008000 + > mk-time + margin-right 0.5em + + > div + padding 0 0 1em 2em + </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 }>{ reply.user.name }への返信: (or <a onclick={ clearReply }>キャンセル</a>)</p> + <p if={ reply }><b>>>{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p> <textarea ref="text" disabled={ wait }></textarea> <button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }> { wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/> diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag index 2d6b439e3..17b2c66dc 100644 --- a/src/web/app/desktop/tags/timeline.tag +++ b/src/web/app/desktop/tags/timeline.tag @@ -112,6 +112,7 @@ </header> <div class="body"> <div class="text" ref="text"> + <p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p> <a class="reply" if={ p.reply_to }> <i class="fa fa-reply"></i> </a> @@ -333,6 +334,9 @@ font-weight 400 font-style normal + > .channel + margin 0 + > .reply margin-right 8px color #717171 diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag index c7f5bfd68..b26a5cb10 100644 --- a/src/web/app/mobile/tags/timeline.tag +++ b/src/web/app/mobile/tags/timeline.tag @@ -164,6 +164,7 @@ </header> <div class="body"> <div class="text" ref="text"> + <p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p> <a class="reply" if={ p.reply_to }> <i class="fa fa-reply"></i> </a> @@ -373,6 +374,9 @@ mk-url-preview margin-top 8px + > .channel + margin 0 + > .reply margin-right 8px color #717171 From e770cd6f55a5e424e731ebb89b5a091afa129904 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 02:16:05 +0900 Subject: [PATCH 09/16] wip --- src/web/app/desktop/router.js | 5 + src/web/app/desktop/tags/index.js | 1 + src/web/app/desktop/tags/pages/channel.tag | 33 +++- .../app/desktop/tags/pages/drive-chooser.tag | 44 ----- .../app/desktop/tags/pages/selectdrive.tag | 159 ++++++++++++++++++ 5 files changed, 196 insertions(+), 46 deletions(-) delete mode 100644 src/web/app/desktop/tags/pages/drive-chooser.tag create mode 100644 src/web/app/desktop/tags/pages/selectdrive.tag diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js index d9300cc69..df67bb7b7 100644 --- a/src/web/app/desktop/router.js +++ b/src/web/app/desktop/router.js @@ -8,6 +8,7 @@ let page = null; export default me => { route('/', index); + route('/selectdrive', selectDrive); route('/i>mentions', mentions); route('/channel', channels); route('/channel/:channel', channel); @@ -66,6 +67,10 @@ export default me => { mount(document.createElement('mk-channels-page')); } + function selectDrive() { + mount(document.createElement('mk-selectdrive-page')); + } + function notFound() { mount(document.createElement('mk-not-found')); } diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index 7fdeb6884..0b92d8c23 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.js @@ -63,6 +63,7 @@ require('./pages/search.tag'); require('./pages/not-found.tag'); require('./pages/channel.tag'); require('./pages/channels.tag'); +require('./pages/selectdrive.tag'); require('./autocomplete-suggestion.tag'); require('./progress-dialog.tag'); require('./user-preview.tag'); diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag index ebd26f07b..a14c0648c 100644 --- a/src/web/app/desktop/tags/pages/channel.tag +++ b/src/web/app/desktop/tags/pages/channel.tag @@ -2,7 +2,9 @@ <mk-ui ref="ui"> <main if={ !parent.fetching }> <h1>{ parent.channel.title }</h1> - <mk-channel-post if={ parent.posts } each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/> + <virtual if={ parent.posts }> + <mk-channel-post each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/> + </virtual> <hr> <mk-channel-form channel={ parent.channel } ref="form"/> </main> @@ -68,6 +70,11 @@ <div> <a if={ post.reply_to }>>>{ post.reply_to.index }</a> { post.text } + <div class="media" if={ post.media }> + <virtual each={ file in post.media }> + <img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/> + </virtual> + </div> </div> <style> :scope @@ -109,13 +116,19 @@ <button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }> { wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/> </button> - + <br> + <button onclick={ drive }>ドライブ</button> + <ol if={ files }> + <li each={ files }>{ name }</li> + </ol> <style> :scope display block </style> <script> + import CONFIG from '../../../common/scripts/config'; + this.mixin('api'); this.channel = this.opts.channel; @@ -128,6 +141,9 @@ this.clear = () => { this.clearReply(); + this.update({ + files: null + }); this.refs.text.value = ''; }; @@ -136,8 +152,13 @@ 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, + media_ids: files, reply_to_id: this.reply ? this.reply.id : undefined, channel_id: this.channel.id }).then(data => { @@ -151,5 +172,13 @@ }); }; + this.drive = () => { + window['cb'] = files => { + this.update({ + files: files + }); + }; + window.open(CONFIG.url + '/selectdrive?multiple=true', '_blank'); + }; </script> </mk-channel-form> diff --git a/src/web/app/desktop/tags/pages/drive-chooser.tag b/src/web/app/desktop/tags/pages/drive-chooser.tag deleted file mode 100644 index 49741ad40..000000000 --- a/src/web/app/desktop/tags/pages/drive-chooser.tag +++ /dev/null @@ -1,44 +0,0 @@ -<mk-drive-chooser> - <mk-drive-browser ref="browser" multiple={ parent.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={ parent.ok }>決定</button> - </div> - - <style> - :scope - display block - height 100% - - </style> - <script> - this.multiple = this.opts.multiple != null ? this.opts.multiple : false; - - this.on('mount', () => { - 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-drive-chooser> diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag new file mode 100644 index 000000000..b196357d8 --- /dev/null +++ b/src/web/app/desktop/tags/pages/selectdrive.tag @@ -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> From f37fb38640a31c4b8865a5562628197ff21f3cce Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 03:17:14 +0900 Subject: [PATCH 10/16] wip --- docs/setup.en.md | 1 + docs/setup.ja.md | 1 + locales/en.yml | 13 +++-- locales/ja.yml | 13 +++-- src/api/endpoints/posts/create.ts | 7 ++- src/api/event.ts | 6 +++ src/api/stream/channel.ts | 12 +++++ src/api/streaming.ts | 22 +++++--- src/config.ts | 2 + src/web/app/ch/router.js | 32 ++++++++++++ src/web/app/ch/script.js | 18 +++++++ src/web/app/ch/style.styl | 4 ++ .../tags/pages => ch/tags}/channel.tag | 52 +++++++++++++------ src/web/app/ch/tags/index.js | 2 + src/web/app/ch/tags/index.tag | 24 +++++++++ src/web/app/common/scripts/channel-stream.js | 6 ++- src/web/app/common/scripts/config.js | 2 + src/web/app/desktop/router.js | 12 ----- src/web/app/desktop/tags/index.js | 2 - src/web/app/desktop/tags/pages/channels.tag | 28 ---------- src/web/app/desktop/tags/timeline.tag | 2 +- src/web/app/desktop/tags/ui.tag | 6 +-- src/web/app/mobile/tags/timeline.tag | 2 +- src/web/app/mobile/tags/ui.tag | 5 +- webpack/webpack.config.ts | 1 + 25 files changed, 189 insertions(+), 86 deletions(-) create mode 100644 src/api/stream/channel.ts create mode 100644 src/web/app/ch/router.js create mode 100644 src/web/app/ch/script.js create mode 100644 src/web/app/ch/style.styl rename src/web/app/{desktop/tags/pages => ch/tags}/channel.tag (76%) create mode 100644 src/web/app/ch/tags/index.js create mode 100644 src/web/app/ch/tags/index.tag delete mode 100644 src/web/app/desktop/tags/pages/channels.tag diff --git a/docs/setup.en.md b/docs/setup.en.md index 3e4893534..dbc0599b5 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -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}* diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 4f48a0808..602fd9b6a 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -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}* diff --git a/locales/en.yml b/locales/en.yml index 5c7a1165b..643649b46 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -164,6 +164,12 @@ common: mk-uploader: waiting: "Waiting" +ch: + tags: + mk-index: + new: "Create new channel" + channel-title: "Channel title" + desktop: tags: mk-api-info: @@ -241,7 +247,7 @@ desktop: mk-ui-header-nav: home: "Home" messaging: "Messages" - channels: "Channels" + ch: "Channels" info: "News" mk-ui-header-search: @@ -352,10 +358,6 @@ desktop: mk-repost-form-window: title: "Are you sure you want to repost this post?" - mk-channels-page: - new: "Create new channel" - channel-title: "Channel title" - mobile: tags: mk-drive-file-viewer: @@ -496,6 +498,7 @@ mobile: home: "Home" notifications: "Notifications" messaging: "Messages" + ch: "Channels" drive: "Drive" settings: "Settings" about: "About Misskey" diff --git a/locales/ja.yml b/locales/ja.yml index dd76a2b90..9fd7d94f0 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -164,6 +164,12 @@ common: mk-uploader: waiting: "待機中" +ch: + tags: + mk-index: + new: "チャンネルを作成" + channel-title: "チャンネルのタイトル" + desktop: tags: mk-api-info: @@ -241,7 +247,7 @@ desktop: mk-ui-header-nav: home: "ホーム" messaging: "メッセージ" - channels: "チャンネル" + ch: "チャンネル" info: "お知らせ" mk-ui-header-search: @@ -352,10 +358,6 @@ desktop: mk-repost-form-window: title: "この投稿をRepostしますか?" - mk-channels-page: - new: "チャンネルを作成" - channel-title: "チャンネルのタイトル" - mobile: tags: mk-drive-file-viewer: @@ -496,6 +498,7 @@ mobile: home: "ホーム" notifications: "通知" messaging: "メッセージ" + ch: "チャンネル" search: "検索" drive: "ドライブ" settings: "設定" diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index 183cabf13..34265dcbc 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -13,7 +13,7 @@ import Watching from '../../models/post-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'; /** @@ -258,6 +258,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Publish event to myself's stream event(user._id, 'post', postObj); + // Publish event to channel + if (channel) { + publishChannelStream(channel._id, 'post', postObj); + } + // Fetch all followers const followers = await Following .find({ diff --git a/src/api/event.ts b/src/api/event.ts index 9613a9f7c..909b0d255 100644 --- a/src/api/event.ts +++ b/src/api/event.ts @@ -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); diff --git a/src/api/stream/channel.ts b/src/api/stream/channel.ts new file mode 100644 index 000000000..d67d77cbf --- /dev/null +++ b/src/api/stream/channel.ts @@ -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); + }); +} diff --git a/src/api/streaming.ts b/src/api/streaming.ts index db600013b..0e512fb21 100644 --- a/src/api/streaming.ts +++ b/src/api/streaming.ts @@ -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 : diff --git a/src/config.ts b/src/config.ts index 46a93f5fe..18017e974 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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}`; diff --git a/src/web/app/ch/router.js b/src/web/app/ch/router.js new file mode 100644 index 000000000..424158f40 --- /dev/null +++ b/src/web/app/ch/router.js @@ -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]; +} diff --git a/src/web/app/ch/script.js b/src/web/app/ch/script.js new file mode 100644 index 000000000..760d405c5 --- /dev/null +++ b/src/web/app/ch/script.js @@ -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); +}); diff --git a/src/web/app/ch/style.styl b/src/web/app/ch/style.styl new file mode 100644 index 000000000..2fc3ac3fc --- /dev/null +++ b/src/web/app/ch/style.styl @@ -0,0 +1,4 @@ +@import "../base" + +html + background #efefef diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/ch/tags/channel.tag similarity index 76% rename from src/web/app/desktop/tags/pages/channel.tag rename to src/web/app/ch/tags/channel.tag index a14c0648c..b16844b8b 100644 --- a/src/web/app/desktop/tags/pages/channel.tag +++ b/src/web/app/ch/tags/channel.tag @@ -1,14 +1,19 @@ -<mk-channel-page> - <mk-ui ref="ui"> - <main if={ !parent.fetching }> - <h1>{ parent.channel.title }</h1> - <virtual if={ parent.posts }> - <mk-channel-post each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/> - </virtual> - <hr> - <mk-channel-form channel={ parent.channel } ref="form"/> - </main> - </mk-ui> +<mk-channel> + <main if={ !fetching }> + <h1>{ channel.title }</h1> + <virtual if={ posts }> + <mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/> + </virtual> + <hr> + <mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/> + <div if={ !SIGNIN }> + <p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p> + </div> + <hr> + <footer> + <small>Misskey ver { version } (葵 aoi)</small> + </footer> + </main> <style> :scope display block @@ -20,16 +25,18 @@ color #f00 </style> <script> - import Progress from '../../../common/scripts/loading'; - import ChannelStream from '../../../common/scripts/channel-stream'; + 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.channel = null; this.posts = null; - this.connection = new ChannelStream(); + this.connection = new ChannelStream(this.id); + this.version = VERSION; this.on('mount', () => { document.documentElement.style.background = '#efefef'; @@ -56,9 +63,22 @@ posts: posts }); }); + + this.connection.on('post', this.onPost); }); + + this.on('unmount', () => { + this.connection.off('post', this.onPost); + this.connection.close(); + }); + + this.onPost = post => { + this.posts.unshift(post); + this.update(); + }; + </script> -</mk-channel-page> +</mk-channel> <mk-channel-post> <header> @@ -127,7 +147,7 @@ </style> <script> - import CONFIG from '../../../common/scripts/config'; + import CONFIG from '../../common/scripts/config'; this.mixin('api'); diff --git a/src/web/app/ch/tags/index.js b/src/web/app/ch/tags/index.js new file mode 100644 index 000000000..1e99ccd43 --- /dev/null +++ b/src/web/app/ch/tags/index.js @@ -0,0 +1,2 @@ +require('./index.tag'); +require('./channel.tag'); diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag new file mode 100644 index 000000000..1c0a037c2 --- /dev/null +++ b/src/web/app/ch/tags/index.tag @@ -0,0 +1,24 @@ +<mk-index> + <button onclick={ new }>%i18n:ch.tags.mk-index.new%</button> + <style> + :scope + display block + + </style> + <script> + this.mixin('api'); + + this.on('mount', () => { + }); + + this.new = () => { + 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> diff --git a/src/web/app/common/scripts/channel-stream.js b/src/web/app/common/scripts/channel-stream.js index 38e7d9113..17944dbe4 100644 --- a/src/web/app/common/scripts/channel-stream.js +++ b/src/web/app/common/scripts/channel-stream.js @@ -6,8 +6,10 @@ import Stream from './stream'; * Channel stream connection */ class Connection extends Stream { - constructor() { - super('channel'); + constructor(channelId) { + super('channel', { + channel: channelId + }); } } diff --git a/src/web/app/common/scripts/config.js b/src/web/app/common/scripts/config.js index 75a7abba2..c5015622f 100644 --- a/src/web/app/common/scripts/config.js +++ b/src/web/app/common/scripts/config.js @@ -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, diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js index df67bb7b7..977e3fa9a 100644 --- a/src/web/app/desktop/router.js +++ b/src/web/app/desktop/router.js @@ -10,8 +10,6 @@ export default me => { route('/', index); route('/selectdrive', selectDrive); route('/i>mentions', mentions); - route('/channel', channels); - route('/channel/:channel', channel); route('/post::post', post); route('/search::query', search); route('/:user', user.bind(null, 'home')); @@ -57,16 +55,6 @@ export default me => { mount(el); } - function channel(ctx) { - const el = document.createElement('mk-channel-page'); - el.setAttribute('id', ctx.params.channel); - mount(el); - } - - function channels() { - mount(document.createElement('mk-channels-page')); - } - function selectDrive() { mount(document.createElement('mk-selectdrive-page')); } diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index 0b92d8c23..37fdfe37e 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.js @@ -61,8 +61,6 @@ require('./pages/user.tag'); require('./pages/post.tag'); require('./pages/search.tag'); require('./pages/not-found.tag'); -require('./pages/channel.tag'); -require('./pages/channels.tag'); require('./pages/selectdrive.tag'); require('./autocomplete-suggestion.tag'); require('./progress-dialog.tag'); diff --git a/src/web/app/desktop/tags/pages/channels.tag b/src/web/app/desktop/tags/pages/channels.tag deleted file mode 100644 index 220f1ca50..000000000 --- a/src/web/app/desktop/tags/pages/channels.tag +++ /dev/null @@ -1,28 +0,0 @@ -<mk-channels-page> - <mk-ui ref="ui"> - <main> - <button onclick={ parent.new }>%i18n:desktop.tags.mk-channels-page.new%</button> - </main> - </mk-ui> - <style> - :scope - display block - - </style> - <script> - this.mixin('api'); - - this.on('mount', () => { - }); - - this.new = () => { - const title = window.prompt('%i18n:desktop.tags.mk-channels-page.channel-title%'); - - this.api('channels/create', { - title: title - }).then(channel => { - location.href = '/channel/' + channel.id; - }); - }; - </script> -</mk-channels-page> diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag index 17b2c66dc..64b64f902 100644 --- a/src/web/app/desktop/tags/timeline.tag +++ b/src/web/app/desktop/tags/timeline.tag @@ -112,7 +112,7 @@ </header> <div class="body"> <div class="text" ref="text"> - <p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p> + <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_to }> <i class="fa fa-reply"></i> </a> diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index 7527358dc..3123c34f4 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -335,10 +335,10 @@ </a> </li> </virtual> - <li class="channels"> - <a href={ CONFIG.url + '/channel' }> + <li class="ch"> + <a href={ CONFIG.chUrl } target="_blank"> <i class="fa fa-television"></i> - <p>%i18n:desktop.tags.mk-ui-header-nav.channels%</p> + <p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p> </a> </li> <li class="info"> diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag index b26a5cb10..ad18521df 100644 --- a/src/web/app/mobile/tags/timeline.tag +++ b/src/web/app/mobile/tags/timeline.tag @@ -164,7 +164,7 @@ </header> <div class="body"> <div class="text" ref="text"> - <p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p> + <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_to }> <i class="fa fa-reply"></i> </a> diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag index fb8cbcdbd..b2d96f6b8 100644 --- a/src/web/app/mobile/tags/ui.tag +++ b/src/web/app/mobile/tags/ui.tag @@ -231,10 +231,11 @@ <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> + <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 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> + <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> diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts index 5199285d5..066df1815 100644 --- a/webpack/webpack.config.ts +++ b/webpack/webpack.config.ts @@ -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', From 3c4719a0b119c78108edeff2ecf7965f1c517237 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 03:31:36 +0900 Subject: [PATCH 11/16] wip --- src/web/app/ch/tags/channel.tag | 2 +- src/web/app/mobile/router.js | 5 ++ src/web/app/mobile/tags/drive.tag | 6 +- src/web/app/mobile/tags/index.js | 1 + src/web/app/mobile/tags/page/selectdrive.tag | 83 ++++++++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/web/app/mobile/tags/page/selectdrive.tag diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag index b16844b8b..d43113a55 100644 --- a/src/web/app/ch/tags/channel.tag +++ b/src/web/app/ch/tags/channel.tag @@ -11,7 +11,7 @@ </div> <hr> <footer> - <small>Misskey ver { version } (葵 aoi)</small> + <small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small> </footer> </main> <style> diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js index d59b2ec3a..01eb3c814 100644 --- a/src/web/app/mobile/router.js +++ b/src/web/app/mobile/router.js @@ -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')); } diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag index 9f3e64773..c17b7ce57 100644 --- a/src/web/app/mobile/tags/drive.tag +++ b/src/web/app/mobile/tags/drive.tag @@ -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; diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.js index a79f4f7e7..19952c20c 100644 --- a/src/web/app/mobile/tags/index.js +++ b/src/web/app/mobile/tags/index.js @@ -19,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'); diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag new file mode 100644 index 000000000..d9e7d95c4 --- /dev/null +++ b/src/web/app/mobile/tags/page/selectdrive.tag @@ -0,0 +1,83 @@ +<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 }/> + + <style> + :scope + display block + width 100% + height 100% + background #fff + + > header + border-bottom solid 1px #eee + + > 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 + height calc(100% - 42px) + overflow scroll + -webkit-overflow-scrolling touch + + </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> From 20707d6fd9ce2dea1342ad38156c32fcec82217a Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 03:41:34 +0900 Subject: [PATCH 12/16] wip --- locales/en.yml | 3 +++ locales/ja.yml | 3 +++ src/web/app/ch/tags/channel.tag | 24 +++++++++++++++++++++--- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 643649b46..afb6d2f2f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -360,6 +360,9 @@ desktop: mobile: tags: + mk-selectdrive-page: + select-file: "Select file(s)" + mk-drive-file-viewer: download: "Download" rename: "Rename" diff --git a/locales/ja.yml b/locales/ja.yml index 9fd7d94f0..03975556b 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -360,6 +360,9 @@ desktop: mobile: tags: + mk-selectdrive-page: + select-file: "ファイルを選択" + mk-drive-file-viewer: download: "ダウンロード" rename: "名前を変更" diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag index d43113a55..e8537e3f0 100644 --- a/src/web/app/ch/tags/channel.tag +++ b/src/web/app/ch/tags/channel.tag @@ -1,9 +1,13 @@ <mk-channel> <main if={ !fetching }> <h1>{ channel.title }</h1> - <virtual if={ posts }> - <mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/> - </virtual> + <p if={ postsFetching }>読み込み中<mk-ellipsis/></p> + <div if={ !postsFetching }> + <p if={ posts == null }></p>> + <virtual if={ posts != null }> + <mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/> + </virtual> + </div> <hr> <mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/> <div if={ !SIGNIN }> @@ -33,6 +37,7 @@ this.id = this.opts.id; this.fetching = true; + this.postsFetching = true; this.channel = null; this.posts = null; this.connection = new ChannelStream(this.id); @@ -60,6 +65,7 @@ channel_id: this.id }).then(posts => { this.update({ + postsFetching: false, posts: posts }); }); @@ -84,6 +90,7 @@ <header> <a class="index" onclick={ reply }>{ post.index }:</a> <a class="name" href={ '/' + 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> @@ -114,6 +121,17 @@ > 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 From 0cffc1cac0140a420c64e039487c32237c581d5e Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 03:42:50 +0900 Subject: [PATCH 13/16] wip --- src/web/app/ch/tags/channel.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag index e8537e3f0..43a1f851f 100644 --- a/src/web/app/ch/tags/channel.tag +++ b/src/web/app/ch/tags/channel.tag @@ -3,7 +3,7 @@ <h1>{ channel.title }</h1> <p if={ postsFetching }>読み込み中<mk-ellipsis/></p> <div if={ !postsFetching }> - <p if={ posts == null }></p>> + <p if={ posts == null }>まだ投稿がありません</p> <virtual if={ posts != null }> <mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/> </virtual> From 42be937fcb6f02037ff4024a2fb1cf463c50ce0c Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 04:11:56 +0900 Subject: [PATCH 14/16] wip --- src/api/endpoints.ts | 3 ++ src/api/endpoints/channels.ts | 59 +++++++++++++++++++++++++++++++++ src/web/app/ch/tags/channel.tag | 7 ++-- src/web/app/ch/tags/index.tag | 13 ++++++-- 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 src/api/endpoints/channels.ts diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 88c01d4e7..c4dacad85 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -490,6 +490,9 @@ const endpoints: Endpoint[] = [ { name: 'channels/posts' }, + { + name: 'channels' + }, ]; export default endpoints; diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts new file mode 100644 index 000000000..e10c94389 --- /dev/null +++ b/src/api/endpoints/channels.ts @@ -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)))); +}); diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag index 43a1f851f..12a6b5a3b 100644 --- a/src/web/app/ch/tags/channel.tag +++ b/src/web/app/ch/tags/channel.tag @@ -1,4 +1,6 @@ <mk-channel> + <header><a href={ CONFIG.chUrl }>Misskey Channels</a></header> + <hr> <main if={ !fetching }> <h1>{ channel.title }</h1> <p if={ postsFetching }>読み込み中<mk-ellipsis/></p> @@ -21,10 +23,9 @@ <style> :scope display block + padding 8px - main - padding 8px - + > main > h1 color #f00 </style> diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag index 1c0a037c2..a64ddb6cc 100644 --- a/src/web/app/ch/tags/index.tag +++ b/src/web/app/ch/tags/index.tag @@ -1,5 +1,9 @@ <mk-index> - <button onclick={ new }>%i18n:ch.tags.mk-index.new%</button> + <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 @@ -9,9 +13,14 @@ this.mixin('api'); this.on('mount', () => { + this.api('channels').then(channels => { + this.update({ + channels: channels + }); + }); }); - this.new = () => { + this.n = () => { const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%'); this.api('channels/create', { From 6f242f229a48eeb97e8d43f9c75b35c172f6e4b1 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 04:16:16 +0900 Subject: [PATCH 15/16] :v: --- src/common/get-post-summary.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/get-post-summary.ts b/src/common/get-post-summary.ts index f628a32b4..ac15077b2 100644 --- a/src/common/get-post-summary.ts +++ b/src/common/get-post-summary.ts @@ -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) { From 92cd2265b17898201a1e45c89e82c321b78e5018 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2017 04:18:02 +0900 Subject: [PATCH 16/16] v2769 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f75462e5..4bf0f6abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ChangeLog (Release Notes) ========================= 主に notable な changes を書いていきます +2769 (2017/11/01) +----------------- +* New: チャンネルシステム + 2752 (2017/10/30) ----------------- * New: 未読の通知がある場合アイコンを表示するように diff --git a/package.json b/package.json index 7a81bed7a..57b3439d6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "0.0.2752", + "version": "0.0.2769", "license": "MIT", "description": "A miniblog-based SNS", "bugs": "https://github.com/syuilo/misskey/issues",