Compare commits

..

46 Commits

Author SHA1 Message Date
9f2d8e1d51 10.39.0 2018-11-05 21:07:24 +09:00
0c98a90b75 [Client] カスタム絵文字にホバーしたときに拡大するエフェクトを追加 2018-11-05 21:04:19 +09:00
0047920c1a Merge pull request #3117 from syuilo/twemoji
Use Twemoji
2018-11-05 20:52:55 +09:00
e4bb534f20 Better emoji regexp 2018-11-05 20:49:17 +09:00
3fc04fcdc5 Improve readdability 2018-11-05 20:49:02 +09:00
e542dcac30 Fix test 2018-11-05 20:40:39 +09:00
a0b13505a0 Insert missing spaces 2018-11-05 20:15:09 +09:00
389f9bfea2 Add test 2018-11-05 20:14:49 +09:00
630a534cee Fix test 2018-11-05 20:10:28 +09:00
5744c391e6 Revert "Fix test fails"
This reverts commit b9b05a7401.
2018-11-05 20:10:00 +09:00
b9b05a7401 Fix test fails 2018-11-05 19:50:38 +09:00
359470a263 Fix bug 2018-11-05 19:40:09 +09:00
3fe934ee62 Better alt value 2018-11-05 19:33:28 +09:00
3abe632f06 Clean up 2018-11-05 19:29:50 +09:00
65961bc15b Refactoring & 設定でTwemojiを使うかどうか切り替えられるように 2018-11-05 19:20:35 +09:00
12f932d48a Update CI configuration (#3120)
* Update config.yml

* Add `npm prune` command

refs: https://misskey.xyz/notes/5bd9b87168b2a30045edb3aa

* Ensure package-lock.json exists
2018-11-05 17:38:57 +09:00
54e9147782 Refactoring codes
refs: https://github.com/syuilo/misskey/pull/3117#pullrequestreview-171437187
2018-11-05 17:04:17 +09:00
31b7626d01 Make code better
refs: https://github.com/syuilo/misskey/pull/3117#pullrequestreview-171423739
refs: https://github.com/syuilo/misskey/pull/3117#pullrequestreview-171424596
refs: https://github.com/syuilo/misskey/pull/3117#pullrequestreview-171425303
2018-11-05 16:19:14 +09:00
200ebefe92 Add support for unicode emojis
refs: https://github.com/syuilo/misskey/pull/3117#issuecomment-435745613
2018-11-05 15:15:37 +09:00
9d29a2e85a 10.38.8 2018-11-05 13:47:57 +09:00
c62a225542 oops 2018-11-05 13:46:46 +09:00
d5d995a3e6 Refactor 2018-11-05 13:38:50 +09:00
b7f10fdc10 Fix bug
refs: https://github.com/syuilo/misskey/pull/3117#discussion_r230624389
2018-11-05 13:24:54 +09:00
cbba03b376 [Client] Fix bug 2018-11-05 13:23:30 +09:00
f84e9c7dc8 絵文字サジェストでスペースを挿入しないように 2018-11-05 12:35:50 +09:00
a22ddb1fb9 ✌️ 2018-11-05 11:58:41 +09:00
0d23ce3d45 Make /api/v1/instance and /api/v1/custom_emojis better (#3118)
* Separate commits

From commit dca110ebaa.

* Re-separate commits

From commit 9719387bee.
2018-11-05 11:57:17 +09:00
9719387bee Re-separate commits 2018-11-05 11:51:14 +09:00
dca110ebaa Separate commits
Flash Back 90's
2018-11-05 11:39:13 +09:00
136f23c7ad Merge branch 'develop' into twemoji 2018-11-05 11:21:34 +09:00
0963e6d6e1 Use Twemoji 2018-11-05 11:19:40 +09:00
712802e682 10.38.7 2018-11-05 11:11:23 +09:00
abe99c3c73 Update locales/ja-JP.yml 2018-11-05 11:10:02 +09:00
d7a3b71028 投稿の最大文字数情報を設定ファイルではなくDBに保存するように 2018-11-05 11:09:05 +09:00
10c434f24a Remove Travis
Closes #3109
2018-11-05 10:52:07 +09:00
fe46c53ea6 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-11-05 10:48:51 +09:00
cdd123dfd3 [doc] specify node version 2018-11-05 10:48:40 +09:00
a1a3ee44b5 Implement /api/v1/custom_emojis (#3116) 2018-11-05 10:45:57 +09:00
4e7fbd8967 Implement /api/v1/custom_emojis 2018-11-05 10:42:46 +09:00
a86c419f95 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-11-05 10:40:15 +09:00
e3ec0ad97e [Client] Improve admin panel usability 2018-11-05 10:40:01 +09:00
75791981ce Fix #3115 2018-11-05 10:34:53 +09:00
e813fe16b9 [API] Better validation of admin/emoji/add 2018-11-05 10:33:49 +09:00
42ac7b954d Improve admin panel usability 2018-11-05 10:32:45 +09:00
c1bbf5dab6 [Client] Fix error 2018-11-05 10:29:57 +09:00
e16dc2a910 Update README.md (#3112) 2018-11-05 01:57:08 +09:00
39 changed files with 384 additions and 176 deletions

View File

@ -23,6 +23,10 @@ jobs:
executor: default executor: default
steps: steps:
- checkout - checkout
- run:
name: Ensure package-lock.json
command: |
[ ! -e package-lock.json ] && echo '{}' > package-lock.json
- restore_cache: - restore_cache:
name: Restore npm package caches name: Restore npm package caches
keys: keys:
@ -35,6 +39,7 @@ jobs:
name: Install Dependencies name: Install Dependencies
command: | command: |
npm install npm install
npm prune
- run: - run:
name: Configure name: Configure
command: | command: |
@ -50,8 +55,8 @@ jobs:
key: npm-v1-arch-{{ arch }}-env-{{ .Environment.variableName }}-package-{{ checksum "package.json" }}-lock-{{ checksum "package-lock.json" }}-ls-{{ checksum "ls" }} key: npm-v1-arch-{{ arch }}-env-{{ .Environment.variableName }}-package-{{ checksum "package.json" }}-lock-{{ checksum "package-lock.json" }}-ls-{{ checksum "ls" }}
paths: paths:
- node_modules - node_modules
- store_artifacts: # - store_artifacts:
path: built # path: built
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: paths:
@ -98,7 +103,6 @@ jobs:
name: Build name: Build
command: | command: |
docker build . | tee docker.log docker build . | tee docker.log
tail -n 1 docker.log | read __Successfully __built tag
- when: - when:
condition: <<parameters.with_deploy>> condition: <<parameters.with_deploy>>
steps: steps:
@ -107,6 +111,7 @@ jobs:
command: | command: |
if [ "$DOCKERHUB_USERNAME$DOCKERHUB_PASSWORD" ] if [ "$DOCKERHUB_USERNAME$DOCKERHUB_PASSWORD" ]
then then
tail -n 1 docker.log | read __Successfully __built tag
docker tag $tag misskey/misskey docker tag $tag misskey/misskey
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD
docker push misskey/misskey docker push misskey/misskey
@ -126,10 +131,13 @@ workflows:
without_redis: "true" without_redis: "true"
requires: requires:
- build - build
- docker:
filters: filters:
branches: branches:
ignore: master only: master
# - docker:
# filters:
# branches:
# ignore: master
- docker: - docker:
with_deploy: "true" with_deploy: "true"
filters: filters:

View File

@ -167,6 +167,3 @@ drive:
# external: true # external: true
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}} # engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
# timeout: 300000 # timeout: 300000
# Max allowed note text length in charactors
maxNoteTextLength: 1000

View File

@ -1,41 +0,0 @@
# travis file
# https://docs.travis-ci.com/user/customizing-the-build
notifications:
email: false
branches:
except:
- l10n_master
language: node_js
node_js:
- 11.0.0
env:
- CXX=g++-4.8 NODE_ENV=production
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
cache:
directories:
- node_modules
services:
- mongodb
- redis-server
before_script:
- npm install
# 設定ファイルを配置
- cp ./.ci/default.yml ./.config
- cp ./.ci/test.yml ./.config
- travis_wait npm run build

View File

@ -23,5 +23,5 @@ Please use [Crowdin](https://crowdin.com/project/misskey) for localization.
* Test codes are located in `/test`. * Test codes are located in `/test`.
## Continuous integration ## Continuous integration
Misskey uses Travis for automated test. Misskey uses CircleCI for automated test.
Configuration files are located in `/.travis`. Configuration files are located in `/.circleci`.

View File

@ -4,7 +4,6 @@
================================================================ ================================================================
[![CircleCI](https://circleci.com/gh/syuilo/misskey.svg?style=svg)](https://circleci.com/gh/syuilo/misskey) [![CircleCI](https://circleci.com/gh/syuilo/misskey.svg?style=svg)](https://circleci.com/gh/syuilo/misskey)
[![][travis-badge]][travis-link]
[![][dependencies-badge]][dependencies-link] [![][dependencies-badge]][dependencies-link]
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
@ -44,7 +43,7 @@ Easiest way to tell your emotions. Misskey allows you to add various type of rea
<h3 align="left">Interface</h3> <h3 align="left">Interface</h3>
<p align="left"> <p align="left">
No UI fits for everyone. Therefore, Misskey has a highly customizable UI for your taste. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home. Highly customizable UI for your taste. We understand no UI fits for everyone. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home.
</p> </p>
--- ---
@ -124,8 +123,6 @@ Misskey is an open-source software licensed under the [GNU AGPLv3](LICENSE).
[agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html [agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html
[agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square [agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
[travis-link]: https://travis-ci.org/syuilo/misskey
[travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
[dependencies-link]: https://david-dm.org/syuilo/misskey [dependencies-link]: https://david-dm.org/syuilo/misskey
[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square [dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square

View File

@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
Please install and setup these softwares: Please install and setup these softwares:
#### Dependencies :package: #### Dependencies :package:
* **[Node.js](https://nodejs.org/en/)** * **[Node.js](https://nodejs.org/en/)** >= 10.0.0
* **[MongoDB](https://www.mongodb.com/)** >= 3.6 * **[MongoDB](https://www.mongodb.com/)** >= 3.6
##### Optional ##### Optional

View File

@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
これらのソフトウェアをインストール・設定してください: これらのソフトウェアをインストール・設定してください:
#### 依存関係 :package: #### 依存関係 :package:
* **[Node.js](https://nodejs.org/en/)** * **[Node.js](https://nodejs.org/en/)** (10.0.0以上)
* **[MongoDB](https://www.mongodb.com/)** (3.6以上) * **[MongoDB](https://www.mongodb.com/)** (3.6以上)
##### オプション ##### オプション

View File

@ -131,6 +131,7 @@ common:
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストを省略しない"
reduce-motion: "UIの動きを減らす" reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
use-os-default-emojis: "OS標準の絵文字を使用"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
@ -1077,10 +1078,12 @@ admin/views/instance.vue:
instance-name: "インスタンス名" instance-name: "インスタンス名"
instance-description: "インスタンスの紹介" instance-description: "インスタンスの紹介"
banner-url: "バナー画像URL" banner-url: "バナー画像URL"
disableRegistration: "ユーザー登録の受付を停止する" max-note-text-length: "投稿の最大文字数"
disableLocalTimeline: "ローカルタイムラインを無効にする" disable-registration: "ユーザー登録の受付を停止する"
disable-local-timeline: "ローカルタイムラインを無効にする"
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
@ -1132,10 +1135,15 @@ admin/views/emoji.vue:
url: "絵文字画像URL" url: "絵文字画像URL"
add: "追加" add: "追加"
info: "50KB以下のPNG画像をおすすめします。" info: "50KB以下のPNG画像をおすすめします。"
added: "絵文字を登録しました"
emojis: emojis:
title: "絵文字一覧" title: "絵文字一覧"
update: "更新" update: "更新"
remove: "削除" remove: "削除"
updated: "更新しました"
remove-emoji:
are-you-sure: "「$1」を削除しますか"
removed: "削除しました"
admin/views/announcements.vue: admin/views/announcements.vue:
announcements: "お知らせ" announcements: "お知らせ"
@ -1144,6 +1152,10 @@ admin/views/announcements.vue:
add: "追加" add: "追加"
title: "タイトル" title: "タイトル"
text: "内容" text: "内容"
saved: "保存しました"
_remove:
are-you-sure: "「$1」を削除しますか"
removed: "削除しました"
admin/views/hashtags.vue: admin/views/hashtags.vue:
hided-tags: "Hidden Tags" hided-tags: "Hidden Tags"

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "10.38.6", "version": "10.39.0",
"clientVersion": "1.0.11516", "clientVersion": "1.0.11562",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,

View File

@ -10,7 +10,7 @@
<span>%i18n:@text%</span> <span>%i18n:@text%</span>
</ui-textarea> </ui-textarea>
<ui-horizon-group> <ui-horizon-group>
<ui-button @click="save">%fa:save R% %i18n:@save%</ui-button> <ui-button @click="save()">%fa:save R% %i18n:@save%</ui-button>
<ui-button @click="remove(i)">%fa:trash-alt R% %i18n:@remove%</ui-button> <ui-button @click="remove(i)">%fa:trash-alt R% %i18n:@remove%</ui-button>
</ui-horizon-group> </ui-horizon-group>
</section> </section>
@ -46,17 +46,36 @@ export default Vue.extend({
}, },
remove(i) { remove(i) {
this.announcements = this.announcements.filter((_, j) => j !== i); this.$swal({
this.save(); type: 'warning',
text: '%i18n:@_remove.are-you-sure%'.replace('$1', this.announcements.find((_, j) => j == i).title),
showCancelButton: true
}).then(res => {
if (!res.value) return;
this.announcements = this.announcements.filter((_, j) => j !== i);
this.save(true);
this.$swal({
type: 'success',
text: '%i18n:@_remove.removed%'
});
});
}, },
save() { save(silent) {
(this as any).api('admin/update-meta', { (this as any).api('admin/update-meta', {
broadcasts: this.announcements broadcasts: this.announcements
}).then(() => { }).then(() => {
//(this as any).os.apis.dialog({ text: `Saved` }); if (!silent) {
this.$swal({
type: 'success',
text: '%i18n:@saved%'
});
}
}).catch(e => { }).catch(e => {
//(this as any).os.apis.dialog({ text: `Failed ${e}` }); this.$swal({
type: 'error',
text: e
});
}); });
} }
} }

View File

@ -67,17 +67,24 @@ export default Vue.extend({
(this as any).api('admin/emoji/add', { (this as any).api('admin/emoji/add', {
name: this.name, name: this.name,
url: this.url, url: this.url,
aliases: this.aliases.split(' ') aliases: this.aliases.split(' ').filter(x => x.length > 0)
}).then(() => { }).then(() => {
//(this as any).os.apis.dialog({ text: `Added` }); this.$swal({
type: 'success',
text: '%i18n:@add-emoji.added%'
});
this.fetchEmojis(); this.fetchEmojis();
}).catch(e => { }).catch(e => {
//(this as any).os.apis.dialog({ text: `Failed ${e}` }); this.$swal({
type: 'error',
text: e
});
}); });
}, },
fetchEmojis() { fetchEmojis() {
(this as any).api('admin/emoji/list').then(emojis => { (this as any).api('admin/emoji/list').then(emojis => {
emojis.reverse();
emojis.forEach(e => e.aliases = (e.aliases || []).join(' ')); emojis.forEach(e => e.aliases = (e.aliases || []).join(' '));
this.emojis = emojis; this.emojis = emojis;
}); });
@ -88,22 +95,42 @@ export default Vue.extend({
id: emoji.id, id: emoji.id,
name: emoji.name, name: emoji.name,
url: emoji.url, url: emoji.url,
aliases: emoji.aliases.split(' ') aliases: emoji.aliases.split(' ').filter(x => x.length > 0)
}).then(() => { }).then(() => {
//(this as any).os.apis.dialog({ text: `Updated` }); this.$swal({
type: 'success',
text: '%i18n:@updated%'
});
}).catch(e => { }).catch(e => {
//(this as any).os.apis.dialog({ text: `Failed ${e}` }); this.$swal({
type: 'error',
text: e
});
}); });
}, },
removeEmoji(emoji) { removeEmoji(emoji) {
(this as any).api('admin/emoji/remove', { this.$swal({
id: emoji.id type: 'warning',
}).then(() => { text: '%i18n:@remove-emoji.are-you-sure%'.replace('$1', emoji.name),
//(this as any).os.apis.dialog({ text: `Removed` }); showCancelButton: true
this.fetchEmojis(); }).then(res => {
}).catch(e => { if (!res.value) return;
//(this as any).os.apis.dialog({ text: `Failed ${e}` });
(this as any).api('admin/emoji/remove', {
id: emoji.id
}).then(() => {
this.$swal({
type: 'success',
text: '%i18n:@remove-emoji.removed%'
});
this.fetchEmojis();
}).catch(e => {
this.$swal({
type: 'error',
text: e
});
});
}); });
} }
} }

