diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f75462e5f..4bf0f6abc1 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/docs/setup.en.md b/docs/setup.en.md index 3e48935346..dbc0599b5a 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 4f48a08088..602fd9b6a1 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 020813ddbb..afb6d2f2fb 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,6 +247,7 @@ desktop: mk-ui-header-nav: home: "Home" messaging: "Messages" + ch: "Channels" info: "News" mk-ui-header-search: @@ -353,6 +360,9 @@ desktop: mobile: tags: + mk-selectdrive-page: + select-file: "Select file(s)" + mk-drive-file-viewer: download: "Download" rename: "Rename" @@ -491,6 +501,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 1b3058fe02..03975556b5 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,6 +247,7 @@ desktop: mk-ui-header-nav: home: "ホーム" messaging: "メッセージ" + ch: "チャンネル" info: "お知らせ" mk-ui-header-search: @@ -353,6 +360,9 @@ desktop: mobile: tags: + mk-selectdrive-page: + select-file: "ファイルを選択" + mk-drive-file-viewer: download: "ダウンロード" rename: "名前を変更" @@ -491,6 +501,7 @@ mobile: home: "ホーム" notifications: "通知" messaging: "メッセージ" + ch: "チャンネル" search: "検索" drive: "ドライブ" settings: "設定" diff --git a/package.json b/package.json index 7a81bed7a6..57b3439d65 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo ", - "version": "0.0.2752", + "version": "0.0.2769", "license": "MIT", "description": "A miniblog-based SNS", "bugs": "https://github.com/syuilo/misskey/issues", diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 29a97bcb8a..c4dacad857 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -474,8 +474,25 @@ 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' + }, ]; export default endpoints; diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts new file mode 100644 index 0000000000..e10c943896 --- /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} + */ +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/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts new file mode 100644 index 0000000000..e0c0e0192a --- /dev/null +++ b/src/api/endpoints/channels/create.ts @@ -0,0 +1,30 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import Channel from '../../models/channel'; +import serialize from '../../serializers/channel'; + +/** + * Create a channel + * + * @param {any} params + * @param {any} user + * @return {Promise} + */ +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 + }); + + // Response + res(await serialize(channel)); +}); diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts new file mode 100644 index 0000000000..fa91fb93ee --- /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} + */ +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/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts new file mode 100644 index 0000000000..8861e54594 --- /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} + */ +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/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index 805dba7f83..34265dcbc3 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -4,16 +4,16 @@ 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 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 +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,47 @@ 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をすることはできません'); + } + } else { + // 返信対象の投稿がチャンネルへの投稿だったらダメ + if (inReplyToPost && inReplyToPost.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') @@ -152,11 +196,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'); } } @@ -164,6 +208,8 @@ 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, @@ -182,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 @@ -206,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 9613a9f7cc..909b0d2556 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/models/channel.ts b/src/api/models/channel.ts new file mode 100644 index 0000000000..c80e84dbc8 --- /dev/null +++ b/src/api/models/channel.ts @@ -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; +}; diff --git a/src/api/models/post.ts b/src/api/models/post.ts index 8b9f7f5ef6..fe07dcb0b1 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; diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts new file mode 100644 index 0000000000..d4e16d6be3 --- /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(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/api/serializers/post.ts b/src/api/serializers/post.ts index df917a8595..7d40df2d6a 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/api/stream/channel.ts b/src/api/stream/channel.ts new file mode 100644 index 0000000000..d67d77cbf4 --- /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 db600013b9..0e512fb210 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/common/get-post-summary.ts b/src/common/get-post-summary.ts index f628a32b41..ac15077b28 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) { diff --git a/src/config.ts b/src/config.ts index 46a93f5fef..18017e9740 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 0000000000..424158f403 --- /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 0000000000..760d405c52 --- /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 0000000000..2fc3ac3fca --- /dev/null +++ b/src/web/app/ch/style.styl @@ -0,0 +1,4 @@ +@import "../base" + +html + background #efefef diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag new file mode 100644 index 0000000000..12a6b5a3b9 --- /dev/null +++ b/src/web/app/ch/tags/channel.tag @@ -0,0 +1,223 @@ + +
Misskey Channels
+
+
+

