commit
2a00930150
42 changed files with 1054 additions and 49 deletions
|
@ -2,6 +2,10 @@ ChangeLog (Release Notes)
|
||||||
=========================
|
=========================
|
||||||
主に notable な changes を書いていきます
|
主に notable な changes を書いていきます
|
||||||
|
|
||||||
|
2769 (2017/11/01)
|
||||||
|
-----------------
|
||||||
|
* New: チャンネルシステム
|
||||||
|
|
||||||
2752 (2017/10/30)
|
2752 (2017/10/30)
|
||||||
-----------------
|
-----------------
|
||||||
* New: 未読の通知がある場合アイコンを表示するように
|
* New: 未読の通知がある場合アイコンを表示するように
|
||||||
|
|
|
@ -25,6 +25,7 @@ Note that Misskey uses following subdomains:
|
||||||
* **api**.*{primary domain}*
|
* **api**.*{primary domain}*
|
||||||
* **auth**.*{primary domain}*
|
* **auth**.*{primary domain}*
|
||||||
* **about**.*{primary domain}*
|
* **about**.*{primary domain}*
|
||||||
|
* **ch**.*{primary domain}*
|
||||||
* **stats**.*{primary domain}*
|
* **stats**.*{primary domain}*
|
||||||
* **status**.*{primary domain}*
|
* **status**.*{primary domain}*
|
||||||
* **dev**.*{primary domain}*
|
* **dev**.*{primary domain}*
|
||||||
|
|
|
@ -26,6 +26,7 @@ Misskeyは以下のサブドメインを使います:
|
||||||
* **api**.*{primary domain}*
|
* **api**.*{primary domain}*
|
||||||
* **auth**.*{primary domain}*
|
* **auth**.*{primary domain}*
|
||||||
* **about**.*{primary domain}*
|
* **about**.*{primary domain}*
|
||||||
|
* **ch**.*{primary domain}*
|
||||||
* **stats**.*{primary domain}*
|
* **stats**.*{primary domain}*
|
||||||
* **status**.*{primary domain}*
|
* **status**.*{primary domain}*
|
||||||
* **dev**.*{primary domain}*
|
* **dev**.*{primary domain}*
|
||||||
|
|
|
@ -164,6 +164,12 @@ common:
|
||||||
mk-uploader:
|
mk-uploader:
|
||||||
waiting: "Waiting"
|
waiting: "Waiting"
|
||||||
|
|
||||||
|
ch:
|
||||||
|
tags:
|
||||||
|
mk-index:
|
||||||
|
new: "Create new channel"
|
||||||
|
channel-title: "Channel title"
|
||||||
|
|
||||||
desktop:
|
desktop:
|
||||||
tags:
|
tags:
|
||||||
mk-api-info:
|
mk-api-info:
|
||||||
|
@ -241,6 +247,7 @@ desktop:
|
||||||
mk-ui-header-nav:
|
mk-ui-header-nav:
|
||||||
home: "Home"
|
home: "Home"
|
||||||
messaging: "Messages"
|
messaging: "Messages"
|
||||||
|
ch: "Channels"
|
||||||
info: "News"
|
info: "News"
|
||||||
|
|
||||||
mk-ui-header-search:
|
mk-ui-header-search:
|
||||||
|
@ -353,6 +360,9 @@ desktop:
|
||||||
|
|
||||||
mobile:
|
mobile:
|
||||||
tags:
|
tags:
|
||||||
|
mk-selectdrive-page:
|
||||||
|
select-file: "Select file(s)"
|
||||||
|
|
||||||
mk-drive-file-viewer:
|
mk-drive-file-viewer:
|
||||||
download: "Download"
|
download: "Download"
|
||||||
rename: "Rename"
|
rename: "Rename"
|
||||||
|
@ -491,6 +501,7 @@ mobile:
|
||||||
home: "Home"
|
home: "Home"
|
||||||
notifications: "Notifications"
|
notifications: "Notifications"
|
||||||
messaging: "Messages"
|
messaging: "Messages"
|
||||||
|
ch: "Channels"
|
||||||
drive: "Drive"
|
drive: "Drive"
|
||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
about: "About Misskey"
|
about: "About Misskey"
|
||||||
|
|
|
@ -164,6 +164,12 @@ common:
|
||||||
mk-uploader:
|
mk-uploader:
|
||||||
waiting: "待機中"
|
waiting: "待機中"
|
||||||
|
|
||||||
|
ch:
|
||||||
|
tags:
|
||||||
|
mk-index:
|
||||||
|
new: "チャンネルを作成"
|
||||||
|
channel-title: "チャンネルのタイトル"
|
||||||
|
|
||||||
desktop:
|
desktop:
|
||||||
tags:
|
tags:
|
||||||
mk-api-info:
|
mk-api-info:
|
||||||
|
@ -241,6 +247,7 @@ desktop:
|
||||||
mk-ui-header-nav:
|
mk-ui-header-nav:
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
messaging: "メッセージ"
|
messaging: "メッセージ"
|
||||||
|
ch: "チャンネル"
|
||||||
info: "お知らせ"
|
info: "お知らせ"
|
||||||
|
|
||||||
mk-ui-header-search:
|
mk-ui-header-search:
|
||||||
|
@ -353,6 +360,9 @@ desktop:
|
||||||
|
|
||||||
mobile:
|
mobile:
|
||||||
tags:
|
tags:
|
||||||
|
mk-selectdrive-page:
|
||||||
|
select-file: "ファイルを選択"
|
||||||
|
|
||||||
mk-drive-file-viewer:
|
mk-drive-file-viewer:
|
||||||
download: "ダウンロード"
|
download: "ダウンロード"
|
||||||
rename: "名前を変更"
|
rename: "名前を変更"
|
||||||
|
@ -491,6 +501,7 @@ mobile:
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
messaging: "メッセージ"
|
messaging: "メッセージ"
|
||||||
|
ch: "チャンネル"
|
||||||
search: "検索"
|
search: "検索"
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "0.0.2752",
|
"version": "0.0.2769",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "A miniblog-based SNS",
|
"description": "A miniblog-based SNS",
|
||||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||||
|
|
|
@ -474,8 +474,25 @@ const endpoints: Endpoint[] = [
|
||||||
name: 'messaging/messages/create',
|
name: 'messaging/messages/create',
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
kind: 'messaging-write'
|
kind: 'messaging-write'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'channels/create',
|
||||||
|
withCredential: true,
|
||||||
|
limit: {
|
||||||
|
duration: ms('1hour'),
|
||||||
|
max: 3,
|
||||||
|
minInterval: ms('10seconds')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'channels/show'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'channels/posts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'channels'
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default endpoints;
|
export default endpoints;
|
||||||
|
|
59
src/api/endpoints/channels.ts
Normal file
59
src/api/endpoints/channels.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
import $ from 'cafy';
|
||||||
|
import Channel from '../models/channel';
|
||||||
|
import serialize from '../serializers/channel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all channels
|
||||||
|
*
|
||||||
|
* @param {any} params
|
||||||
|
* @param {any} me
|
||||||
|
* @return {Promise<any>}
|
||||||
|
*/
|
||||||
|
module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'limit' parameter
|
||||||
|
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||||
|
if (limitErr) return rej('invalid limit param');
|
||||||
|
|
||||||
|
// Get 'since_id' parameter
|
||||||
|
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||||
|
if (sinceIdErr) return rej('invalid since_id param');
|
||||||
|
|
||||||
|
// Get 'max_id' parameter
|
||||||
|
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||||
|
if (maxIdErr) return rej('invalid max_id param');
|
||||||
|
|
||||||
|
// Check if both of since_id and max_id is specified
|
||||||
|
if (sinceId && maxId) {
|
||||||
|
return rej('cannot set since_id and max_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct query
|
||||||
|
const sort = {
|
||||||
|
_id: -1
|
||||||
|
};
|
||||||
|
const query = {} as any;
|
||||||
|
if (sinceId) {
|
||||||
|
sort._id = 1;
|
||||||
|
query._id = {
|
||||||
|
$gt: sinceId
|
||||||
|
};
|
||||||
|
} else if (maxId) {
|
||||||
|
query._id = {
|
||||||
|
$lt: maxId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue query
|
||||||
|
const channels = await Channel
|
||||||
|
.find(query, {
|
||||||
|
limit: limit,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serialize
|
||||||
|
res(await Promise.all(channels.map(async channel =>
|
||||||
|
await serialize(channel, me))));
|
||||||
|
});
|
30
src/api/endpoints/channels/create.ts
Normal file
30
src/api/endpoints/channels/create.ts
Normal file
|
@ -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<any>}
|
||||||
|
*/
|
||||||
|
module.exports = async (params, user) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'title' parameter
|
||||||
|
const [title, titleErr] = $(params.title).string().range(1, 100).$;
|
||||||
|
if (titleErr) return rej('invalid title param');
|
||||||
|
|
||||||
|
// Create a channel
|
||||||
|
const channel = await Channel.insert({
|
||||||
|
created_at: new Date(),
|
||||||
|
user_id: user._id,
|
||||||
|
title: title,
|
||||||
|
index: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response
|
||||||
|
res(await serialize(channel));
|
||||||
|
});
|
79
src/api/endpoints/channels/posts.ts
Normal file
79
src/api/endpoints/channels/posts.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
import $ from 'cafy';
|
||||||
|
import { default as Channel, IChannel } from '../../models/channel';
|
||||||
|
import { default as Post, IPost } from '../../models/post';
|
||||||
|
import serialize from '../../serializers/post';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a posts of a channel
|
||||||
|
*
|
||||||
|
* @param {any} params
|
||||||
|
* @param {any} user
|
||||||
|
* @return {Promise<any>}
|
||||||
|
*/
|
||||||
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'limit' parameter
|
||||||
|
const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$;
|
||||||
|
if (limitErr) return rej('invalid limit param');
|
||||||
|
|
||||||
|
// Get 'since_id' parameter
|
||||||
|
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||||
|
if (sinceIdErr) return rej('invalid since_id param');
|
||||||
|
|
||||||
|
// Get 'max_id' parameter
|
||||||
|
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||||
|
if (maxIdErr) return rej('invalid max_id param');
|
||||||
|
|
||||||
|
// Check if both of since_id and max_id is specified
|
||||||
|
if (sinceId && maxId) {
|
||||||
|
return rej('cannot set since_id and max_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 'channel_id' parameter
|
||||||
|
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||||
|
if (channelIdErr) return rej('invalid channel_id param');
|
||||||
|
|
||||||
|
// Fetch channel
|
||||||
|
const channel: IChannel = await Channel.findOne({
|
||||||
|
_id: channelId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channel === null) {
|
||||||
|
return rej('channel not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region Construct query
|
||||||
|
const sort = {
|
||||||
|
_id: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
channel_id: channel._id
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
if (sinceId) {
|
||||||
|
sort._id = 1;
|
||||||
|
query._id = {
|
||||||
|
$gt: sinceId
|
||||||
|
};
|
||||||
|
} else if (maxId) {
|
||||||
|
query._id = {
|
||||||
|
$lt: maxId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//#endregion Construct query
|
||||||
|
|
||||||
|
// Issue query
|
||||||
|
const posts = await Post
|
||||||
|
.find(query, {
|
||||||
|
limit: limit,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serialize
|
||||||
|
res(await Promise.all(posts.map(async (post) =>
|
||||||
|
await serialize(post, user)
|
||||||
|
)));
|
||||||
|
});
|
31
src/api/endpoints/channels/show.ts
Normal file
31
src/api/endpoints/channels/show.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
import $ from 'cafy';
|
||||||
|
import { default as Channel, IChannel } from '../../models/channel';
|
||||||
|
import serialize from '../../serializers/channel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a channel
|
||||||
|
*
|
||||||
|
* @param {any} params
|
||||||
|
* @param {any} user
|
||||||
|
* @return {Promise<any>}
|
||||||
|
*/
|
||||||
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'channel_id' parameter
|
||||||
|
const [channelId, channelIdErr] = $(params.channel_id).id().$;
|
||||||
|
if (channelIdErr) return rej('invalid channel_id param');
|
||||||
|
|
||||||
|
// Fetch channel
|
||||||
|
const channel: IChannel = await Channel.findOne({
|
||||||
|
_id: channelId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channel === null) {
|
||||||
|
return rej('channel not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize
|
||||||
|
res(await serialize(channel, user));
|
||||||
|
});
|
|
@ -4,16 +4,16 @@
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import deepEqual = require('deep-equal');
|
import deepEqual = require('deep-equal');
|
||||||
import parse from '../../common/text';
|
import parse from '../../common/text';
|
||||||
import Post from '../../models/post';
|
import { default as Post, IPost, isValidText } from '../../models/post';
|
||||||
import { isValidText } from '../../models/post';
|
|
||||||
import { default as User, IUser } from '../../models/user';
|
import { default as User, IUser } from '../../models/user';
|
||||||
|
import { default as Channel, IChannel } from '../../models/channel';
|
||||||
import Following from '../../models/following';
|
import Following from '../../models/following';
|
||||||
import DriveFile from '../../models/drive-file';
|
import DriveFile from '../../models/drive-file';
|
||||||
import Watching from '../../models/post-watching';
|
import Watching from '../../models/post-watching';
|
||||||
import serialize from '../../serializers/post';
|
import serialize from '../../serializers/post';
|
||||||
import notify from '../../common/notify';
|
import notify from '../../common/notify';
|
||||||
import watch from '../../common/watch-post';
|
import watch from '../../common/watch-post';
|
||||||
import event from '../../event';
|
import { default as event, publishChannelStream } from '../../event';
|
||||||
import config from '../../../conf';
|
import config from '../../../conf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +62,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
|
const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
|
||||||
if (repostIdErr) return rej('invalid repost_id');
|
if (repostIdErr) return rej('invalid repost_id');
|
||||||
|
|
||||||
let repost = null;
|
let repost: IPost = null;
|
||||||
|
let isQuote = false;
|
||||||
if (repostId !== undefined) {
|
if (repostId !== undefined) {
|
||||||
// Fetch repost to post
|
// Fetch repost to post
|
||||||
repost = await Post.findOne({
|
repost = await Post.findOne({
|
||||||
|
@ -84,18 +85,20 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
isQuote = text != null || files != null;
|
||||||
|
|
||||||
// 直近と同じRepost対象かつ引用じゃなかったらエラー
|
// 直近と同じRepost対象かつ引用じゃなかったらエラー
|
||||||
if (latestPost &&
|
if (latestPost &&
|
||||||
latestPost.repost_id &&
|
latestPost.repost_id &&
|
||||||
latestPost.repost_id.equals(repost._id) &&
|
latestPost.repost_id.equals(repost._id) &&
|
||||||
text === undefined && files === null) {
|
!isQuote) {
|
||||||
return rej('cannot repost same post that already reposted in your latest post');
|
return rej('cannot repost same post that already reposted in your latest post');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直近がRepost対象かつ引用じゃなかったらエラー
|
// 直近がRepost対象かつ引用じゃなかったらエラー
|
||||||
if (latestPost &&
|
if (latestPost &&
|
||||||
latestPost._id.equals(repost._id) &&
|
latestPost._id.equals(repost._id) &&
|
||||||
text === undefined && files === null) {
|
!isQuote) {
|
||||||
return rej('cannot repost your latest post');
|
return rej('cannot repost your latest post');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +107,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$;
|
const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$;
|
||||||
if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id');
|
if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id');
|
||||||
|
|
||||||
let inReplyToPost = null;
|
let inReplyToPost: IPost = null;
|
||||||
if (inReplyToPostId !== undefined) {
|
if (inReplyToPostId !== undefined) {
|
||||||
// Fetch reply
|
// Fetch reply
|
||||||
inReplyToPost = await Post.findOne({
|
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
|
// Get 'poll' parameter
|
||||||
const [poll, pollErr] = $(params.poll).optional.strict.object()
|
const [poll, pollErr] = $(params.poll).optional.strict.object()
|
||||||
.have('choices', $().array('string')
|
.have('choices', $().array('string')
|
||||||
|
@ -164,6 +208,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
// 投稿を作成
|
// 投稿を作成
|
||||||
const post = await Post.insert({
|
const post = await Post.insert({
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
|
channel_id: channel ? channel._id : undefined,
|
||||||
|
index: channel ? channel.index + 1 : undefined,
|
||||||
media_ids: files ? files.map(file => file._id) : undefined,
|
media_ids: files ? files.map(file => file._id) : undefined,
|
||||||
reply_to_id: inReplyToPost ? inReplyToPost._id : undefined,
|
reply_to_id: inReplyToPost ? inReplyToPost._id : undefined,
|
||||||
repost_id: repost ? repost._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
|
// Post processes
|
||||||
|
|
||||||
|
Channel.update({ _id: channel._id }, {
|
||||||
|
$inc: {
|
||||||
|
index: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
User.update({ _id: user._id }, {
|
User.update({ _id: user._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
latest_post: post
|
latest_post: post
|
||||||
|
@ -206,6 +258,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
// Publish event to myself's stream
|
// Publish event to myself's stream
|
||||||
event(user._id, 'post', postObj);
|
event(user._id, 'post', postObj);
|
||||||
|
|
||||||
|
// Publish event to channel
|
||||||
|
if (channel) {
|
||||||
|
publishChannelStream(channel._id, 'post', postObj);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch all followers
|
// Fetch all followers
|
||||||
const followers = await Following
|
const followers = await Following
|
||||||
.find({
|
.find({
|
||||||
|
|
|
@ -25,6 +25,10 @@ class MisskeyEvent {
|
||||||
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public publishChannelStream(channelId: ID, type: string, value?: any): void {
|
||||||
|
this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
|
}
|
||||||
|
|
||||||
private publish(channel: string, type: string, value?: any): void {
|
private publish(channel: string, type: string, value?: any): void {
|
||||||
const message = value == null ?
|
const message = value == null ?
|
||||||
{ type: type } :
|
{ type: type } :
|
||||||
|
@ -41,3 +45,5 @@ export default ev.publishUserStream.bind(ev);
|
||||||
export const publishPostStream = ev.publishPostStream.bind(ev);
|
export const publishPostStream = ev.publishPostStream.bind(ev);
|
||||||
|
|
||||||
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
|
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
|
||||||
|
|
||||||
|
export const publishChannelStream = ev.publishChannelStream.bind(ev);
|
||||||
|
|
14
src/api/models/channel.ts
Normal file
14
src/api/models/channel.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import db from '../../db/mongodb';
|
||||||
|
|
||||||
|
const collection = db.get('channels');
|
||||||
|
|
||||||
|
export default collection as any; // fuck type definition
|
||||||
|
|
||||||
|
export type IChannel = {
|
||||||
|
_id: mongo.ObjectID;
|
||||||
|
created_at: Date;
|
||||||
|
title: string;
|
||||||
|
user_id: mongo.ObjectID;
|
||||||
|
index: number;
|
||||||
|
};
|
|
@ -10,6 +10,7 @@ export function isValidText(text: string): boolean {
|
||||||
|
|
||||||
export type IPost = {
|
export type IPost = {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
|
channel_id: mongo.ObjectID;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
media_ids: mongo.ObjectID[];
|
media_ids: mongo.ObjectID[];
|
||||||
reply_to_id: mongo.ObjectID;
|
reply_to_id: mongo.ObjectID;
|
||||||
|
|
44
src/api/serializers/channel.ts
Normal file
44
src/api/serializers/channel.ts
Normal file
|
@ -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);
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ import Reaction from '../models/post-reaction';
|
||||||
import { IUser } from '../models/user';
|
import { IUser } from '../models/user';
|
||||||
import Vote from '../models/poll-vote';
|
import Vote from '../models/poll-vote';
|
||||||
import serializeApp from './app';
|
import serializeApp from './app';
|
||||||
|
import serializeChannel from './channel';
|
||||||
import serializeUser from './user';
|
import serializeUser from './user';
|
||||||
import serializeDriveFile from './drive-file';
|
import serializeDriveFile from './drive-file';
|
||||||
import parse from '../common/text';
|
import parse from '../common/text';
|
||||||
|
@ -76,8 +77,13 @@ const self = (
|
||||||
_post.app = await serializeApp(_post.app_id);
|
_post.app = await serializeApp(_post.app_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_post.media_ids) {
|
// Populate channel
|
||||||
|
if (_post.channel_id) {
|
||||||
|
_post.channel = await serializeChannel(_post.channel_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Populate media
|
// Populate media
|
||||||
|
if (_post.media_ids) {
|
||||||
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
||||||
await serializeDriveFile(fileId)
|
await serializeDriveFile(fileId)
|
||||||
));
|
));
|
||||||
|
|
12
src/api/stream/channel.ts
Normal file
12
src/api/stream/channel.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import * as websocket from 'websocket';
|
||||||
|
import * as redis from 'redis';
|
||||||
|
|
||||||
|
export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void {
|
||||||
|
const channel = request.resourceURL.query.channel;
|
||||||
|
|
||||||
|
// Subscribe channel stream
|
||||||
|
subscriber.subscribe(`misskey:channel-stream:${channel}`);
|
||||||
|
subscriber.on('message', (_, data) => {
|
||||||
|
connection.send(data);
|
||||||
|
});
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import isNativeToken from './common/is-native-token';
|
||||||
import homeStream from './stream/home';
|
import homeStream from './stream/home';
|
||||||
import messagingStream from './stream/messaging';
|
import messagingStream from './stream/messaging';
|
||||||
import serverStream from './stream/server';
|
import serverStream from './stream/server';
|
||||||
|
import channelStream from './stream/channel';
|
||||||
|
|
||||||
module.exports = (server: http.Server) => {
|
module.exports = (server: http.Server) => {
|
||||||
/**
|
/**
|
||||||
|
@ -26,14 +27,6 @@ module.exports = (server: http.Server) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await authenticate(request.resourceURL.query.i);
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
connection.send('authentication-failed');
|
|
||||||
connection.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to Redis
|
// Connect to Redis
|
||||||
const subscriber = redis.createClient(
|
const subscriber = redis.createClient(
|
||||||
config.redis.port, config.redis.host);
|
config.redis.port, config.redis.host);
|
||||||
|
@ -43,6 +36,19 @@ module.exports = (server: http.Server) => {
|
||||||
subscriber.quit();
|
subscriber.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (request.resourceURL.pathname === '/channel') {
|
||||||
|
channelStream(request, connection, subscriber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await authenticate(request.resourceURL.query.i);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
connection.send('authentication-failed');
|
||||||
|
connection.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const channel =
|
const channel =
|
||||||
request.resourceURL.pathname === '/' ? homeStream :
|
request.resourceURL.pathname === '/' ? homeStream :
|
||||||
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
||||||
|
|
|
@ -3,7 +3,13 @@
|
||||||
* @param {*} post 投稿
|
* @param {*} post 投稿
|
||||||
*/
|
*/
|
||||||
const summarize = (post: any): string => {
|
const summarize = (post: any): string => {
|
||||||
let summary = post.text ? post.text : '';
|
let summary = '';
|
||||||
|
|
||||||
|
// チャンネル
|
||||||
|
summary += post.channel ? `${post.channel.title}:` : '';
|
||||||
|
|
||||||
|
// 本文
|
||||||
|
summary += post.text ? post.text : '';
|
||||||
|
|
||||||
// メディアが添付されているとき
|
// メディアが添付されているとき
|
||||||
if (post.media) {
|
if (post.media) {
|
||||||
|
|
|
@ -88,6 +88,7 @@ type Mixin = {
|
||||||
api_url: string;
|
api_url: string;
|
||||||
auth_url: string;
|
auth_url: string;
|
||||||
about_url: string;
|
about_url: string;
|
||||||
|
ch_url: stirng;
|
||||||
stats_url: string;
|
stats_url: string;
|
||||||
status_url: string;
|
status_url: string;
|
||||||
dev_url: string;
|
dev_url: string;
|
||||||
|
@ -122,6 +123,7 @@ export default function load() {
|
||||||
mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
|
mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
|
||||||
mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
|
mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
|
||||||
mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
|
mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
|
||||||
|
mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
|
||||||
mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
|
mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
|
||||||
mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
|
mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
|
||||||
mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
|
mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
|
||||||
|
|
32
src/web/app/ch/router.js
Normal file
32
src/web/app/ch/router.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import * as riot from 'riot';
|
||||||
|
const route = require('page');
|
||||||
|
let page = null;
|
||||||
|
|
||||||
|
export default me => {
|
||||||
|
route('/', index);
|
||||||
|
route('/:channel', channel);
|
||||||
|
route('*', notFound);
|
||||||
|
|
||||||
|
function index() {
|
||||||
|
mount(document.createElement('mk-index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function channel(ctx) {
|
||||||
|
const el = document.createElement('mk-channel');
|
||||||
|
el.setAttribute('id', ctx.params.channel);
|
||||||
|
mount(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notFound() {
|
||||||
|
mount(document.createElement('mk-not-found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXEC
|
||||||
|
route();
|
||||||
|
};
|
||||||
|
|
||||||
|
function mount(content) {
|
||||||
|
if (page) page.unmount();
|
||||||
|
const body = document.getElementById('app');
|
||||||
|
page = riot.mount(body.appendChild(content))[0];
|
||||||
|
}
|
18
src/web/app/ch/script.js
Normal file
18
src/web/app/ch/script.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Channels
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Style
|
||||||
|
import './style.styl';
|
||||||
|
|
||||||
|
require('./tags');
|
||||||
|
import init from '../init';
|
||||||
|
import route from './router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* init
|
||||||
|
*/
|
||||||
|
init(me => {
|
||||||
|
// Start routing
|
||||||
|
route(me);
|
||||||
|
});
|
4
src/web/app/ch/style.styl
Normal file
4
src/web/app/ch/style.styl
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
@import "../base"
|
||||||
|
|
||||||
|
html
|
||||||
|
background #efefef
|
223
src/web/app/ch/tags/channel.tag
Normal file
223
src/web/app/ch/tags/channel.tag
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
<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>
|
||||||
|
<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 }>
|
||||||
|
<p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<footer>
|
||||||
|
<small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
<style>
|
||||||
|
:scope
|
||||||
|
display block
|
||||||
|
padding 8px
|
||||||
|
|
||||||
|
> main
|
||||||
|
> h1
|
||||||
|
color #f00
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import Progress from '../../common/scripts/loading';
|
||||||
|
import ChannelStream from '../../common/scripts/channel-stream';
|
||||||
|
|
||||||
|
this.mixin('i');
|
||||||
|
this.mixin('api');
|
||||||
|
|
||||||
|
this.id = this.opts.id;
|
||||||
|
this.fetching = true;
|
||||||
|
this.postsFetching = true;
|
||||||
|
this.channel = null;
|
||||||
|
this.posts = null;
|
||||||
|
this.connection = new ChannelStream(this.id);
|
||||||
|
this.version = VERSION;
|
||||||
|
|
||||||
|
this.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'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api('channels/posts', {
|
||||||
|
channel_id: this.id
|
||||||
|
}).then(posts => {
|
||||||
|
this.update({
|
||||||
|
postsFetching: false,
|
||||||
|
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>
|
||||||
|
|
||||||
|
<mk-channel-post>
|
||||||
|
<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>
|
||||||
|
<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
|
||||||
|
display block
|
||||||
|
margin 0
|
||||||
|
padding 0
|
||||||
|
|
||||||
|
> header
|
||||||
|
> .index
|
||||||
|
margin-right 0.25em
|
||||||
|
color #000
|
||||||
|
|
||||||
|
> .name
|
||||||
|
margin-right 0.5em
|
||||||
|
color #008000
|
||||||
|
|
||||||
|
> mk-time
|
||||||
|
margin-right 0.5em
|
||||||
|
|
||||||
|
&:first-of-type
|
||||||
|
display none
|
||||||
|
|
||||||
|
@media (max-width 600px)
|
||||||
|
> mk-time
|
||||||
|
&:first-of-type
|
||||||
|
display initial
|
||||||
|
|
||||||
|
&:last-of-type
|
||||||
|
display none
|
||||||
|
|
||||||
|
> div
|
||||||
|
padding 0 0 1em 2em
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
this.post = this.opts.post;
|
||||||
|
this.form = this.opts.form;
|
||||||
|
|
||||||
|
this.reply = () => {
|
||||||
|
this.form.update({
|
||||||
|
reply: this.post
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</mk-channel-post>
|
||||||
|
|
||||||
|
<mk-channel-form>
|
||||||
|
<p if={ reply }><b>>>{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
|
||||||
|
<textarea ref="text" disabled={ wait }></textarea>
|
||||||
|
<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;
|
||||||
|
|
||||||
|
this.clearReply = () => {
|
||||||
|
this.update({
|
||||||
|
reply: null
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clear = () => {
|
||||||
|
this.clearReply();
|
||||||
|
this.update({
|
||||||
|
files: null
|
||||||
|
});
|
||||||
|
this.refs.text.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
this.post = e => {
|
||||||
|
this.update({
|
||||||
|
wait: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = this.files && this.files.length > 0
|
||||||
|
? this.files.map(f => f.id)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
this.api('posts/create', {
|
||||||
|
text: this.refs.text.value,
|
||||||
|
media_ids: files,
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.drive = () => {
|
||||||
|
window['cb'] = files => {
|
||||||
|
this.update({
|
||||||
|
files: files
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.open(CONFIG.url + '/selectdrive?multiple=true', '_blank');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</mk-channel-form>
|
2
src/web/app/ch/tags/index.js
Normal file
2
src/web/app/ch/tags/index.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
require('./index.tag');
|
||||||
|
require('./channel.tag');
|
33
src/web/app/ch/tags/index.tag
Normal file
33
src/web/app/ch/tags/index.tag
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<mk-index>
|
||||||
|
<button onclick={ n }>%i18n:ch.tags.mk-index.new%</button>
|
||||||
|
<hr>
|
||||||
|
<ul if={ channels }>
|
||||||
|
<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
|
||||||
|
</ul>
|
||||||
|
<style>
|
||||||
|
:scope
|
||||||
|
display block
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
this.mixin('api');
|
||||||
|
|
||||||
|
this.on('mount', () => {
|
||||||
|
this.api('channels').then(channels => {
|
||||||
|
this.update({
|
||||||
|
channels: channels
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.n = () => {
|
||||||
|
const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%');
|
||||||
|
|
||||||
|
this.api('channels/create', {
|
||||||
|
title: title
|
||||||
|
}).then(channel => {
|
||||||
|
location.href = '/' + channel.id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</mk-index>
|
16
src/web/app/common/scripts/channel-stream.js
Normal file
16
src/web/app/common/scripts/channel-stream.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import Stream from './stream';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Channel stream connection
|
||||||
|
*/
|
||||||
|
class Connection extends Stream {
|
||||||
|
constructor(channelId) {
|
||||||
|
super('channel', {
|
||||||
|
channel: channelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Connection;
|
|
@ -6,6 +6,7 @@ const host = isRoot ? Url.host : Url.host.substring(Url.host.indexOf('.') + 1, U
|
||||||
const scheme = Url.protocol;
|
const scheme = Url.protocol;
|
||||||
const url = `${scheme}//${host}`;
|
const url = `${scheme}//${host}`;
|
||||||
const apiUrl = `${scheme}//api.${host}`;
|
const apiUrl = `${scheme}//api.${host}`;
|
||||||
|
const chUrl = `${scheme}//ch.${host}`;
|
||||||
const devUrl = `${scheme}//dev.${host}`;
|
const devUrl = `${scheme}//dev.${host}`;
|
||||||
const aboutUrl = `${scheme}//about.${host}`;
|
const aboutUrl = `${scheme}//about.${host}`;
|
||||||
const statsUrl = `${scheme}//stats.${host}`;
|
const statsUrl = `${scheme}//stats.${host}`;
|
||||||
|
@ -16,6 +17,7 @@ export default {
|
||||||
scheme,
|
scheme,
|
||||||
url,
|
url,
|
||||||
apiUrl,
|
apiUrl,
|
||||||
|
chUrl,
|
||||||
devUrl,
|
devUrl,
|
||||||
aboutUrl,
|
aboutUrl,
|
||||||
statsUrl,
|
statsUrl,
|
||||||
|
|
|
@ -8,6 +8,7 @@ let page = null;
|
||||||
|
|
||||||
export default me => {
|
export default me => {
|
||||||
route('/', index);
|
route('/', index);
|
||||||
|
route('/selectdrive', selectDrive);
|
||||||
route('/i>mentions', mentions);
|
route('/i>mentions', mentions);
|
||||||
route('/post::post', post);
|
route('/post::post', post);
|
||||||
route('/search::query', search);
|
route('/search::query', search);
|
||||||
|
@ -54,6 +55,10 @@ export default me => {
|
||||||
mount(el);
|
mount(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectDrive() {
|
||||||
|
mount(document.createElement('mk-selectdrive-page'));
|
||||||
|
}
|
||||||
|
|
||||||
function notFound() {
|
function notFound() {
|
||||||
mount(document.createElement('mk-not-found'));
|
mount(document.createElement('mk-not-found'));
|
||||||
}
|
}
|
||||||
|
@ -67,6 +72,7 @@ export default me => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function mount(content) {
|
function mount(content) {
|
||||||
|
document.documentElement.style.background = '#313a42';
|
||||||
document.documentElement.removeAttribute('data-page');
|
document.documentElement.removeAttribute('data-page');
|
||||||
if (page) page.unmount();
|
if (page) page.unmount();
|
||||||
const body = document.getElementById('app');
|
const body = document.getElementById('app');
|
||||||
|
|
|
@ -61,6 +61,7 @@ require('./pages/user.tag');
|
||||||
require('./pages/post.tag');
|
require('./pages/post.tag');
|
||||||
require('./pages/search.tag');
|
require('./pages/search.tag');
|
||||||
require('./pages/not-found.tag');
|
require('./pages/not-found.tag');
|
||||||
|
require('./pages/selectdrive.tag');
|
||||||
require('./autocomplete-suggestion.tag');
|
require('./autocomplete-suggestion.tag');
|
||||||
require('./progress-dialog.tag');
|
require('./progress-dialog.tag');
|
||||||
require('./user-preview.tag');
|
require('./user-preview.tag');
|
||||||
|
|
159
src/web/app/desktop/tags/pages/selectdrive.tag
Normal file
159
src/web/app/desktop/tags/pages/selectdrive.tag
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<mk-selectdrive-page>
|
||||||
|
<mk-drive-browser ref="browser" multiple={ multiple }/>
|
||||||
|
<div>
|
||||||
|
<button class="upload" title="PCからドライブにファイルをアップロード" onclick={ upload }><i class="fa fa-upload"></i></button>
|
||||||
|
<button class="cancel" onclick={ close }>キャンセル</button>
|
||||||
|
<button class="ok" onclick={ ok }>決定</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:scope
|
||||||
|
display block
|
||||||
|
height 100%
|
||||||
|
background #fff
|
||||||
|
|
||||||
|
> mk-drive-browser
|
||||||
|
height calc(100% - 72px)
|
||||||
|
|
||||||
|
> div
|
||||||
|
position fixed
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
width 100%
|
||||||
|
height 72px
|
||||||
|
background lighten($theme-color, 95%)
|
||||||
|
|
||||||
|
.upload
|
||||||
|
display inline-block
|
||||||
|
position absolute
|
||||||
|
top 8px
|
||||||
|
left 16px
|
||||||
|
cursor pointer
|
||||||
|
padding 0
|
||||||
|
margin 8px 4px 0 0
|
||||||
|
width 40px
|
||||||
|
height 40px
|
||||||
|
font-size 1em
|
||||||
|
color rgba($theme-color, 0.5)
|
||||||
|
background transparent
|
||||||
|
outline none
|
||||||
|
border solid 1px transparent
|
||||||
|
border-radius 4px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background transparent
|
||||||
|
border-color rgba($theme-color, 0.3)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
color rgba($theme-color, 0.6)
|
||||||
|
background transparent
|
||||||
|
border-color rgba($theme-color, 0.5)
|
||||||
|
box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
&:after
|
||||||
|
content ""
|
||||||
|
pointer-events none
|
||||||
|
position absolute
|
||||||
|
top -5px
|
||||||
|
right -5px
|
||||||
|
bottom -5px
|
||||||
|
left -5px
|
||||||
|
border 2px solid rgba($theme-color, 0.3)
|
||||||
|
border-radius 8px
|
||||||
|
|
||||||
|
.ok
|
||||||
|
.cancel
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
bottom 16px
|
||||||
|
cursor pointer
|
||||||
|
padding 0
|
||||||
|
margin 0
|
||||||
|
width 120px
|
||||||
|
height 40px
|
||||||
|
font-size 1em
|
||||||
|
outline none
|
||||||
|
border-radius 4px
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
&:after
|
||||||
|
content ""
|
||||||
|
pointer-events none
|
||||||
|
position absolute
|
||||||
|
top -5px
|
||||||
|
right -5px
|
||||||
|
bottom -5px
|
||||||
|
left -5px
|
||||||
|
border 2px solid rgba($theme-color, 0.3)
|
||||||
|
border-radius 8px
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
opacity 0.7
|
||||||
|
cursor default
|
||||||
|
|
||||||
|
.ok
|
||||||
|
right 16px
|
||||||
|
color $theme-color-foreground
|
||||||
|
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||||
|
border solid 1px lighten($theme-color, 15%)
|
||||||
|
|
||||||
|
&:not(:disabled)
|
||||||
|
font-weight bold
|
||||||
|
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
|
||||||
|
border-color $theme-color
|
||||||
|
|
||||||
|
&:active:not(:disabled)
|
||||||
|
background $theme-color
|
||||||
|
border-color $theme-color
|
||||||
|
|
||||||
|
.cancel
|
||||||
|
right 148px
|
||||||
|
color #888
|
||||||
|
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||||
|
border solid 1px #e2e2e2
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
|
||||||
|
border-color #dcdcdc
|
||||||
|
|
||||||
|
&:active
|
||||||
|
background #ececec
|
||||||
|
border-color #dcdcdc
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
const q = (new URL(location)).searchParams;
|
||||||
|
this.multiple = q.get('multiple') == 'true' ? true : false;
|
||||||
|
|
||||||
|
this.on('mount', () => {
|
||||||
|
document.documentElement.style.background = '#fff';
|
||||||
|
|
||||||
|
this.refs.browser.on('selected', file => {
|
||||||
|
this.files = [file];
|
||||||
|
this.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refs.browser.on('change-selection', files => {
|
||||||
|
this.update({
|
||||||
|
files: files
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.upload = () => {
|
||||||
|
this.refs.browser.selectLocalFile();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.close = () => {
|
||||||
|
window.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ok = () => {
|
||||||
|
window.opener.cb(this.multiple ? this.files : this.files[0]);
|
||||||
|
window.close();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</mk-selectdrive-page>
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
this.refs.ui.refs.user.on('user-fetched', user => {
|
this.refs.ui.refs.user.on('user-fetched', user => {
|
||||||
Progress.set(0.5);
|
Progress.set(0.5);
|
||||||
document.title = user.name + ' | Misskey'
|
document.title = user.name + ' | Misskey';
|
||||||
});
|
});
|
||||||
|
|
||||||
this.refs.ui.refs.user.on('loaded', () => {
|
this.refs.ui.refs.user.on('loaded', () => {
|
||||||
|
|
|
@ -112,6 +112,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="text" ref="text">
|
<div class="text" ref="text">
|
||||||
|
<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 }>
|
<a class="reply" if={ p.reply_to }>
|
||||||
<i class="fa fa-reply"></i>
|
<i class="fa fa-reply"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -333,6 +334,9 @@
|
||||||
font-weight 400
|
font-weight 400
|
||||||
font-style normal
|
font-style normal
|
||||||
|
|
||||||
|
> .channel
|
||||||
|
margin 0
|
||||||
|
|
||||||
> .reply
|
> .reply
|
||||||
margin-right 8px
|
margin-right 8px
|
||||||
color #717171
|
color #717171
|
||||||
|
|
|
@ -319,7 +319,8 @@
|
||||||
</mk-ui-header-notifications>
|
</mk-ui-header-notifications>
|
||||||
|
|
||||||
<mk-ui-header-nav>
|
<mk-ui-header-nav>
|
||||||
<ul if={ SIGNIN }>
|
<ul>
|
||||||
|
<virtual if={ SIGNIN }>
|
||||||
<li class="home { active: page == 'home' }">
|
<li class="home { active: page == 'home' }">
|
||||||
<a href={ CONFIG.url }>
|
<a href={ CONFIG.url }>
|
||||||
<i class="fa fa-home"></i>
|
<i class="fa fa-home"></i>
|
||||||
|
@ -333,6 +334,13 @@
|
||||||
<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
|
<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
</virtual>
|
||||||
|
<li class="ch">
|
||||||
|
<a href={ CONFIG.chUrl } target="_blank">
|
||||||
|
<i class="fa fa-television"></i>
|
||||||
|
<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="info">
|
<li class="info">
|
||||||
<a href="https://twitter.com/misskey_xyz" target="_blank">
|
<a href="https://twitter.com/misskey_xyz" target="_blank">
|
||||||
<i class="fa fa-info"></i>
|
<i class="fa fa-info"></i>
|
||||||
|
|
|
@ -8,6 +8,7 @@ let page = null;
|
||||||
|
|
||||||
export default me => {
|
export default me => {
|
||||||
route('/', index);
|
route('/', index);
|
||||||
|
route('/selectdrive', selectDrive);
|
||||||
route('/i/notifications', notifications);
|
route('/i/notifications', notifications);
|
||||||
route('/i/messaging', messaging);
|
route('/i/messaging', messaging);
|
||||||
route('/i/messaging/:username', messaging);
|
route('/i/messaging/:username', messaging);
|
||||||
|
@ -122,6 +123,10 @@ export default me => {
|
||||||
mount(el);
|
mount(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectDrive() {
|
||||||
|
mount(document.createElement('mk-selectdrive-page'));
|
||||||
|
}
|
||||||
|
|
||||||
function notFound() {
|
function notFound() {
|
||||||
mount(document.createElement('mk-not-found'));
|
mount(document.createElement('mk-not-found'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -483,7 +483,7 @@
|
||||||
if (fn == null || fn == '') return;
|
if (fn == null || fn == '') return;
|
||||||
switch (fn) {
|
switch (fn) {
|
||||||
case '1':
|
case '1':
|
||||||
this.refs.file.click();
|
this.selectLocalFile();
|
||||||
break;
|
break;
|
||||||
case '2':
|
case '2':
|
||||||
this.urlUpload();
|
this.urlUpload();
|
||||||
|
@ -503,6 +503,10 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.selectLocalFile = () => {
|
||||||
|
this.refs.file.click();
|
||||||
|
};
|
||||||
|
|
||||||
this.createFolder = () => {
|
this.createFolder = () => {
|
||||||
const name = window.prompt('フォルダー名');
|
const name = window.prompt('フォルダー名');
|
||||||
if (name == null || name == '') return;
|
if (name == null || name == '') return;
|
||||||
|
|
|
@ -19,6 +19,7 @@ require('./page/settings/authorized-apps.tag');
|
||||||
require('./page/settings/twitter.tag');
|
require('./page/settings/twitter.tag');
|
||||||
require('./page/messaging.tag');
|
require('./page/messaging.tag');
|
||||||
require('./page/messaging-room.tag');
|
require('./page/messaging-room.tag');
|
||||||
|
require('./page/selectdrive.tag');
|
||||||
require('./home.tag');
|
require('./home.tag');
|
||||||
require('./home-timeline.tag');
|
require('./home-timeline.tag');
|
||||||
require('./timeline.tag');
|
require('./timeline.tag');
|
||||||
|
|
83
src/web/app/mobile/tags/page/selectdrive.tag
Normal file
83
src/web/app/mobile/tags/page/selectdrive.tag
Normal file
|
@ -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>
|
|
@ -164,6 +164,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="text" ref="text">
|
<div class="text" ref="text">
|
||||||
|
<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 }>
|
<a class="reply" if={ p.reply_to }>
|
||||||
<i class="fa fa-reply"></i>
|
<i class="fa fa-reply"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -373,6 +374,9 @@
|
||||||
mk-url-preview
|
mk-url-preview
|
||||||
margin-top 8px
|
margin-top 8px
|
||||||
|
|
||||||
|
> .channel
|
||||||
|
margin 0
|
||||||
|
|
||||||
> .reply
|
> .reply
|
||||||
margin-right 8px
|
margin-right 8px
|
||||||
color #717171
|
color #717171
|
||||||
|
|
|
@ -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>
|
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
<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>
|
||||||
|
|
|
@ -16,6 +16,7 @@ module.exports = langs.map(([lang, locale]) => {
|
||||||
const entry = {
|
const entry = {
|
||||||
desktop: './src/web/app/desktop/script.js',
|
desktop: './src/web/app/desktop/script.js',
|
||||||
mobile: './src/web/app/mobile/script.js',
|
mobile: './src/web/app/mobile/script.js',
|
||||||
|
ch: './src/web/app/ch/script.js',
|
||||||
stats: './src/web/app/stats/script.js',
|
stats: './src/web/app/stats/script.js',
|
||||||
status: './src/web/app/status/script.js',
|
status: './src/web/app/status/script.js',
|
||||||
dev: './src/web/app/dev/script.js',
|
dev: './src/web/app/dev/script.js',
|
||||||
|
|
Loading…
Reference in a new issue