View File

@ -6,6 +6,7 @@
<ui-input v-model="name">%i18n:@instance-name%</ui-input> <ui-input v-model="name">%i18n:@instance-name%</ui-input>
<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea> <ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea>
<ui-input v-model="bannerUrl">%i18n:@banner-url%</ui-input> <ui-input v-model="bannerUrl">%i18n:@banner-url%</ui-input>
<ui-input v-model="maxNoteTextLength">%i18n:@max-note-text-length%</ui-input>
<ui-button @click="updateMeta">%i18n:@save%</ui-button> <ui-button @click="updateMeta">%i18n:@save%</ui-button>
</section> </section>
</ui-card> </ui-card>
@ -39,6 +40,7 @@ export default Vue.extend({
bannerUrl: null, bannerUrl: null,
name: null, name: null,
description: null, description: null,
maxNoteTextLength: null,
inviteCode: null, inviteCode: null,
}; };
}, },
@ -48,6 +50,7 @@ export default Vue.extend({
this.bannerUrl = meta.bannerUrl; this.bannerUrl = meta.bannerUrl;
this.name = meta.name; this.name = meta.name;
this.description = meta.description; this.description = meta.description;
this.maxNoteTextLength = meta.maxNoteTextLength;
}); });
}, },
@ -56,7 +59,10 @@ export default Vue.extend({
(this as any).api('admin/invite').then(x => { (this as any).api('admin/invite').then(x => {
this.inviteCode = x.code; this.inviteCode = x.code;
}).catch(e => { }).catch(e => {
//(this as any).os.apis.dialog({ text: `Failed ${e}` }); this.$swal({
type: 'error',
text: e
});
}); });
}, },
@ -66,11 +72,18 @@ export default Vue.extend({
disableLocalTimeline: this.disableLocalTimeline, disableLocalTimeline: this.disableLocalTimeline,
bannerUrl: this.bannerUrl, bannerUrl: this.bannerUrl,
name: this.name, name: this.name,
description: this.description description: this.description,
maxNoteTextLength: parseInt(this.maxNoteTextLength, 10)
}).then(() => { }).then(() => {
//(this as any).os.apis.dialog({ text: `Saved` }); this.$swal({
type: 'success',
text: '%i18n:@saved%'
});
}).catch(e => { }).catch(e => {
//(this as any).os.apis.dialog({ text: `Failed ${e}` }); this.$swal({
type: 'error',
text: e
});
}); });
} }
} }