{ channel.title }

+

読み込み中

+
+

まだ投稿がありません

+ + + +
+
+ +
+

参加するにはログインまたは新規登録してください

+
+
+
+ Misskey ver { version } (葵 aoi) +
+
+ + +
+ + +
+ { post.index }: + { post.user.name } + + + ID:{ post.user.username } +
+
+ >>{ post.reply_to.index } + { post.text } +
+ + { + +
+
+ + +
+ + +

>>{ reply.index } ({ reply.user.name }): [x]

+ + +
+ +
    +
  1. { name }
  2. +
+ + +
diff --git a/src/web/app/ch/tags/index.js b/src/web/app/ch/tags/index.js new file mode 100644 index 0000000000..1e99ccd43e --- /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 0000000000..a64ddb6ccd --- /dev/null +++ b/src/web/app/ch/tags/index.tag @@ -0,0 +1,33 @@ + + +
+ + + +
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 0000000000..17944dbe45 --- /dev/null +++ b/src/web/app/common/scripts/channel-stream.js @@ -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; diff --git a/src/web/app/common/scripts/config.js b/src/web/app/common/scripts/config.js index 75a7abba29..c5015622f0 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 afa8a2dce3..977e3fa9a6 100644 --- a/src/web/app/desktop/router.js +++ b/src/web/app/desktop/router.js @@ -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'); diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index 4e286013a1..37fdfe37e4 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/selectdrive.tag'); require('./autocomplete-suggestion.tag'); require('./progress-dialog.tag'); require('./user-preview.tag'); 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 0000000000..b196357d85 --- /dev/null +++ b/src/web/app/desktop/tags/pages/selectdrive.tag @@ -0,0 +1,159 @@ + + +
+ + + +
+ + + +
diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag index 864fe22735..811ca5c0fd 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', () => { diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag index 2d6b439e38..64b64f902f 100644 --- a/src/web/app/desktop/tags/timeline.tag +++ b/src/web/app/desktop/tags/timeline.tag @@ -112,6 +112,7 @@
+

{ p.channel.title }:

@@ -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/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index e0d7393b08..3123c34f4f 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -319,18 +319,26 @@ -
    -
  • - - -

    %i18n:desktop.tags.mk-ui-header-nav.home%

    -
    -
  • -
  • - - -

    %i18n:desktop.tags.mk-ui-header-nav.messaging%

    - +
      + +
    • + + +

      %i18n:desktop.tags.mk-ui-header-nav.home%

      +
      +
    • +
    • + + +

      %i18n:desktop.tags.mk-ui-header-nav.messaging%

      + +
      +
    • + +
    • + + +

      %i18n:desktop.tags.mk-ui-header-nav.ch%

    • diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js index d59b2ec3a1..01eb3c8145 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 9f3e647735..c17b7ce579 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 a79f4f7e7e..19952c20cd 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 0000000000..d9e7d95c41 --- /dev/null +++ b/src/web/app/mobile/tags/page/selectdrive.tag @@ -0,0 +1,83 @@ + +
      +

      %i18n:mobile.tags.mk-selectdrive-page.select-file% 0 }>({ files.length })

      + + +
      + + + + +
      diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag index c7f5bfd681..ad18521df6 100644 --- a/src/web/app/mobile/tags/timeline.tag +++ b/src/web/app/mobile/tags/timeline.tag @@ -164,6 +164,7 @@
      +

      { p.channel.title }:

      @@ -373,6 +374,9 @@ mk-url-preview margin-top 8px + > .channel + margin 0 + > .reply margin-right 8px color #717171 diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag index fb8cbcdbd2..b2d96f6b8b 100644 --- a/src/web/app/mobile/tags/ui.tag +++ b/src/web/app/mobile/tags/ui.tag @@ -231,10 +231,11 @@
    • %i18n:mobile.tags.mk-ui-nav.messaging%
    • %i18n:mobile.tags.mk-ui-nav.settings%
    • diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts index 5199285d55..066df18157 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',