View File

@ -14,7 +14,8 @@
</ol> </ol>
<ol class="emojis" ref="suggests" v-if="emojis.length > 0"> <ol class="emojis" ref="suggests" v-if="emojis.length > 0">
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1"> <li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji" v-if="emoji.url"><img :src="emoji.url" :alt="emoji.emoji"/></span> <span class="emoji" v-if="emoji.isCustomEmoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else-if="!useOsDefaultEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else>{{ emoji.emoji }}</span> <span class="emoji" v-else>{{ emoji.emoji }}</span>
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> <span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span> <span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
@ -33,6 +34,7 @@ type EmojiDef = {
name: string; name: string;
aliasOf?: string; aliasOf?: string;
url?: string; url?: string;
isCustomEmoji?: boolean;
}; };
const lib = Object.entries(emojilib.lib).filter((x: any) => { const lib = Object.entries(emojilib.lib).filter((x: any) => {
@ -42,7 +44,8 @@ const lib = Object.entries(emojilib.lib).filter((x: any) => {
const emjdb: EmojiDef[] = lib.map((x: any) => ({ const emjdb: EmojiDef[] = lib.map((x: any) => ({
emoji: x[1].char, emoji: x[1].char,
name: x[0], name: x[0],
aliasOf: null aliasOf: null,
url: `https://twemoji.maxcdn.com/2/svg/${x[1].char.codePointAt(0).toString(16)}.svg`
})); }));
lib.forEach((x: any) => { lib.forEach((x: any) => {
@ -51,7 +54,8 @@ lib.forEach((x: any) => {
emjdb.push({ emjdb.push({
emoji: x[1].char, emoji: x[1].char,
name: k, name: k,
aliasOf: x[0] aliasOf: x[0],
url: `https://twemoji.maxcdn.com/2/svg/${x[1].char.codePointAt(0).toString(16)}.svg`
}); });
}); });
} }
@ -77,6 +81,10 @@ export default Vue.extend({
computed: { computed: {
items(): HTMLCollection { items(): HTMLCollection {
return (this.$refs.suggests as Element).children; return (this.$refs.suggests as Element).children;
},
useOsDefaultEmojis(): boolean {
return this.$store.state.device.useOsDefaultEmojis;
} }
}, },
@ -107,7 +115,8 @@ export default Vue.extend({
emojiDefinitions.push({ emojiDefinitions.push({
name: x.name, name: x.name,
emoji: `:${x.name}:`, emoji: `:${x.name}:`,
url: x.url url: x.url,
isCustomEmoji: true
}); });
if (x.aliases) { if (x.aliases) {
@ -116,7 +125,8 @@ export default Vue.extend({
name: alias, name: alias,
aliasOf: x.name, aliasOf: x.name,
emoji: `:${x.name}:`, emoji: `:${x.name}:`,
url: x.url url: x.url,
isCustomEmoji: true
}); });
}); });
} }

View File

@ -0,0 +1,81 @@
<template>
<img v-if="customEmoji" class="fvgwvorwhxigeolkkrcderjzcawqrscl custom" :src="url" :alt="alt" :title="alt"/>
<img v-else-if="char && !useOsDefaultEmojis" class="fvgwvorwhxigeolkkrcderjzcawqrscl" :src="url" :alt="alt" :title="alt"/>
<span v-else-if="char && useOsDefaultEmojis">{{ char }}</span>
<span v-else>:{{ name }}:</span>
</template>
<script lang="ts">
import Vue from 'vue';
import { lib } from 'emojilib';
export default Vue.extend({
props: {
name: {
type: String,
required: false
},
emoji: {
type: String,
required: false
},
customEmojis: {
required: false,
default: []
}
},
data() {
return {
url: null,
char: null,
customEmoji: null
}
},
computed: {
alt(): string {
return this.customEmoji ? `:${this.customEmoji.name}:` : this.char;
},
useOsDefaultEmojis(): boolean {
return this.$store.state.device.useOsDefaultEmojis;
}
},
created() {
if (this.name) {
const customEmoji = this.customEmojis.find(x => x.name == this.name);
if (customEmoji) {
this.customEmoji = customEmoji;
this.url = customEmoji.url;
} else {
const emoji = lib[this.name];
if (emoji) {
this.char = emoji.char;
}
}
} else {
this.char = this.emoji;
}
if (this.char) {
this.url = `https://twemoji.maxcdn.com/2/svg/${this.char.codePointAt(0).toString(16)}.svg`;
}
}
});
</script>
<style lang="stylus" scoped>
.fvgwvorwhxigeolkkrcderjzcawqrscl
height 1em
&.custom
height 2.5em
vertical-align middle
transition transform 0.2s ease
&:hover
transform scale(1.2)
</style>

View File

@ -39,6 +39,7 @@ import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue'; import twitterSetting from './twitter-setting.vue';
import githubSetting from './github-setting.vue'; import githubSetting from './github-setting.vue';
import fileTypeIcon from './file-type-icon.vue'; import fileTypeIcon from './file-type-icon.vue';
import emoji from './emoji.vue';
import Reversi from './games/reversi/reversi.vue'; import Reversi from './games/reversi/reversi.vue';
import welcomeTimeline from './welcome-timeline.vue'; import welcomeTimeline from './welcome-timeline.vue';
import uiInput from './ui/input.vue'; import uiInput from './ui/input.vue';
@ -93,6 +94,7 @@ Vue.component('mk-url-preview', urlPreview);
Vue.component('mk-twitter-setting', twitterSetting); Vue.component('mk-twitter-setting', twitterSetting);
Vue.component('mk-github-setting', githubSetting); Vue.component('mk-github-setting', githubSetting);
Vue.component('mk-file-type-icon', fileTypeIcon); Vue.component('mk-file-type-icon', fileTypeIcon);
Vue.component('mk-emoji', emoji);
Vue.component('mk-reversi', Reversi); Vue.component('mk-reversi', Reversi);
Vue.component('mk-welcome-timeline', welcomeTimeline); Vue.component('mk-welcome-timeline', welcomeTimeline);
Vue.component('ui-input', uiInput); Vue.component('ui-input', uiInput);

View File

@ -1,5 +1,4 @@
import Vue, { VNode } from 'vue'; import Vue, { VNode } from 'vue';
import * as emojilib from 'emojilib';
import { length } from 'stringz'; import { length } from 'stringz';
import parse from '../../../../../mfm/parse'; import parse from '../../../../../mfm/parse';
import getAcct from '../../../../../misc/acct/render'; import getAcct from '../../../../../misc/acct/render';
@ -188,24 +187,15 @@ export default Vue.component('misskey-flavored-markdown', {
} }
case 'emoji': { case 'emoji': {
//#region カスタム絵文字 return [createElement('mk-emoji', {
if (this.customEmojis != null) { attrs: {
const customEmoji = this.customEmojis.find(e => e.name == token.emoji || (e.aliases || []).includes(token.emoji)); emoji: token.emoji,
if (customEmoji) { name: token.name
return [createElement('img', { },
attrs: { props: {
src: customEmoji.url, customEmojis: this.customEmojis
alt: token.emoji,
title: token.emoji,
style: 'height: 2.5em; vertical-align: middle;'
}
})];
} }
} })];
//#endregion
const emoji = emojilib.lib[token.emoji];
return [createElement('span', emoji ? emoji.char : token.content)];
} }
case 'search': { case 'search': {

View File

@ -12,7 +12,11 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
inject: ['horizonGrouped'], inject: {
horizonGrouped: {
default: false
}
},
props: { props: {
type: { type: {
type: String, type: String,

View File

@ -41,7 +41,11 @@ import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength'); const getPasswordStrength = require('syuilo-password-strength');
export default Vue.extend({ export default Vue.extend({
inject: ['horizonGrouped'], inject: {
horizonGrouped: {
default: false
}
},
props: { props: {
value: { value: {
required: false required: false

View File

@ -145,6 +145,7 @@ class Autocomplete {
} else { } else {
// サジェスト要素作成 // サジェスト要素作成
this.suggestion = new MkAutocomplete({ this.suggestion = new MkAutocomplete({
parent: this.vm,
propsData: { propsData: {
textarea: this.textarea, textarea: this.textarea,
complete: this.complete, complete: this.complete,
@ -222,8 +223,6 @@ class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf(':')); const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
const after = source.substr(caret); const after = source.substr(caret);
if (value.startsWith(':')) value = value + ' ';
// 挿入 // 挿入
this.text = trimmedBefore + value + after; this.text = trimmedBefore + value + after;

View File

@ -115,6 +115,7 @@
<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion%</ui-switch> <ui-switch v-model="reduceMotion">%i18n:common.reduce-motion%</ui-switch>
<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch> <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch> <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
<ui-switch v-model="useOsDefaultEmojis">%i18n:common.use-os-default-emojis%</ui-switch>
<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch> <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
</section> </section>
<section> <section>
@ -324,6 +325,11 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
useOsDefaultEmojis: {
get() { return this.$store.state.device.useOsDefaultEmojis; },
set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); }
},
reduceMotion: { reduceMotion: {
get() { return this.$store.state.device.reduceMotion; }, get() { return this.$store.state.device.reduceMotion; },
set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); } set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }

View File

@ -23,6 +23,7 @@
<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch> <ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch> <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch> <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
<ui-switch v-model="useOsDefaultEmojis">%i18n:common.use-os-default-emojis%</ui-switch>
<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch> <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
<ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch> <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
<ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch> <ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
@ -199,6 +200,11 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
}, },
useOsDefaultEmojis: {
get() { return this.$store.state.device.useOsDefaultEmojis; },
set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); }
},
reduceMotion: { reduceMotion: {
get() { return this.$store.state.device.reduceMotion; }, get() { return this.$store.state.device.reduceMotion; },
set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); } set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }

View File

@ -62,7 +62,8 @@ const defaultDeviceSettings = {
deckColumnAlign: 'center', deckColumnAlign: 'center',
mobileNotificationPosition: 'bottom', mobileNotificationPosition: 'bottom',
deckTemporaryColumn: null, deckTemporaryColumn: null,
deckDefault: false deckDefault: false,
useOsDefaultEmojis: false
}; };
export default (os: MiOS) => new Vuex.Store({ export default (os: MiOS) => new Vuex.Store({

View File

@ -49,8 +49,6 @@ export default function load() {
if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256; if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256;
if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8; if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8;
if (config.maxNoteTextLength == null) config.maxNoteTextLength = 1000;
return Object.assign(config, mixin); return Object.assign(config, mixin);
} }

View File

@ -107,8 +107,6 @@ export type Source = {
engine: string; engine: string;
timeout: number; timeout: number;
}; };
maxNoteTextLength?: number;
}; };
/** /**

View File

@ -5,16 +5,29 @@
export type TextElementEmoji = { export type TextElementEmoji = {
type: 'emoji'; type: 'emoji';
content: string; content: string;
emoji: string; emoji?: string;
name?: string;
}; };
const emojiRegex = /^[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/ug;
export default function(text: string) { export default function(text: string) {
const match = text.match(/^:([a-zA-Z0-9+_-]+):/); const name = text.match(/^:([a-zA-Z0-9+_-]+):/);
if (!match) return null; if (name) {
const emoji = match[0]; return {
return { type: 'emoji',
type: 'emoji', content: name[0],
content: emoji, name: name[1]
emoji: match[1] } as TextElementEmoji;
} as TextElementEmoji; }
const unicode = text.match(emojiRegex);
if (unicode) {
const [content] = unicode;
return {
type: 'emoji',
content,
emoji: content
} as TextElementEmoji;
}
return null;
} }

View File

@ -43,4 +43,9 @@ export type IMeta = {
disableLocalTimeline?: boolean; disableLocalTimeline?: boolean;
hidedTags?: string[]; hidedTags?: string[];
bannerUrl?: string; bannerUrl?: string;
/**
* Max allowed note text length in charactors
*/
maxNoteTextLength?: number;
}; };

View File

@ -11,7 +11,6 @@ import Reaction from './note-reaction';
import { packMany as packFileMany, IDriveFile } from './drive-file'; import { packMany as packFileMany, IDriveFile } from './drive-file';
import Favorite from './favorite'; import Favorite from './favorite';
import Following from './following'; import Following from './following';
import config from '../config';
import Emoji from './emoji'; import Emoji from './emoji';
const Note = db.get<INote>('notes'); const Note = db.get<INote>('notes');
@ -27,10 +26,6 @@ Note.createIndex({ createdAt: -1 });
Note.createIndex({ score: -1 }, { sparse: true }); Note.createIndex({ score: -1 }, { sparse: true });
export default Note; export default Note;
export function isValidText(text: string): boolean {
return length(text.trim()) <= config.maxNoteTextLength && text.trim() != '';
}
export function isValidCw(text: string): boolean { export function isValidCw(text: string): boolean {
return length(text.trim()) <= 100; return length(text.trim()) <= 100;
} }

View File

@ -62,15 +62,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
}) as IRemoteUser; }) as IRemoteUser;
} }
//#region Log
publishApLogStream({
direction: 'in',
activity: activity.type,
host: user.host,
actor: user.username
});
//#endregion
// Update activityの場合は、ここで署名検証/更新処理まで実施して終了 // Update activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') { if (activity.type === 'Update') {
if (activity.object && activity.object.type === 'Person') { if (activity.object && activity.object.type === 'Person') {
@ -101,6 +92,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
done(); done();
return; return;
} }
//#region Log
publishApLogStream({
direction: 'in',
activity: activity.type,
host: user.host,
actor: user.username
});
//#endregion
// アクティビティを処理 // アクティビティを処理
try { try {

View File

@ -1,6 +0,0 @@
import parse from '../../../mfm/parse';
export default function(text: string) {
if (!text) return [];
return parse(text).filter(t => t.type === 'emoji').map(t => (t as any).emoji);
}

View File

@ -8,9 +8,7 @@ import Note, { INote } from '../../../models/note';
import User from '../../../models/user'; import User from '../../../models/user';
import toHtml from '../misc/get-note-html'; import toHtml from '../misc/get-note-html';
import parseMfm from '../../../mfm/parse'; import parseMfm from '../../../mfm/parse';
import getEmojiNames from '../misc/get-emoji-names';
import Emoji, { IEmoji } from '../../../models/emoji'; import Emoji, { IEmoji } from '../../../models/emoji';
import { unique } from '../../../prelude/array';
export default async function renderNote(note: INote, dive = true): Promise<any> { export default async function renderNote(note: INote, dive = true): Promise<any> {
const promisedFiles: Promise<IDriveFile[]> = note.fileIds const promisedFiles: Promise<IDriveFile[]> = note.fileIds
@ -110,8 +108,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
const content = toHtml(Object.assign({}, note, { text })); const content = toHtml(Object.assign({}, note, { text }));
const emojiNames = unique(getEmojiNames(content)); const emojis = await getEmojis(note.emojis);
const emojis = await getEmojis(emojiNames);
const apemojis = emojis.map(emoji => renderEmoji(emoji)); const apemojis = emojis.map(emoji => renderEmoji(emoji));
const tag = [ const tag = [
@ -141,12 +138,10 @@ async function getEmojis(names: string[]): Promise<IEmoji[]> {
if (names == null || names.length < 1) return []; if (names == null || names.length < 1) return [];
const emojis = await Promise.all( const emojis = await Promise.all(
names.map(async name => { names.map(name => Emoji.findOne({
return await Emoji.findOne({ name,
name, host: null
host: null }))
});
})
); );
return emojis.filter(emoji => emoji != null); return emojis.filter(emoji => emoji != null);

View File

@ -12,15 +12,15 @@ export const meta = {
params: { params: {
name: { name: {
validator: $.str validator: $.str.min(1)
}, },
url: { url: {
validator: $.str validator: $.str.min(1)
}, },
aliases: { aliases: {
validator: $.arr($.str).optional, validator: $.arr($.str.min(1)).optional,
default: [] as string[] default: [] as string[]
} }
} }

View File

@ -59,6 +59,13 @@ export const meta = {
'ja-JP': 'インスタンスの紹介文' 'ja-JP': 'インスタンスの紹介文'
} }
}, },
maxNoteTextLength: {
validator: $.num.optional.min(1),
desc: {
'ja-JP': '投稿の最大文字数'
}
}
} }
}; };
@ -93,6 +100,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
set.description = ps.description; set.description = ps.description;
} }
if (ps.maxNoteTextLength) {
set.maxNoteTextLength = ps.maxNoteTextLength;
}
await Meta.update({}, { await Meta.update({}, {
$set: set $set: set
}, { upsert: true }); }, { upsert: true });

View File

@ -62,7 +62,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
swPublickey: config.sw ? config.sw.public_key : null, swPublickey: config.sw ? config.sw.public_key : null,
hidedTags: (me && me.isAdmin) ? met.hidedTags : undefined, hidedTags: (me && me.isAdmin) ? met.hidedTags : undefined,
bannerUrl: met.bannerUrl, bannerUrl: met.bannerUrl,
maxNoteTextLength: config.maxNoteTextLength, maxNoteTextLength: met.maxNoteTextLength || 1000,
emojis: emojis, emojis: emojis,

View File

@ -1,10 +1,20 @@
import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id';
const ms = require('ms'); const ms = require('ms');
import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; import { length } from 'stringz';
import Note, { INote, isValidCw, pack } from '../../../../models/note';
import User, { IUser } from '../../../../models/user'; import User, { IUser } from '../../../../models/user';
import DriveFile, { IDriveFile } from '../../../../models/drive-file'; import DriveFile, { IDriveFile } from '../../../../models/drive-file';
import create from '../../../../services/note/create'; import create from '../../../../services/note/create';
import define from '../../define'; import define from '../../define';
import Meta from '../../../../models/meta';
let maxNoteTextLength = 1000;
setInterval(() => {
Meta.findOne({}).then(m => {
if (m.maxNoteTextLength) maxNoteTextLength = m.maxNoteTextLength;
});
}, 3000);
export const meta = { export const meta = {
stability: 'stable', stability: 'stable',
@ -40,7 +50,9 @@ export const meta = {
}, },
text: { text: {
validator: $.str.optional.nullable.pipe(isValidText), validator: $.str.optional.nullable.pipe(text =>
length(text.trim()) <= maxNoteTextLength && text.trim() != ''
),
default: null as any, default: null as any,
desc: { desc: {
'ja-JP': '投稿内容' 'ja-JP': '投稿内容'

View File

@ -0,0 +1,35 @@
export type IMastodonEmoji = {
shortcode: string,
url: string,
static_url: string,
visible_in_picker: boolean
};
export async function toMastodonEmojis(emoji: any): Promise<IMastodonEmoji[]> {
return [{
shortcode: emoji.name,
url: emoji.url,
static_url: emoji.url, // TODO: Implement ensuring static emoji
visible_in_picker: true
}, ...(emoji.aliases as string[] || []).map(x => ({
shortcode: x,
url: emoji.url,
static_url: emoji.url,
visible_in_picker: true
}))];
}
export function toMisskeyEmojiSync(emoji: IMastodonEmoji) {
return {
name: emoji.shortcode,
url: emoji.url
};
}
export function toMisskeyEmojiWithAliasesSync(emoji: IMastodonEmoji, ...aliases: string[]) {
return {
name: emoji.shortcode,
aliases,
url: emoji.url
};
}

View File

@ -1,15 +1,22 @@
import * as Router from 'koa-router'; import * as Router from 'koa-router';
import User from '../../models/user'; import User from '../../../models/user';
import { toASCII } from 'punycode'; import { toASCII } from 'punycode';
import config from '../../config'; import config from '../../../config';
import Meta from '../../models/meta'; import Meta from '../../../models/meta';
import { ObjectID } from 'bson'; import { ObjectID } from 'bson';
const pkg = require('../../../package.json'); import Emoji from '../../../models/emoji';
import { toMastodonEmojis } from './emoji';
const pkg = require('../../../../package.json');
// Init router // Init router
const router = new Router(); const router = new Router();
router.get('/v1/custom_emojis', async ctx => ctx.body = {}); router.get('/v1/custom_emojis', async ctx => ctx.body =
(await Emoji.find({ host: null }, {
fields: {
_id: false
}
})).map(x => toMastodonEmojis(x)));
router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods! router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods!
const meta = await Meta.findOne() || {}; const meta = await Meta.findOne() || {};
@ -34,6 +41,11 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement
notesCount: 0 notesCount: 0
}; };
const acct = maintainer.host ? `${maintainer.username}@${maintainer.host}` : maintainer.username; const acct = maintainer.host ? `${maintainer.username}@${maintainer.host}` : maintainer.username;
const emojis = (await Emoji.find({ host: null }, {
fields: {
_id: false
}
})).map(toMastodonEmojis);
ctx.body = { ctx.body = {
uri: config.hostname, uri: config.hostname,
@ -73,7 +85,7 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement
followers_count: maintainer.followersCount, followers_count: maintainer.followersCount,
following_count: maintainer.followingCount, following_count: maintainer.followingCount,
statuses_count: maintainer.notesCount, statuses_count: maintainer.notesCount,
emojis: [], emojis: emojis,
moved: null, moved: null,
fields: null fields: null
} }

View File

@ -109,7 +109,7 @@ if (!config.github || !redis) {
} }
const params = { const params = {
redirect_uri: `${config.url}:8089/api/gh/cb`, redirect_uri: `${config.url}/api/gh/cb`,
scope: ['read:user'], scope: ['read:user'],
state: uuid() state: uuid()
}; };
@ -122,7 +122,7 @@ if (!config.github || !redis) {
const sessid = uuid(); const sessid = uuid();
const params = { const params = {
redirect_uri: `${config.url}:8089/api/gh/cb`, redirect_uri: `${config.url}/api/gh/cb`,
scope: ['read:user'], scope: ['read:user'],
state: uuid() state: uuid()
}; };

View File

@ -456,8 +456,8 @@ function extractHashtags(tokens: ReturnType<typeof parse>): string[] {
function extractEmojis(tokens: ReturnType<typeof parse>): string[] { function extractEmojis(tokens: ReturnType<typeof parse>): string[] {
// Extract emojis // Extract emojis
const emojis = tokens const emojis = tokens
.filter(t => t.type == 'emoji') .filter(t => t.type == 'emoji' && t.name)
.map(t => (t as TextElementEmoji).emoji) .map(t => (t as TextElementEmoji).name)
.filter(emoji => emoji.length <= 100); .filter(emoji => emoji.length <= 100);
return unique(emojis); return unique(emojis);

View File

@ -16,7 +16,7 @@ describe('Text', () => {
{ type: 'text', content: ' '}, { type: 'text', content: ' '},
{ type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }, { type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
{ type: 'text', content: ' お腹ペコい ' }, { type: 'text', content: ' お腹ペコい ' },
{ type: 'emoji', content: ':cat:', emoji: 'cat'}, { type: 'emoji', content: ':cat:', name: 'cat'},
{ type: 'text', content: ' '}, { type: 'text', content: ' '},
{ type: 'hashtag', content: '#yryr', hashtag: 'yryr' } { type: 'hashtag', content: '#yryr', hashtag: 'yryr' }
], tokens); ], tokens);
@ -182,15 +182,20 @@ describe('Text', () => {
it('emoji', () => { it('emoji', () => {
const tokens1 = analyze(':cat:'); const tokens1 = analyze(':cat:');
assert.deepEqual([ assert.deepEqual([
{ type: 'emoji', content: ':cat:', emoji: 'cat'} { type: 'emoji', content: ':cat:', name: 'cat' }
], tokens1); ], tokens1);
const tokens2 = analyze(':cat::cat::cat:'); const tokens2 = analyze(':cat::cat::cat:');
assert.deepEqual([ assert.deepEqual([
{ type: 'emoji', content: ':cat:', emoji: 'cat'}, { type: 'emoji', content: ':cat:', name: 'cat' },
{ type: 'emoji', content: ':cat:', emoji: 'cat'}, { type: 'emoji', content: ':cat:', name: 'cat' },
{ type: 'emoji', content: ':cat:', emoji: 'cat'} { type: 'emoji', content: ':cat:', name: 'cat' }
], tokens2); ], tokens2);
const tokens3 = analyze('🍎');
assert.deepEqual([
{ type: 'emoji', content: '🍎', emoji: '🍎' }
], tokens3);
}); });
it('block code', () => { it('block code', () => {