Merge branch 'develop'

This commit is contained in:
syuilo 2020-03-21 13:36:41 +09:00
commit 6fb7721798
110 changed files with 964 additions and 670 deletions

View File

@ -88,7 +88,9 @@ redis:
#elasticsearch: #elasticsearch:
# host: localhost # host: localhost
# port: 9200 # port: 9200
# pass: null # ssl: false
# user:
# pass:
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────

View File

@ -21,6 +21,7 @@ jobs:
- 5432:5432 - 5432:5432
env: env:
POSTGRES_DB: test-misskey POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis: redis:
image: redis:alpine image: redis:alpine
ports: ports:
@ -40,3 +41,13 @@ jobs:
run: yarn build run: yarn build
- name: Test - name: Test
run: yarn test run: yarn test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
- run: yarn install
- run: yarn lint

4
.mocharc.json Normal file
View File

@ -0,0 +1,4 @@
{
"timeout": 30000,
"slow": 1000
}

View File

@ -129,7 +129,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td> <td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td> <td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td>
<td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td> <td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td>
@ -140,7 +139,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td> <td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td>
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td> <td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td>
<td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td> <td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28779508/3cd4cb7f017f4ee0864341e3464d42f9/1.png?token-time=2145916800&token-hash=eGQtR15be44kgvh8fw2Jx8Db4Bv15YBp2ldxh0EKRxA%3D" alt="S Y" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28779508/3cd4cb7f017f4ee0864341e3464d42f9/1.png?token-time=2145916800&token-hash=eGQtR15be44kgvh8fw2Jx8Db4Bv15YBp2ldxh0EKRxA%3D" alt="S Y" width="100"></td>
@ -167,6 +165,8 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
@ -175,6 +175,8 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td> <td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td>
<td><a href="https://www.patreon.com/user?u=28295158">012</a></td>
<td><a href="https://www.patreon.com/nijimiss">nafuchoco</a></td>
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td> <td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td> <td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td> <td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
@ -183,18 +185,18 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td> <td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi" width="100"></td> <td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td> <td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=23932002">nenohi</a></td> <td><a href="https://www.patreon.com/user?u=23932002">nenohi</a></td>
<td><a href="https://www.patreon.com/efertone">Efertone</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Fri, 21 Feb 2020 13:39:06 UTC **Last updated:** Tue, 17 Mar 2020 18:57:08 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
[backer-url]: #backers [backer-url]: #backers

View File

@ -84,7 +84,7 @@ clickToShow: "クリックして表示"
sensitive: "閲覧注意" sensitive: "閲覧注意"
add: "追加" add: "追加"
reaction: "リアクション" reaction: "リアクション"
reactionSettingDescription: "リアクションピッカーに表示するリアクションを改行で区切って設定します。" reactionSettingDescription: "リアクションピッカーに表示するリアクションを設定します。"
rememberNoteVisibility: "公開範囲を記憶する" rememberNoteVisibility: "公開範囲を記憶する"
renameFile: "ファイル名を変更" renameFile: "ファイル名を変更"
attachCancel: "添付取り消し" attachCancel: "添付取り消し"
@ -168,6 +168,7 @@ intro: "Misskeyのインストールが完了しました管理者アカウ
done: "完了" done: "完了"
processing: "処理中" processing: "処理中"
preview: "プレビュー" preview: "プレビュー"
default: "デフォルト"
noCustomEmojis: "絵文字はありません" noCustomEmojis: "絵文字はありません"
customEmojisOfRemote: "リモートの絵文字" customEmojisOfRemote: "リモートの絵文字"
noJobs: "ジョブはありません" noJobs: "ジョブはありません"
@ -422,16 +423,28 @@ hideThisNote: "このノートを非表示"
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示する" showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示する"
objectStorage: "オブジェクトストレージ" objectStorage: "オブジェクトストレージ"
useObjectStorage: "オブジェクトストレージを使用" useObjectStorage: "オブジェクトストレージを使用"
objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "参照に使用するURL。CDNやProxyを使用している場合はそのURL、S3: 'https://<bucket>.s3.amazonaws.com'、GCS等: 'https://storage.googleapis.com/<bucket>'。"
objectStorageBucket: "Bucket"
objectStorageBucketDesc: "使用サービスのbucket名を指定してください。"
objectStoragePrefix: "Prefix"
objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されます。"
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
objectStorageUseSSL: "SSLを使用する"
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全て削除" deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する" showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
newNoteRecived: "新しいノートがあります" newNoteRecived: "新しいノートがあります"
useNotificationsPopup: "通知一覧をポップアップで表示"
sounds: "サウンド" sounds: "サウンド"
listen: "聴く" listen: "聴く"
none: "なし" none: "なし"
volume: "音量" volume: "音量"
details: "詳細" details: "詳細"
chooseEmoji: "絵文字を選択"
_sfx: _sfx:
note: "ノート" note: "ノート"

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class noteReactionLength1582875306439 implements MigrationInterface {
name = 'noteReactionLength1582875306439'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(128)`, undefined);
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>", "author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.21.0", "version": "12.22.0",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@ -21,7 +21,7 @@
"gulp": "gulp build", "gulp": "gulp build",
"clean": "gulp clean", "clean": "gulp clean",
"cleanall": "gulp cleanall", "cleanall": "gulp cleanall",
"lint": "gulp lint", "lint": "tslint 'src/**/*.ts'",
"test": "cross-env TS_NODE_FILES=true gulp test", "test": "cross-env TS_NODE_FILES=true gulp test",
"format": "gulp format" "format": "gulp format"
}, },

View File

@ -171,6 +171,7 @@ declare module 'jsrsasign' {
public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV; public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV;
// tslint:disable-next-line:bool-param-default
public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V; public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V;
public static hextooidstr(hex: ASN1OIDV): OID; public static hextooidstr(hex: ASN1OIDV): OID;
@ -620,9 +621,7 @@ declare module 'jsrsasign' {
public encrypt(text: string): HexString | null; public encrypt(text: string): HexString | null;
public encryptOAEP(text: string, hash?: string, hashLen?: number): HexString | null; public encryptOAEP(text: string, hash?: string | ((s: string) => string), hashLen?: number): HexString | null;
public encryptOAEP(text: string, hash?: (s: string) => string, hashLen?: number): HexString | null;
//// RSA PRIVATE //// RSA PRIVATE
@ -638,9 +637,7 @@ declare module 'jsrsasign' {
public decrypt(ctext: HexString): string; public decrypt(ctext: HexString): string;
public decryptOAEP(ctext: HexString, hash?: string, hashLen?: number): string | null; public decryptOAEP(ctext: HexString, hash?: string | ((s: string) => string), hashLen?: number): string | null;
public encryptOAEP(ctext: HexString, hash?: (s: string) => string, hashLen?: number): string | null;
//// RSA PEM //// RSA PEM

View File

@ -51,11 +51,7 @@
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span> <fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
</router-link> </router-link>
<template v-if="$store.getters.isSignedIn"> <template v-if="$store.getters.isSignedIn">
<button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.state.device.useNotificationsPopup"> <router-link class="item notifications" active-class="active" to="/my/notifications" ref="notificationButton">
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</button>
<router-link class="item notifications" active-class="active" to="/my/notifications" ref="notificationButton" v-else>
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span> <fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i> <i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</router-link> </router-link>
@ -149,17 +145,12 @@
<button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadSpecifiedNotes || $store.state.i.hasPendingReceivedFollowRequest || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement)"><fa :icon="faCircle"/></i></button> <button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadSpecifiedNotes || $store.state.i.hasPendingReceivedFollowRequest || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement)"><fa :icon="faCircle"/></i></button>
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button> <button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button> <button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
<button v-if="$store.getters.isSignedIn && $store.state.device.useNotificationsPopup" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button> <button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="$router.push('/my/notifications')" ref="notificationButton2"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn && !$store.state.device.useNotificationsPopup" class="button notifications _button" @click="$router.push('/my/notifications')" ref="notificationButton2"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> <button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div> </div>
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> <button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
<transition name="zoom-in-top">
<x-notifications v-if="notificationsOpen" class="notifications" ref="notifications"/>
</transition>
<stream-indicator v-if="$store.getters.isSignedIn"/> <stream-indicator v-if="$store.getters.isSignedIn"/>
</div> </div>
</template> </template>
@ -173,7 +164,6 @@ import { v4 as uuid } from 'uuid';
import i18n from './i18n'; import i18n from './i18n';
import { host, instanceName } from './config'; import { host, instanceName } from './config';
import { search } from './scripts/search'; import { search } from './scripts/search';
import contains from './scripts/contains';
import MkToast from './components/toast.vue'; import MkToast from './components/toast.vue';
const DESKTOP_THRESHOLD = 1100; const DESKTOP_THRESHOLD = 1100;
@ -183,7 +173,6 @@ export default Vue.extend({
components: { components: {
XClock: () => import('./components/header-clock.vue').then(m => m.default), XClock: () => import('./components/header-clock.vue').then(m => m.default),
XNotifications: () => import('./components/notifications.vue').then(m => m.default),
MkButton: () => import('./components/ui/button.vue').then(m => m.default), MkButton: () => import('./components/ui/button.vue').then(m => m.default),
XDraggable: () => import('vuedraggable'), XDraggable: () => import('vuedraggable'),
}, },
@ -194,7 +183,6 @@ export default Vue.extend({
pageKey: 0, pageKey: 0,
showNav: false, showNav: false,
searching: false, searching: false,
notificationsOpen: false,
accounts: [], accounts: [],
lists: [], lists: [],
connection: null, connection: null,
@ -226,23 +214,10 @@ export default Vue.extend({
watch:{ watch:{
$route(to, from) { $route(to, from) {
this.pageKey++; this.pageKey++;
this.notificationsOpen = false;
this.showNav = false; this.showNav = false;
this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
}, },
notificationsOpen(open) {
if (open) {
for (const el of Array.from(document.querySelectorAll('*'))) {
el.addEventListener('mousedown', this.onMousedown);
}
} else {
for (const el of Array.from(document.querySelectorAll('*'))) {
el.removeEventListener('mousedown', this.onMousedown);
}
}
},
isDesktop() { isDesktop() {
if (this.isDesktop) this.adjustWidgetsWidth(); if (this.isDesktop) this.adjustWidgetsWidth();
} }
@ -568,15 +543,6 @@ export default Vue.extend({
this.$root.sound('notification'); this.$root.sound('notification');
}, },
onMousedown(e) {
e.preventDefault();
if (!contains(this.$refs.notifications.$el, e.target) &&
!contains(this.$refs.notificationButton, e.target) &&
!contains(this.$refs.notificationButton2, e.target)
) this.notificationsOpen = false;
return false;
},
widgetFunc(id) { widgetFunc(id) {
const w = this.$refs[id][0]; const w = this.$refs[id][0];
if (w.func) w.func(); if (w.func) w.func();
@ -652,7 +618,7 @@ export default Vue.extend({
$header-height: 60px; $header-height: 60px;
$nav-width: 250px; $nav-width: 250px;
$nav-icon-only-width: 74px; $nav-icon-only-width: 74px;
$main-width: 700px; $main-width: 650px;
$ui-font-size: 1em; $ui-font-size: 1em;
$nav-icon-only-threshold: 1300px; $nav-icon-only-threshold: 1300px;
$nav-hide-threshold: 700px; $nav-hide-threshold: 700px;
@ -975,17 +941,21 @@ export default Vue.extend({
> main { > main {
width: $main-width; width: $main-width;
min-width: $main-width; min-width: $main-width;
box-shadow: 1px 0 0 0 var(--divider), -1px 0 0 0 var(--divider);
@media (max-width: $side-hide-threshold) { @media (max-width: $side-hide-threshold) {
min-width: 0; min-width: 0;
} }
> .content { > .content {
padding: 16px; > * {
box-sizing: border-box; &:not(.full) {
padding: var(--margin) 0;
}
@media (max-width: 500px) { &:not(.naked) {
padding: 8px; background: var(--pageBg);
}
} }
} }
@ -1023,6 +993,7 @@ export default Vue.extend({
> .widgets { > .widgets {
box-sizing: border-box; box-sizing: border-box;
margin-left: var(--margin);
@media (max-width: $side-hide-threshold) { @media (max-width: $side-hide-threshold) {
display: none; display: none;
@ -1175,34 +1146,5 @@ export default Vue.extend({
} }
} }
} }
> .notifications {
position: fixed;
top: 32px;
left: 0;
right: 0;
margin: 0 auto;
padding: 8px 8px 0 8px;
z-index: 10001;
width: 350px;
height: 400px;
box-sizing: border-box;
background: var(--vocsgcxy);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
border-radius: 6px;
box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
overflow: auto;
@media (max-width: 800px) {
width: 320px;
height: 350px;
}
@media (max-width: 500px) {
width: 290px;
height: 310px;
}
}
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv" name="list" tag="div" appear :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'"> <component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
<template v-for="(item, i) in items"> <template v-for="(item, i) in items">
<slot :item="item" :i="i"></slot> <slot :item="item" :i="i"></slot>
<div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)"> <div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)">
@ -109,8 +109,6 @@ export default Vue.extend({
line-height: 32px; line-height: 32px;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
border-radius: 64px;
background: var(--dateLabelBg);
color: var(--dateLabelFg); color: var(--dateLabelFg);
> span { > span {

View File

@ -27,8 +27,6 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.mjndxjcg { .mjndxjcg {
max-width: 350px;
margin: 0 auto;
padding: 32px; padding: 32px;
text-align: center; text-align: center;

View File

@ -1,12 +1,13 @@
<template> <template>
<div class="mk-google"> <div class="mk-google">
<input type="search" v-model="query" :placeholder="q"> <input type="search" v-model="query" :placeholder="q">
<button @click="search"><fa icon="search"/> {{ $t('search') }}</button> <button @click="search"><fa :icon="faSearch"/> {{ $t('search') }}</button>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n'; import i18n from '../i18n';
export default Vue.extend({ export default Vue.extend({
@ -14,7 +15,8 @@ export default Vue.extend({
props: ['q'], props: ['q'],
data() { data() {
return { return {
query: null query: null,
faSearch
}; };
}, },
mounted() { mounted() {
@ -42,27 +44,17 @@ export default Vue.extend({
width: 100%; width: 100%;
height: 40px; height: 40px;
font-size: 16px; font-size: 16px;
color: var(--googleSearchFg); border: solid 1px var(--divider);
background: var(--googleSearchBg);
border: solid 1px var(--googleSearchBorder);
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
&:hover {
border-color: var(--googleSearchHoverBorder);
}
} }
> button { > button {
flex-shrink: 0; flex-shrink: 0;
padding: 0 16px; padding: 0 16px;
border: solid 1px var(--googleSearchBorder); border: solid 1px var(--divider);
border-left: none; border-left: none;
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
&:hover {
background-color: var(--googleSearchHoverButton);
}
&:active { &:active {
box-shadow: 0 2px 4px rgba(#000, 0.15) inset; box-shadow: 0 2px 4px rgba(#000, 0.15) inset;
} }

View File

@ -9,7 +9,6 @@ import ellipsis from './ellipsis.vue';
import time from './time.vue'; import time from './time.vue';
import url from './url.vue'; import url from './url.vue';
import loading from './loading.vue'; import loading from './loading.vue';
import SequentialEntrance from './sequential-entrance.vue';
import error from './error.vue'; import error from './error.vue';
import streamIndicator from './stream-indicator.vue'; import streamIndicator from './stream-indicator.vue';
@ -23,5 +22,4 @@ Vue.component('mk-time', time);
Vue.component('mk-url', url); Vue.component('mk-url', url);
Vue.component('mk-loading', loading); Vue.component('mk-loading', loading);
Vue.component('mk-error', error); Vue.component('mk-error', error);
Vue.component('sequential-entrance', SequentialEntrance);
Vue.component('stream-indicator', streamIndicator); Vue.component('stream-indicator', streamIndicator);

View File

@ -90,7 +90,7 @@ export default Vue.extend({
> div { > div {
background-color: var(--fg); background-color: var(--fg);
border-radius: 6px; border-radius: 6px;
color: var(--secondary); color: var(--accentLighten);
display: inline-block; display: inline-block;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;

View File

@ -1,6 +1,6 @@
<template> <template>
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap"> <x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
<sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction" ref="items"> <div class="rrevdjwt" :class="{ left: align === 'left' }" ref="items">
<template v-for="(item, i) in items.filter(item => item !== undefined)"> <template v-for="(item, i) in items.filter(item => item !== undefined)">
<div v-if="item === null" class="divider" :key="i"></div> <div v-if="item === null" class="divider" :key="i"></div>
<span v-else-if="item.type === 'label'" class="label item" :key="i"> <span v-else-if="item.type === 'label'" class="label item" :key="i">
@ -28,7 +28,7 @@
<i v-if="item.indicate"><fa :icon="faCircle"/></i> <i v-if="item.indicate"><fa :icon="faCircle"/></i>
</button> </button>
</template> </template>
</sequential-entrance> </div>
</x-popup> </x-popup>
</template> </template>
@ -91,7 +91,7 @@ export default Vue.extend({
mounted() { mounted() {
if (this.viaKeyboard) { if (this.viaKeyboard) {
this.$nextTick(() => { this.$nextTick(() => {
focusNext(this.$refs.items.$slots.default[0].elm, true); focusNext(this.$refs.items.children[0], true);
}); });
} }
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="zlrxdaqttccpwhpaagdmkawtzklsccam"> <div class="wrpstxzv" v-size="[{ max: 450 }]">
<mk-avatar class="avatar" :user="note.user"/> <mk-avatar class="avatar" :user="note.user"/>
<div class="main"> <div class="main">
<x-note-header class="header" :note="note" :mini="true"/> <x-note-header class="header" :note="note" :mini="true"/>
@ -56,13 +56,12 @@ export default Vue.extend({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.zlrxdaqttccpwhpaagdmkawtzklsccam { .wrpstxzv {
display: flex; display: flex;
padding: 16px 32px; padding: 16px 32px;
font-size: 0.9em; font-size: 0.9em;
background: rgba(0, 0, 0, 0.03);
@media (max-width: 450px) { &.max-width_450px {
padding: 14px 16px; padding: 14px 16px;
} }

View File

@ -79,7 +79,7 @@
<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div> <div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
</div> </div>
</article> </article>
<x-sub v-for="note in replies" :key="note.id" :note="note"/> <x-sub v-for="note in replies" :key="note.id" :note="note" class="reply"/>
</div> </div>
</template> </template>
@ -684,6 +684,7 @@ export default Vue.extend({
.note { .note {
position: relative; position: relative;
transition: box-shadow 0.1s ease; transition: box-shadow 0.1s ease;
overflow: hidden;
&.max-width_500px { &.max-width_500px {
font-size: 0.9em; font-size: 0.9em;
@ -749,14 +750,6 @@ export default Vue.extend({
opacity: 1; opacity: 1;
} }
> *:first-child {
border-radius: var(--radius) var(--radius) 0 0;
}
> *:last-child {
border-radius: 0 0 var(--radius) var(--radius);
}
> .info { > .info {
display: flex; display: flex;
align-items: center; align-items: center;
@ -784,6 +777,11 @@ export default Vue.extend({
padding-top: 8px; padding-top: 8px;
} }
> .reply-to {
opacity: 0.7;
padding-bottom: 0;
}
> .renote { > .renote {
display: flex; display: flex;
align-items: center; align-items: center;
@ -937,5 +935,9 @@ export default Vue.extend({
} }
} }
} }
> .reply {
border-top: solid 1px var(--divider);
}
} }
</style> </style>

View File

@ -7,22 +7,22 @@
<mk-error v-if="error" @retry="init()"/> <mk-error v-if="error" @retry="init()"/>
<div class="more" v-if="more && reversed" style="margin-bottom: var(--margin);"> <div v-if="more && reversed" style="margin-bottom: var(--margin);">
<mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary> <button class="_panel _button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template> <template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template> <template v-if="moreFetching"><mk-loading inline/></template>
</mk-button> </button>
</div> </div>
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> <x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/> <x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
</x-list> </x-list>
<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);"> <div v-if="more && !reversed" style="margin-top: var(--margin);">
<mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary> <button class="_panel _button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template> <template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template> <template v-if="moreFetching"><mk-loading inline/></template>
</mk-button> </button>
</div> </div>
</div> </div>
</template> </template>
@ -111,16 +111,10 @@ export default Vue.extend({
&.max-width_500px { &.max-width_500px {
> .notes { > .notes {
> ::v-deep *:not(:last-child) { > ::v-deep *:not(:last-child) {
margin-bottom: var(--marginHalf); //margin-bottom: var(--marginHalf);
margin-bottom: 0;
} }
} }
} }
> .more > .button {
margin-left: auto;
margin-right: auto;
height: 48px;
width: 100%;
}
} }
</style> </style>

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="mk-notifications" :class="{ page }"> <div class="mk-notifications">
<x-list class="notifications" :items="items" v-slot="{ item: notification }"> <x-list class="notifications" :items="items" v-slot="{ item: notification }">
<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" :key="notification.id"/> <x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" :key="notification.id"/>
<x-notification v-else :notification="notification" :with-time="true" :full="true" class="notification" :class="{ _panel: page }" :key="notification.id"/> <x-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</x-list> </x-list>
<button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching"> <button class="_panel _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template> <template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template> <template v-if="moreFetching"><mk-loading inline/></template>
</button> </button>
<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p> <p class="empty" v-if="empty">{{ $t('noNotifications') }}</p>
@ -18,7 +18,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n'; import i18n from '../i18n';
import paging from '../scripts/paging'; import paging from '../scripts/paging';
import XNotification from './notification.vue'; import XNotification from './notification.vue';
@ -43,11 +42,6 @@ export default Vue.extend({
type: String, type: String,
required: false required: false
}, },
page: {
type: Boolean,
required: false,
default: false
}
}, },
data() { data() {
@ -60,7 +54,6 @@ export default Vue.extend({
includeTypes: this.type ? [this.type] : undefined includeTypes: this.type ? [this.type] : undefined
}) })
}, },
faSpinner
}; };
}, },
@ -94,35 +87,10 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-notifications { .mk-notifications {
&.page {
> .notifications { > .notifications {
> ::v-deep * { > ::v-deep * {
margin-bottom: var(--margin); //margin-bottom: var(--margin);
} margin-bottom: 0;
}
}
&:not(.page) {
> .notifications {
> ::v-deep * {
margin-bottom: 8px;
}
> .notification {
background: var(--panel);
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
}
> .more {
display: block;
width: 100%;
padding: 16px;
> [data-icon] {
margin-right: 4px;
} }
} }

View File

@ -53,7 +53,7 @@ import Vue from 'vue';
import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n'; import i18n from '../i18n';
import { erase } from '../../prelude/array'; import { erase } from '../../prelude/array';
import { addTimespan } from '../../prelude/time'; import { addTime } from '../../prelude/time';
import { formatDateTimeString } from '../../misc/format-time-string'; import { formatDateTimeString } from '../../misc/format-time-string';
import MkInput from './ui/input.vue'; import MkInput from './ui/input.vue';
import MkSelect from './ui/select.vue'; import MkSelect from './ui/select.vue';
@ -73,7 +73,7 @@ export default Vue.extend({
choices: ['', ''], choices: ['', ''],
multiple: false, multiple: false,
expiration: 'infinite', expiration: 'infinite',
atDate: formatDateTimeString(addTimespan(new Date(), 1, 'days'), 'yyyy-MM-dd'), atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'),
atTime: '00:00', atTime: '00:00',
after: 0, after: 0,
unit: 'second', unit: 'second',

View File

@ -17,7 +17,8 @@
:initial-note="initialNote" :initial-note="initialNote"
:instant="instant" :instant="instant"
@posted="onPosted" @posted="onPosted"
@cancel="onCanceled"/> @cancel="onCanceled"
style="border-radius: var(--radius);"/>
</transition> </transition>
</div> </div>
</div> </div>

View File

@ -586,7 +586,6 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.gafaadew { .gafaadew {
background: var(--panel); background: var(--panel);
border-radius: var(--radius);
> header { > header {
z-index: 1000; z-index: 1000;

View File

@ -0,0 +1,36 @@
<template>
<div class="jmgmzlwq _panel"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
export default Vue.extend({
i18n,
props: {
href: {
type: String,
required: true
},
},
data() {
return {
faExclamationTriangle
};
}
});
</script>
<style lang="scss" scoped>
.jmgmzlwq {
font-size: 0.8em;
padding: 16px;
> a {
margin-left: 4px;
color: var(--accent);
}
}
</style>

View File

@ -1,40 +0,0 @@
<template>
<transition-group v-if="$store.state.device.animation"
class="uupnnhew"
name="staggered"
tag="div"
appear
>
<slot></slot>
</transition-group>
<div v-else>
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
methods: {
focus() {
this.$slots.default[0].elm.focus();
}
},
});
</script>
<style lang="scss">
.uupnnhew {
> .staggered-enter {
opacity: 0;
transform: translateY(-64px);
}
@for $i from 1 through 30 {
> .staggered-enter-active:nth-child(#{$i}) {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1)), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1));
}
}
}
</style>

0
src/client/components/signin.vue Normal file → Executable file
View File

View File

@ -124,7 +124,6 @@ export default Vue.extend({
&.primary { &.primary {
color: #fff; color: #fff;
background: var(--accent); background: var(--accent);
box-shadow: 0 6px 16px var(--accentShadow);
&:not(:disabled):hover { &:not(:disabled):hover {
background: var(--jkhztclx); background: var(--jkhztclx);

View File

@ -110,6 +110,7 @@ export default Vue.extend({
> header { > header {
position: relative; position: relative;
box-shadow: 0 1px 0 0 var(--divider); box-shadow: 0 1px 0 0 var(--divider);
z-index: 1;
> .title { > .title {
margin: 0; margin: 0;

View File

@ -1,5 +1,5 @@
<template> <template>
<sequential-entrance class="cxiknjgy" :class="{ autoMargin }"> <div class="cxiknjgy" :class="{ autoMargin }">
<slot :items="items"></slot> <slot :items="items"></slot>
<div class="empty" v-if="empty" key="_empty_"> <div class="empty" v-if="empty" key="_empty_">
<slot name="empty"></slot> <slot name="empty"></slot>
@ -10,7 +10,7 @@
<template v-if="moreFetching"><mk-loading inline/></template> <template v-if="moreFetching"><mk-loading inline/></template>
</mk-button> </mk-button>
</div> </div>
</sequential-entrance> </div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -0,0 +1,138 @@
<template>
<div class="timctyfi" :class="{ focused, disabled }">
<div class="icon"><slot name="icon"></slot></div>
<span class="title"><slot name="title"></slot></span>
<input
type="range"
ref="input"
v-model="v"
:disabled="disabled"
:min="min"
:max="max"
:step="step"
:autofocus="autofocus"
@focus="focused = true"
@blur="focused = false"
@input="$emit('input', $event.target.value)"
/>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
props: {
value: {
type: Number,
required: false,
default: 0
},
disabled: {
type: Boolean,
required: false,
default: false
},
min: {
type: Number,
required: false,
default: 0
},
max: {
type: Number,
required: false,
default: 100
},
step: {
type: Number,
required: false,
default: 1
},
autofocus: {
type: Boolean,
required: false
}
},
data() {
return {
v: this.value,
focused: false
};
},
watch: {
value(v) {
this.v = parseFloat(v);
}
},
mounted() {
if (this.autofocus) {
this.$nextTick(() => {
this.$refs.input.focus();
});
}
}
});
</script>
<style lang="scss" scoped>
.timctyfi {
position: relative;
margin: 8px;
> .icon {
display: inline-block;
width: 24px;
text-align: center;
}
> .title {
pointer-events: none;
font-size: 16px;
color: var(--inputLabel);
overflow: hidden;
}
> input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: var(--xxubwiul);
height: 7px;
margin: 0 8px;
outline: 0;
border: 0;
border-radius: 7px;
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
box-sizing: content-box;
}
&::-moz-range-thumb {
-moz-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
}
}
}
</style>

View File

@ -158,6 +158,11 @@ export default Vue.extend({
outline: none; outline: none;
box-shadow: none; box-shadow: none;
color: var(--fg); color: var(--fg);
option,
optgroup {
background: var(--bg);
}
} }
> .prefix, > .prefix,

View File

@ -230,8 +230,8 @@ export default Vue.extend({
position: relative; position: relative;
display: block; display: block;
font-size: 14px; font-size: 14px;
box-shadow: 0 1px 4px var(--tyvedwbe); box-shadow: 0 0 0 1px var(--divider);
border-radius: 4px; border-radius: 6px;
overflow: hidden; overflow: hidden;
&:hover { &:hover {

View File

@ -6,15 +6,15 @@
<button class="_button" @click="close()"><fa :icon="faTimes"/></button> <button class="_button" @click="close()"><fa :icon="faTimes"/></button>
</div> </div>
<sequential-entrance class="users"> <div class="users">
<router-link v-for="(item, i) in items" class="user" :key="item.id" :to="extract ? extract(item) : item | userPage"> <router-link v-for="item in items" class="user" :key="item.id" :to="extract ? extract(item) : item | userPage">
<mk-avatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/> <mk-avatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/>
<div class="body"> <div class="body">
<mk-user-name :user="extract ? extract(item) : item" class="name"/> <mk-user-name :user="extract ? extract(item) : item" class="name"/>
<mk-acct :user="extract ? extract(item) : item" class="acct"/> <mk-acct :user="extract ? extract(item) : item" class="acct"/>
</div> </div>
</router-link> </router-link>
</sequential-entrance> </div>
<button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching"> <button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template> <template v-if="!moreFetching">{{ $t('loadMore') }}</template>

View File

@ -1,6 +1,6 @@
<template> <template>
<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }"> <x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
<sequential-entrance class="gqyayizv" :delay="30"> <div class="gqyayizv">
<button class="_button" @click="choose('public')" :class="{ active: v == 'public' }" data-index="1" key="public"> <button class="_button" @click="choose('public')" :class="{ active: v == 'public' }" data-index="1" key="public">
<div><fa :icon="faGlobe"/></div> <div><fa :icon="faGlobe"/></div>
<div> <div>
@ -29,7 +29,7 @@
<span>{{ $t('_visibility.specifiedDescription') }}</span> <span>{{ $t('_visibility.specifiedDescription') }}</span>
</div> </div>
</button> </button>
</sequential-entrance> </div>
</x-popup> </x-popup>
</template> </template>

View File

@ -81,14 +81,14 @@ if (lang == null) {
// Detect the user agent // Detect the user agent
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
let isMobile = /mobile|iphone|ipad|android/.test(ua); const isMobile = /mobile|iphone|ipad|android/.test(ua);
// Get the <head> element // Get the <head> element
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];
// If mobile, insert the viewport meta tag // If mobile, insert the viewport meta tag
if (isMobile || window.innerWidth <= 1024) { if (isMobile || window.innerWidth <= 1024) {
const viewport = document.getElementsByName("viewport").item(0); const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content', viewport.setAttribute('content',
`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`); `${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
head.appendChild(viewport); head.appendChild(viewport);

View File

@ -123,7 +123,12 @@ export default class MiOS extends EventEmitter {
}); });
} else { } else {
// Get token from localStorage // Get token from localStorage
const i = localStorage.getItem('i'); let i = localStorage.getItem('i');
// 連携ログインの場合用にCookieを参照する
if (i == null || i === 'null') {
i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
}
fetchme(i, me => { fetchme(i, me => {
if (me) { if (me) {

5
src/client/pages/auth.vue Normal file → Executable file
View File

@ -26,7 +26,7 @@
</div> </div>
<div class="signin" v-else> <div class="signin" v-else>
<h1>{{ $t('sign-in') }}</h1> <h1>{{ $t('sign-in') }}</h1>
<mk-signin/> <mk-signin @login="onLogin"/>
</div> </div>
</template> </template>
@ -85,6 +85,9 @@ export default Vue.extend({
if (this.session.app.callbackUrl) { if (this.session.app.callbackUrl) {
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`; location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
} }
}, onLogin(res) {
localStorage.setItem('i', res.i);
location.reload();
} }
} }
}); });

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="naked full">
<portal to="header"> <portal to="header">
<button @click="menu" class="_button _jmoebdiw_"> <button @click="menu" class="_button _jmoebdiw_">
<fa :icon="faCloud" style="margin-right: 8px;"/> <fa :icon="faCloud" style="margin-right: 8px;"/>

View File

@ -11,12 +11,12 @@
<canvas ref="chart"></canvas> <canvas ref="chart"></canvas>
</div> </div>
<div class="_content" style="max-height: 180px; overflow: auto;"> <div class="_content" style="max-height: 180px; overflow: auto;">
<sequential-entrance :delay="15" v-if="jobs.length > 0"> <div v-if="jobs.length > 0">
<div v-for="(job, i) in jobs" :key="job[0]"> <div v-for="job in jobs" :key="job[0]">
<span>{{ job[0] }}</span> <span>{{ job[0] }}</span>
<span style="margin-left: 8px; opacity: 0.7;">({{ job[1] | number }} jobs)</span> <span style="margin-left: 8px; opacity: 0.7;">({{ job[1] | number }} jobs)</span>
</div> </div>
</sequential-entrance> </div>
<span v-else style="opacity: 0.5;">{{ $t('noJobs') }}</span> <span v-else style="opacity: 0.5;">{{ $t('noJobs') }}</span>
</div> </div>
</section> </section>

View File

@ -102,21 +102,20 @@
<div class="_content"> <div class="_content">
<mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch> <mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch>
<template v-if="useObjectStorage"> <template v-if="useObjectStorage">
<mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">URL</mk-input> <mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('objectStorageBaseUrl') }}<template #desc>{{ $t('objectStorageBaseUrlDesc') }}</template></mk-input>
<div class="_inputs"> <div class="_inputs">
<mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">Bucket</mk-input> <mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('objectStorageBucket') }}<template #desc>{{ $t('objectStorageBucketDesc') }}</template></mk-input>
<mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">Prefix</mk-input> <mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('objectStoragePrefix') }}<template #desc>{{ $t('objectStoragePrefixDesc') }}</template></mk-input>
</div> </div>
<mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">Endpoint</mk-input> <mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('objectStorageEndpoint') }}<template #desc>{{ $t('objectStorageEndpointDesc') }}</template></mk-input>
<div class="_inputs"> <div class="_inputs">
<mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">Region</mk-input> <mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('objectStorageRegion') }}<template #desc>{{ $t('objectStorageRegionDesc') }}</template></mk-input>
<mk-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">Port</mk-input>
</div> </div>
<div class="_inputs"> <div class="_inputs">
<mk-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Access key</mk-input> <mk-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Access key</mk-input>
<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input> <mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input>
</div> </div>
<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">SSL</mk-switch> <mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></mk-switch>
</template> </template>
</div> </div>
<div class="_footer"> <div class="_footer">

View File

@ -2,7 +2,7 @@
<div class="thvuemwp" :data-is-me="isMe"> <div class="thvuemwp" :data-is-me="isMe">
<mk-avatar class="avatar" :user="message.user"/> <mk-avatar class="avatar" :user="message.user"/>
<div class="content"> <div class="content">
<div class="balloon _panel" :data-no-text="message.text == null"> <div class="balloon" :data-no-text="message.text == null">
<button class="delete-button" v-if="isMe" :title="$t('delete')" @click="del"> <button class="delete-button" v-if="isMe" :title="$t('delete')" @click="del">
<img src="/assets/remove.png" alt="Delete"/> <img src="/assets/remove.png" alt="Delete"/>
</button> </button>
@ -243,13 +243,14 @@ export default Vue.extend({
} }
&:not([data-is-me]) { &:not([data-is-me]) {
padding-left: var(--margin);
> .content { > .content {
padding-left: 16px; padding-left: 16px;
padding-right: 32px; padding-right: 32px;
> .balloon { > .balloon {
$color: var(--panel); $color: var(--messageBg);
background: $color; background: $color;
&[data-no-text] { &[data-no-text] {
@ -279,6 +280,7 @@ export default Vue.extend({
&[data-is-me] { &[data-is-me] {
flex-direction: row-reverse; flex-direction: row-reverse;
padding-right: var(--margin);
> .content { > .content {
padding-right: 16px; padding-right: 16px;
@ -287,7 +289,6 @@ export default Vue.extend({
> .balloon { > .balloon {
background: $me-balloon-color; background: $me-balloon-color;
box-shadow: 0 6px 16px var(--accentShadow);
text-align: left; text-align: left;
&[data-no-text] { &[data-no-text] {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mk-messaging-room" <div class="mk-messaging-room naked"
@dragover.prevent.stop="onDragover" @dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop" @drop.prevent.stop="onDrop"
> >

View File

@ -5,7 +5,7 @@
<mk-button @click="start" primary class="start"><fa :icon="faPlus"/> {{ $t('startMessaging') }}</mk-button> <mk-button @click="start" primary class="start"><fa :icon="faPlus"/> {{ $t('startMessaging') }}</mk-button>
<sequential-entrance class="history" v-if="messages.length > 0" :delay="30"> <div class="history" v-if="messages.length > 0">
<router-link v-for="(message, i) in messages" <router-link v-for="(message, i) in messages"
class="message _panel" class="message _panel"
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
@ -30,7 +30,7 @@
</div> </div>
</div> </div>
</router-link> </router-link>
</sequential-entrance> </div>
<div class="no-history" v-if="!fetching && messages.length == 0"> <div class="no-history" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/> <img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div>{{ $t('noHistory') }}</div> <div>{{ $t('noHistory') }}</div>

View File

@ -70,11 +70,10 @@ export default Vue.extend({
}, },
mounted() { mounted() {
if (!document.cookie.match(/i=(\w+)/)) { document.cookie = `igi=${this.$store.state.i.token}; path=/;` +
document.cookie = `i=${this.$store.state.i.token}; path=/;` + ` max-age=31536000;` +
` domain=${document.location.hostname}; max-age=31536000;` +
(document.location.protocol.startsWith('https') ? ' secure' : ''); (document.location.protocol.startsWith('https') ? ' secure' : '');
}
this.$watch('integrations', () => { this.$watch('integrations', () => {
if (this.integrations.twitter) { if (this.integrations.twitter) {
if (this.twitterForm) this.twitterForm.close(); if (this.twitterForm) this.twitterForm.close();

View File

@ -2,7 +2,10 @@
<section class="_card"> <section class="_card">
<div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div> <div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
<div class="_content"> <div class="_content">
<mk-textarea v-model="reactions">{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }}</template></mk-textarea> <mk-input v-model="reactions" style="font-family: 'Segoe UI Emoji', 'Noto Color Emoji', Roboto, HelveticaNeue, Arial, sans-serif">
{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></template>
</mk-input>
<mk-button inline @click="setDefault"><fa :icon="faUndo"/> {{ $t('default') }}</mk-button>
</div> </div>
<div class="_footer"> <div class="_footer">
<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> <mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
@ -14,24 +17,26 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons'; import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
import MkTextarea from '../../components/ui/textarea.vue'; import { faUndo } from '@fortawesome/free-solid-svg-icons';
import MkInput from '../../components/ui/input.vue';
import MkButton from '../../components/ui/button.vue'; import MkButton from '../../components/ui/button.vue';
import MkReactionPicker from '../../components/reaction-picker.vue'; import MkReactionPicker from '../../components/reaction-picker.vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
components: { components: {
MkTextarea, MkInput,
MkButton, MkButton,
}, },
data() { data() {
return { return {
reactions: this.$store.state.settings.reactions.join('\n'), reactions: this.$store.state.settings.reactions.join(''),
changed: false, changed: false,
faLaugh, faSave, faEye faLaugh, faSave, faEye, faUndo
} }
}, },
@ -41,21 +46,40 @@ export default Vue.extend({
} }
}, },
computed: {
splited(): any {
return this.reactions.match(emojiRegexWithCustom);
},
},
methods: { methods: {
save() { save() {
this.$store.dispatch('settings/set', { key: 'reactions', value: this.reactions.trim().split('\n') }); this.$store.dispatch('settings/set', { key: 'reactions', value: this.splited });
this.changed = false; this.changed = false;
}, },
preview(ev) { preview(ev) {
const picker = this.$root.new(MkReactionPicker, { const picker = this.$root.new(MkReactionPicker, {
source: ev.currentTarget || ev.target, source: ev.currentTarget || ev.target,
reactions: this.reactions.trim().split('\n'), reactions: this.splited,
showFocus: false, showFocus: false,
}); });
picker.$once('chosen', reaction => { picker.$once('chosen', reaction => {
picker.close(); picker.close();
}); });
},
setDefault() {
this.reactions = '👍❤😆🤔😮🎉💢😥😇🍮';
},
async chooseEmoji(ev) {
const vm = this.$root.new(await import('../../components/emoji-picker.vue').then(m => m.default), {
source: ev.currentTarget || ev.target
}).$once('chosen', emoji => {
this.reactions += emoji;
vm.close();
});
} }
} }
}); });

View File

@ -1,24 +1,28 @@
<template> <template>
<div class="mk-note-page"> <div class="mk-note-page">
<portal to="avatar" v-if="note"><mk-avatar class="avatar" :user="note.user" :disable-preview="true"/></portal> <portal to="avatar" v-if="note"><mk-avatar class="avatar" :user="note.user" :disable-preview="true"/></portal>
<portal to="title" v-if="note">{{ $t('noteOf', { user: note.user.name }) }}</portal> <portal to="title" v-if="note">
<mfm
:text="$t('noteOf', { user: note.user.name || note.user.username })"
:plain="true" :nowrap="true" :custom-emojis="note.user.emojis" :is-note="false"
/>
</portal>
<transition :name="$store.state.device.animation ? 'zoom' : ''" mode="out-in">
<div v-if="note"> <div v-if="note">
<mk-button v-if="hasNext && !showNext" @click="showNext = true" primary style="margin: 0 auto var(--margin) auto;"><fa :icon="faChevronUp"/></mk-button> <button class="_panel _button" v-if="hasNext && !showNext" @click="showNext = true" style="margin: 0 auto var(--margin) auto;"><fa :icon="faChevronUp"/></button>
<x-notes v-if="showNext" ref="next" :pagination="next"/> <x-notes v-if="showNext" ref="next" :pagination="next"/>
<hr v-if="showNext"/> <hr v-if="showNext"/>
<mk-remote-caution v-if="note.user.host != null" :href="note.uri" style="margin-bottom: var(--margin)"/>
<x-note :note="note" :key="note.id" :detail="true"/> <x-note :note="note" :key="note.id" :detail="true"/>
<div v-if="error"> <div v-if="error">
<mk-error @retry="fetch()"/> <mk-error @retry="fetch()"/>
</div> </div>
<mk-button v-if="hasPrev && !showPrev" @click="showPrev = true" primary style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></mk-button> <button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
<hr v-if="showPrev"/> <hr v-if="showPrev"/>
<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/> <x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
</div> </div>
</transition>
</div> </div>
</template> </template>
@ -29,7 +33,7 @@ import i18n from '../i18n';
import Progress from '../scripts/loading'; import Progress from '../scripts/loading';
import XNote from '../components/note.vue'; import XNote from '../components/note.vue';
import XNotes from '../components/notes.vue'; import XNotes from '../components/notes.vue';
import MkButton from '../components/ui/button.vue'; import MkRemoteCaution from '../components/remote-caution.vue';
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
@ -41,7 +45,7 @@ export default Vue.extend({
components: { components: {
XNote, XNote,
XNotes, XNotes,
MkButton, MkRemoteCaution,
}, },
data() { data() {
return { return {

View File

@ -8,8 +8,10 @@
<section class="_card"> <section class="_card">
<div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div> <div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div>
<div class="_content"> <div class="_content">
{{ $t('volume') }} <mk-range v-model="sfxVolume" min="0" max="1" step="0.1">
<input type="range" v-model="sfxVolume" min="0" max="1" step="0.1"/> <fa slot="icon" :icon="volumeIcon"/>
<span slot="title">{{ $t('volume') }}</span>
</mk-range>
</div> </div>
<div class="_content"> <div class="_content">
<mk-select v-model="sfxNote"> <mk-select v-model="sfxNote">
@ -61,7 +63,6 @@
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template> <template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</mk-switch> </mk-switch>
<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch> <mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch>
<mk-switch v-model="useNotificationsPopup">{{ $t('useNotificationsPopup') }}</mk-switch>
</div> </div>
<div class="_content"> <div class="_content">
<mk-select v-model="lang"> <mk-select v-model="lang">
@ -85,12 +86,13 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faImage, faCog, faMusic, faPlay } from '@fortawesome/free-solid-svg-icons'; import { faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons';
import MkInput from '../../components/ui/input.vue'; import MkInput from '../../components/ui/input.vue';
import MkButton from '../../components/ui/button.vue'; import MkButton from '../../components/ui/button.vue';
import MkSwitch from '../../components/ui/switch.vue'; import MkSwitch from '../../components/ui/switch.vue';
import MkSelect from '../../components/ui/select.vue'; import MkSelect from '../../components/ui/select.vue';
import MkRadio from '../../components/ui/radio.vue'; import MkRadio from '../../components/ui/radio.vue';
import MkRange from '../../components/ui/range.vue';
import XTheme from './theme.vue'; import XTheme from './theme.vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { langs } from '../../config'; import { langs } from '../../config';
@ -128,6 +130,7 @@ export default Vue.extend({
MkSwitch, MkSwitch,
MkSelect, MkSelect,
MkRadio, MkRadio,
MkRange
}, },
data() { data() {
@ -136,7 +139,7 @@ export default Vue.extend({
lang: localStorage.getItem('lang'), lang: localStorage.getItem('lang'),
fontSize: localStorage.getItem('fontSize'), fontSize: localStorage.getItem('fontSize'),
sounds, sounds,
faImage, faCog, faMusic, faPlay faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute
} }
}, },
@ -171,14 +174,9 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); } set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); }
}, },
useNotificationsPopup: {
get() { return this.$store.state.device.useNotificationsPopup; },
set(value) { this.$store.commit('device/set', { key: 'useNotificationsPopup', value }); }
},
sfxVolume: { sfxVolume: {
get() { return this.$store.state.device.sfxVolume; }, get() { return this.$store.state.device.sfxVolume; },
set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value }); } set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
}, },
sfxNote: { sfxNote: {
@ -210,6 +208,12 @@ export default Vue.extend({
get() { return this.$store.state.device.sfxAntenna; }, get() { return this.$store.state.device.sfxAntenna; },
set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); } set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
}, },
volumeIcon: {
get() {
return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
}
}
}, },
watch: { watch: {

View File

@ -3,8 +3,7 @@
<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal> <portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> <portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
<div class="remote-caution _panel" v-if="user.host != null"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div> <mk-remote-caution v-if="user.host != null" :href="user.url" style="margin-bottom: var(--margin)"/>
<transition :name="$store.state.device.animation ? 'zoom' : ''" mode="out-in" appear>
<div class="profile _panel" :key="user.id"> <div class="profile _panel" :key="user.id">
<div class="banner-container" :style="style"> <div class="banner-container" :style="style">
<div class="banner" ref="banner" :style="style"></div> <div class="banner" ref="banner" :style="style"></div>
@ -64,7 +63,7 @@
</dd> </dd>
</dl> </dl>
</div> </div>
<div class="status" v-if="user.host === null"> <div class="status">
<router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }"> <router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }">
<b>{{ user.notesCount | number }}</b> <b>{{ user.notesCount | number }}</b>
<span>{{ $t('notes') }}</span> <span>{{ $t('notes') }}</span>
@ -79,12 +78,11 @@
</router-link> </router-link>
</div> </div>
</div> </div>
</transition>
<router-view :user="user"></router-view> <router-view :user="user"></router-view>
<template v-if="$route.name == 'user'"> <template v-if="$route.name == 'user'">
<sequential-entrance class="pins"> <div class="pins">
<x-note v-for="(note, i) in user.pinnedNotes" class="note" :note="note" :key="note.id" :detail="true" :pinned="true"/> <x-note v-for="note in user.pinnedNotes" class="note" :note="note" :key="note.id" :detail="true" :pinned="true"/>
</sequential-entrance> </div>
<mk-container :body-togglable="true" class="content"> <mk-container :body-togglable="true" class="content">
<template #header><fa :icon="faImage"/>{{ $t('images') }}</template> <template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
<div> <div>
@ -107,7 +105,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons'; import { faEllipsisH, faRobot, faLock, faBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons';
import { faCalendarAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons'; import { faCalendarAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import * as age from 's-age'; import * as age from 's-age';
import XUserTimeline from './index.timeline.vue'; import XUserTimeline from './index.timeline.vue';
@ -115,6 +113,7 @@ import XUserMenu from '../../components/user-menu.vue';
import XNote from '../../components/note.vue'; import XNote from '../../components/note.vue';
import MkFollowButton from '../../components/follow-button.vue'; import MkFollowButton from '../../components/follow-button.vue';
import MkContainer from '../../components/ui/container.vue'; import MkContainer from '../../components/ui/container.vue';
import MkRemoteCaution from '../../components/remote-caution.vue';
import Progress from '../../scripts/loading'; import Progress from '../../scripts/loading';
import parseAcct from '../../../misc/acct/parse'; import parseAcct from '../../../misc/acct/parse';
@ -124,6 +123,7 @@ export default Vue.extend({
XNote, XNote,
MkFollowButton, MkFollowButton,
MkContainer, MkContainer,
MkRemoteCaution,
XPhotos: () => import('./index.photos.vue').then(m => m.default), XPhotos: () => import('./index.photos.vue').then(m => m.default),
XActivity: () => import('./index.activity.vue').then(m => m.default), XActivity: () => import('./index.activity.vue').then(m => m.default),
}, },
@ -139,7 +139,7 @@ export default Vue.extend({
user: null, user: null,
error: null, error: null,
parallaxAnimationId: null, parallaxAnimationId: null,
faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
}; };
}, },
@ -217,17 +217,6 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-user-page { .mk-user-page {
> .remote-caution {
font-size: 0.8em;
padding: 16px;
margin-bottom: var(--margin);
> a {
margin-left: 4px;
color: var(--accent);
}
}
> .profile { > .profile {
position: relative; position: relative;
margin-bottom: var(--margin); margin-bottom: var(--margin);

View File

@ -12,14 +12,22 @@ type action = {
patterns: pattern[]; patterns: pattern[];
callback: Function; callback: Function;
allowRepeat: boolean;
}; };
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => { const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
const result = { const result = {
patterns: [], patterns: [],
callback: callback callback: callback,
allowRepeat: true
} as action; } as action;
if (patterns.match(/^\(.*\)$/) !== null) {
result.allowRepeat = false;
patterns = patterns.slice(1, -1);
}
result.patterns = patterns.split('|').map(part => { result.patterns = patterns.split('|').map(part => {
const pattern = { const pattern = {
which: [], which: [],
@ -77,6 +85,7 @@ export default {
const matched = match(e, action.patterns); const matched = match(e, action.patterns);
if (matched) { if (matched) {
if (!action.allowRepeat && e.repeat) return;
if (el._hotkey_global && match(e, targetReservedKeys)) return; if (el._hotkey_global && match(e, targetReservedKeys)) return;
e.preventDefault(); e.preventDefault();

View File

@ -40,7 +40,6 @@ const defaultDeviceSettings = {
animatedMfm: true, animatedMfm: true,
imageNewTab: false, imageNewTab: false,
showFixedPostForm: false, showFixedPostForm: false,
useNotificationsPopup: true,
sfxVolume: 0.3, sfxVolume: 0.3,
sfxNote: 'syuilo/down', sfxNote: 'syuilo/down',
sfxNoteMy: 'syuilo/up', sfxNoteMy: 'syuilo/up',
@ -101,6 +100,7 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('settings/init', {}); ctx.commit('settings/init', {});
ctx.commit('deviceUser/init', {}); ctx.commit('deviceUser/init', {});
localStorage.removeItem('i'); localStorage.removeItem('i');
document.cookie = `igi=; path=/`;
}, },
async switchAccount(ctx, i) { async switchAccount(ctx, i) {

View File

@ -3,7 +3,7 @@
:root { :root {
--radius: 8px; --radius: 8px;
--marginFull: 16px; --marginFull: 16px;
--marginHalf: 8px; --marginHalf: 10px;
--margin: var(--marginFull); --margin: var(--marginFull);
@ -230,7 +230,6 @@ hr {
@extend ._button; @extend ._button;
color: #fff; color: #fff;
background: var(--accent); background: var(--accent);
box-shadow: 0 6px 16px var(--accentShadow);
&:not(:disabled):hover { &:not(:disabled):hover {
background: var(--jkhztclx); background: var(--jkhztclx);
@ -276,23 +275,29 @@ hr {
} }
} }
._shadow {
box-shadow: 0 8px 32px var(--shadow);
@media (max-width: 700px) {
box-shadow: 0 4px 16px var(--shadow);
}
@media (max-width: 500px) {
box-shadow: 0 2px 8px var(--shadow);
}
}
._panel { ._panel {
@extend ._shadow;
position: relative; position: relative;
background: var(--panel); background: var(--panel);
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: 0 0 0 1px var(--divider);
}
main ._panel {
border-radius: 0;
box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider);
}
._panel ._panel {
border-radius: 0;
box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider);
}
._panel._button {
display: flex;
width: 100%;
min-height: 48px;
align-items: center;
justify-content: center;
} }
._card { ._card {

View File

@ -17,9 +17,10 @@
fgHighlighted: ':lighten<3<@fg', fgHighlighted: ':lighten<3<@fg',
html: '@bg', html: '@bg',
indicator: '@accent', indicator: '@accent',
panel: '#111213', panel: '#000',
shadow: 'rgba(0, 0, 0, 0.1)', shadow: 'rgba(0, 0, 0, 0.1)',
header: 'rgba(20, 20, 20, 0.75)', header: 'rgba(20, 20, 20, 0.75)',
pageBg: ':lighten<5<@bg',
navBg: '@panel', navBg: '@panel',
navFg: '@fg', navFg: '@fg',
navHoverFg: ':lighten<17<@fg', navHoverFg: ':lighten<17<@fg',
@ -33,8 +34,7 @@
divider: 'rgba(255, 255, 255, 0.1)', divider: 'rgba(255, 255, 255, 0.1)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)', scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
dateLabelBg: 'rgba(255, 255, 255, 0.08)', dateLabelFg: '@fg',
dateLabelFg: '#fff',
infoBg: '#253142', infoBg: '#253142',
infoFg: '#fff', infoFg: '#fff',
infoWarnBg: '#42321c', infoWarnBg: '#42321c',
@ -51,14 +51,13 @@
driveFolderBg: ':alpha<0.3<@accent', driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
badge: '#31b1ce', badge: '#31b1ce',
messageBg: ':lighten<5<@bg',
bonzsgfz: ':alpha<0<@bg', bonzsgfz: ':alpha<0<@bg',
pcncwizz: ':darken<2<@panel', pcncwizz: ':darken<2<@panel',
vocsgcxy: 'rgba(0, 0, 0, 0.5)',
yrnqrguo: 'rgba(255, 255, 255, 0.05)', yrnqrguo: 'rgba(255, 255, 255, 0.05)',
nwjktjjq: 'rgba(255, 255, 255, 0.1)', nwjktjjq: 'rgba(255, 255, 255, 0.1)',
geavgsxy: 'rgba(255, 255, 255, 0.05)', geavgsxy: 'rgba(255, 255, 255, 0.05)',
nhzhphzx: 'rgba(255, 255, 255, 0.15)', nhzhphzx: 'rgba(255, 255, 255, 0.15)',
tyvedwbe: 'rgba(0, 0, 0, 0.5)',
bwqtlupy: 'rgba(255, 255, 255, 0.05)', bwqtlupy: 'rgba(255, 255, 255, 0.05)',
jkhztclx: ':lighten<5<@accent', jkhztclx: ':lighten<5<@accent',
zbqjwygh: ':darken<5<@accent', zbqjwygh: ':darken<5<@accent',

View File

@ -20,6 +20,7 @@
panel: '#fff', panel: '#fff',
shadow: 'rgba(0, 0, 0, 0.1)', shadow: 'rgba(0, 0, 0, 0.1)',
header: 'rgba(255, 255, 255, 0.75)', header: 'rgba(255, 255, 255, 0.75)',
pageBg: '@bg',
navBg: '@panel', navBg: '@panel',
navFg: '@fg', navFg: '@fg',
navHoverFg: ':darken<17<@fg', navHoverFg: ':darken<17<@fg',
@ -33,8 +34,7 @@
divider: 'rgba(0, 0, 0, 0.1)', divider: 'rgba(0, 0, 0, 0.1)',
scrollbarHandle: 'rgba(0, 0, 0, 0.2)', scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
dateLabelBg: 'rgba(0, 0, 0, 0.5)', dateLabelFg: '@fg',
dateLabelFg: '#fff',
infoBg: '#e5f5ff', infoBg: '#e5f5ff',
infoFg: '#72818a', infoFg: '#72818a',
infoWarnBg: '#fff0db', infoWarnBg: '#fff0db',
@ -51,14 +51,13 @@
driveFolderBg: ':alpha<0.3<@accent', driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
badge: '#31b1ce', badge: '#31b1ce',
messageBg: '@panel',
bonzsgfz: ':alpha<0<@bg', bonzsgfz: ':alpha<0<@bg',
pcncwizz: ':darken<2<@panel', pcncwizz: ':darken<2<@panel',
vocsgcxy: 'rgba(255, 255, 255, 0.5)',
yrnqrguo: 'rgba(0, 0, 0, 0.05)', yrnqrguo: 'rgba(0, 0, 0, 0.05)',
nwjktjjq: 'rgba(0, 0, 0, 0.1)', nwjktjjq: 'rgba(0, 0, 0, 0.1)',
geavgsxy: 'rgba(0, 0, 0, 0.05)', geavgsxy: 'rgba(0, 0, 0, 0.05)',
nhzhphzx: 'rgba(0, 0, 0, 0.25)', nhzhphzx: 'rgba(0, 0, 0, 0.25)',
tyvedwbe: 'rgba(0, 0, 0, 0.1)',
bwqtlupy: 'rgba(0, 0, 0, 0.05)', bwqtlupy: 'rgba(0, 0, 0, 0.05)',
jkhztclx: ':lighten<5<@accent', jkhztclx: ':lighten<5<@accent',
zbqjwygh: ':darken<5<@accent', zbqjwygh: ':darken<5<@accent',

View File

@ -14,6 +14,5 @@
link: '@accent', link: '@accent',
mention: '@accent', mention: '@accent',
hashtag: '@accent', hashtag: '@accent',
dateLabelBg: 'rgb(204, 186, 188)',
}, },
} }

View File

@ -3,7 +3,7 @@
<mk-container :show-header="!props.compact" class="container"> <mk-container :show-header="!props.compact" class="container">
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template> <template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
<div class="tl"> <div>
<x-notifications/> <x-notifications/>
</div> </div>
</mk-container> </mk-container>
@ -81,10 +81,5 @@ export default define({
flex-grow: 1; flex-grow: 1;
} }
} }
.tl {
background: var(--bg);
padding: 8px;
}
} }
</style> </style>

View File

@ -14,7 +14,7 @@
</button> </button>
</template> </template>
<div class="tl"> <div>
<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/> <x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/>
</div> </div>
</mk-container> </mk-container>
@ -148,11 +148,5 @@ export default define({
flex-grow: 1; flex-grow: 1;
} }
} }
.tl {
padding: 8px;
background: var(--bg);
box-sizing: border-box;
}
} }
</style> </style>

View File

@ -27,9 +27,10 @@ export type Source = {
elasticsearch: { elasticsearch: {
host: string; host: string;
port: number; port: number;
pass: string;
index?: string;
ssl?: boolean; ssl?: boolean;
user?: string;
pass?: string;
index?: string;
}; };
proxy?: string; proxy?: string;

View File

@ -33,6 +33,10 @@ const index = {
// Init ElasticSearch connection // Init ElasticSearch connection
const client = config.elasticsearch ? new elasticsearch.Client({ const client = config.elasticsearch ? new elasticsearch.Client({
node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`, node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`,
auth: (config.elasticsearch.user && config.elasticsearch.pass) ? {
username: config.elasticsearch.user,
password: config.elasticsearch.pass
} : undefined,
pingTimeout: 30000 pingTimeout: 30000
}) : null; }) : null;

112
src/mfm/toString.ts Normal file
View File

@ -0,0 +1,112 @@
import { MfmForest, MfmTree } from './prelude';
import { nyaize } from '../misc/nyaize';
export type RestoreOptions = {
doNyaize?: boolean;
};
export function toString(tokens: MfmForest | null, opts?: RestoreOptions): string {
if (tokens === null) return '';
function appendChildren(children: MfmForest, opts?: RestoreOptions): string {
return children.map(t => handlers[t.node.type](t, opts)).join('');
}
const handlers: { [key: string]: (token: MfmTree, opts?: RestoreOptions) => string } = {
bold(token, opts) {
return `**${appendChildren(token.children, opts)}**`;
},
big(token, opts) {
return `***${appendChildren(token.children, opts)}***`;
},
small(token, opts) {
return `<small>${appendChildren(token.children, opts)}</small>`;
},
strike(token, opts) {
return `~~${appendChildren(token.children, opts)}~~`;
},
italic(token, opts) {
return `<i>${appendChildren(token.children, opts)}</i>`;
},
motion(token, opts) {
return `<motion>${appendChildren(token.children, opts)}</motion>`;
},
spin(token, opts) {
return `<spin>${appendChildren(token.children, opts)}</spin>`;
},
jump(token, opts) {
return `<jump>${appendChildren(token.children, opts)}</jump>`;
},
flip(token, opts) {
return `<flip>${appendChildren(token.children, opts)}</flip>`;
},
blockCode(token) {
return `\`\`\`${token.node.props.lang || ''}\n${token.node.props.code}\n\`\`\`\n`;
},
center(token, opts) {
return `<center>${appendChildren(token.children, opts)}</center>`;
},
emoji(token) {
return (token.node.props.emoji ? token.node.props.emoji : `:${token.node.props.name}:`);
},
hashtag(token) {
return `#${token.node.props.hashtag}`;
},
inlineCode(token) {
return `\`${token.node.props.code}\``;
},
mathInline(token) {
return `\\(${token.node.props.formula}\\)`;
},
mathBlock(token) {
return `\\[${token.node.props.formula}\\]`;
},
link(token, opts) {
return `[${appendChildren(token.children, opts)}](${token.node.props.url})`;
},
mention(token) {
return token.node.props.canonical;
},
quote(token) {
return `${appendChildren(token.children, {doNyaize: false}).replace(/^/gm,'>').trim()}\n`;
},
title(token, opts) {
return `[${appendChildren(token.children, opts)}]\n`;
},
text(token, opts) {
return (opts && opts.doNyaize) ? nyaize(token.node.props.text) : token.node.props.text;
},
url(token) {
return `<${token.node.props.url}>`;
},
search(token, opts) {
const query = token.node.props.query;
return `${(opts && opts.doNyaize ? nyaize(query) : query)} [search]\n`;
}
};
return appendChildren(tokens, { doNyaize: (opts && opts.doNyaize) || false }).trim();
}

View File

@ -24,3 +24,7 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
export function getNodeinfoLock(host: string, timeout = 30 * 1000) { export function getNodeinfoLock(host: string, timeout = 30 * 1000) {
return lock(`nodeinfo:${host}`, timeout); return lock(`nodeinfo:${host}`, timeout);
} }
export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {
return lock(`chart-insert:${lockKey}`, timeout);
}

View File

@ -0,0 +1,15 @@
import { Notes } from '../models';
export async function countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
const query = Notes.createQueryBuilder('note')
.where('note.userId = :userId', { userId })
.andWhere('note.renoteId = :renoteId', { renoteId })
// 指定した投稿を除く
if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId })
}
return await query.getCount();
}

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,5 @@
import rndstr from 'rndstr';
export function nyaize(text: string): string { export function nyaize(text: string): string {
const [toNyaize, exclusionMap] = exclude(text); return text
const nyaized = toNyaize
// ja-JP // ja-JP
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
// en-US // en-US
@ -13,34 +10,4 @@ export function nyaize(text: string): string {
)) ))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
return replaceExceptions(nyaized, exclusionMap);
}
function exclude(text: string): [string, Record<string, string>] {
const map: Record<string, string> = {};
function substitute(match: string): string {
let randomstr: string;
do {
randomstr = rndstr({ length: 16, chars: '🀀-🀫' });
} while(Object.prototype.hasOwnProperty.call(map, randomstr));
map[randomstr] = match;
return randomstr;
}
const replaced = text
.replace(/```(.+?)?\n([\s\S]+?)```(\n|$)/gm, match => substitute(match)) // code block
.replace(/`([^`\n]+?)`/g, match => substitute(match)) // inline code
.replace(/(https?:\/\/.*?)(?= |$)/gm, match => substitute(match)) // URL
.replace(/:([a-z0-9_+-]+):/gim, match => substitute(match)) // emoji
.replace(/#([^\s.,!?'"#:\/\[\]【】]+)/gm, match => substitute(match)) // hashtag
.replace(/@\w([\w-]*\w)?(?:@[\w.\-]+\w)?/gm, match => substitute(match)); // mention
return [replaced, map];
}
function replaceExceptions(text: string, map: Record<string, string>): string {
for (const rule in map) {
if (Object.prototype.hasOwnProperty.call(map, rule)) {
text = text.replace(rule, map[rule]);
}
}
return text;
} }

View File

@ -36,7 +36,7 @@ export class NoteReaction {
public note: Note | null; public note: Note | null;
@Column('varchar', { @Column('varchar', {
length: 128 length: 130
}) })
public reaction: string; public reaction: string;
} }

View File

@ -1,12 +1,13 @@
import { EntityRepository, Repository, In } from 'typeorm'; import { EntityRepository, Repository, In } from 'typeorm';
import { Note } from '../entities/note'; import { Note } from '../entities/note';
import { User } from '../entities/user'; import { User } from '../entities/user';
import { nyaize } from '../../misc/nyaize';
import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..'; import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
import { ensure } from '../../prelude/ensure'; import { ensure } from '../../prelude/ensure';
import { SchemaType } from '../../misc/schema'; import { SchemaType } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all'; import { awaitAll } from '../../prelude/await-all';
import { convertLegacyReaction, convertLegacyReactions } from '../../misc/reaction-lib'; import { convertLegacyReaction, convertLegacyReactions } from '../../misc/reaction-lib';
import { toString } from '../../mfm/toString';
import { parse } from '../../mfm/parse';
export type PackedNote = SchemaType<typeof packedNoteSchema>; export type PackedNote = SchemaType<typeof packedNoteSchema>;
@ -217,7 +218,8 @@ export class NoteRepository extends Repository<Note> {
}); });
if (packed.user.isCat && packed.text) { if (packed.user.isCat && packed.text) {
packed.text = nyaize(packed.text); const tokens = packed.text ? parse(packed.text) : [];
packed.text = toString(tokens, { doNyaize: true });
} }
if (!opts.skipHide) { if (!opts.skipHide) {

View File

@ -1,17 +1,15 @@
const dateTimeIntervals = { const dateTimeIntervals = {
'days': 86400000, 'day': 86400000,
'hours': 3600000, 'hour': 3600000,
'ms': 1,
}; };
export function DateUTC(time: number[]): Date { export function dateUTC(time: number[]): Date {
const r = new Date(0); return new Date(Date.UTC(...time));
r.setUTCFullYear(time[0], time[1], time[2]);
if (time[3]) r.setUTCHours(time[3], ...time.slice(4));
return r;
} }
export function isTimeSame(a: Date, b: Date): boolean { export function isTimeSame(a: Date, b: Date): boolean {
return (a.getTime() - b.getTime()) === 0; return a.getTime() === b.getTime();
} }
export function isTimeBefore(a: Date, b: Date): boolean { export function isTimeBefore(a: Date, b: Date): boolean {
@ -22,10 +20,10 @@ export function isTimeAfter(a: Date, b: Date): boolean {
return (a.getTime() - b.getTime()) > 0; return (a.getTime() - b.getTime()) > 0;
} }
export function addTimespan(x: Date, value: number, span: keyof typeof dateTimeIntervals): Date { export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
return new Date(x.getTime() + (value * dateTimeIntervals[span])); return new Date(x.getTime() + (value * dateTimeIntervals[span]));
} }
export function subtractTimespan(x: Date, value: number, span: keyof typeof dateTimeIntervals): Date { export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
return new Date(x.getTime() - (value * dateTimeIntervals[span])); return new Date(x.getTime() - (value * dateTimeIntervals[span]));
} }

View File

@ -7,10 +7,10 @@ import config from '../../config';
import { ILocalUser } from '../../models/entities/user'; import { ILocalUser } from '../../models/entities/user';
import { UserKeypairs } from '../../models'; import { UserKeypairs } from '../../models';
import { ensure } from '../../prelude/ensure'; import { ensure } from '../../prelude/ensure';
import * as httpsProxyAgent from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = config.proxy const agent = config.proxy
? new httpsProxyAgent(config.proxy) ? new HttpsProxyAgent(config.proxy)
: new https.Agent({ : new https.Agent({
lookup: cache.lookup, lookup: cache.lookup,
}); });

View File

@ -9,16 +9,12 @@ import { publishMainStream } from '../../../services/stream';
export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
if (redirect) { if (redirect) {
//#region Cookie //#region Cookie
const expires = 1000 * 60 * 60 * 24 * 365; // One Year ctx.cookies.set('igi', user.token, {
ctx.cookies.set('i', user.token, {
path: '/', path: '/',
domain: config.hostname,
// SEE: https://github.com/koajs/koa/issues/974 // SEE: https://github.com/koajs/koa/issues/974
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
secure: config.url.startsWith('https'), secure: config.url.startsWith('https'),
httpOnly: false, httpOnly: false
expires: new Date(Date.now() + expires),
maxAge: expires
}); });
//#endregion //#endregion

View File

@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
}, },
res: convertLog(activeUsersChart.schema), res: convertLog(activeUsersChart.schema),
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await activeUsersChart.getChart(ps.span as any, ps.limit!); return await activeUsersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
}); });

View File

@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
}, },
res: convertLog(driveChart.schema), res: convertLog(driveChart.schema),
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await driveChart.getChart(ps.span as any, ps.limit!); return await driveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
}); });

View File

@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
}, },
res: convertLog(federationChart.schema), res: convertLog(federationChart.schema),
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await federationChart.getChart(ps.span as any, ps.limit!); return await federationChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
}); });

View File

@ -26,6 +26,11 @@ export const meta = {
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
tag: { tag: {
validator: $.str, validator: $.str,
desc: { desc: {
@ -38,5 +43,5 @@ export const meta = {
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.tag); return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.tag);
}); });

View File

@ -26,6 +26,11 @@ export const meta = {
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
host: { host: {
validator: $.str, validator: $.str,
desc: { desc: {
@ -39,5 +44,5 @@ export const meta = {
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await instanceChart.getChart(ps.span as any, ps.limit!, ps.host); return await instanceChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.host);
}); });

View File

@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
}, },
res: convertLog(networkChart.schema), res: convertLog(networkChart.schema),
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await networkChart.getChart(ps.span as any, ps.limit!); return await networkChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
}); });

View File

@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
}, },
res: convertLog(notesChart.schema), res: convertLog(notesChart.schema),
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await notesChart.getChart(ps.span as any, ps.limit!); return await notesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
}); });

View File

@ -27,6 +27,11 @@ export const meta = {
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
userId: { userId: {
validator: $.type(ID), validator: $.type(ID),
desc: { desc: {
@ -40,5 +45,5 @@ export const meta = {
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.userId); return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
}); });

View File

@ -27,6 +27,11 @@ export const meta = {
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
userId: { userId: {
validator: $.type(ID), validator: $.type(ID),
desc: { desc: {
@ -40,5 +45,5 @@ export const meta = {
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.userId); return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
}); });

View File

@ -27,6 +27,11 @@ export const meta = {
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
userId: { userId: {
validator: $.type(ID), validator: $.type(ID),
desc: { desc: {
@ -40,5 +45,5 @@ export const meta = {
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.userId); return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
}); });

View File

@ -27,6 +27,11 @@ export const meta = {
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
userId: { userId: {
validator: $.type(ID), validator: $.type(ID),
desc: { desc: {
@ -40,5 +45,5 @@ export const meta = {
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.userId); return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
}); });

View File

@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
} }
}, },
offset: {
validator: $.optional.nullable.num,
default: null,
},
}, },
res: convertLog(usersChart.schema), res: convertLog(usersChart.schema),
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
return await usersChart.getChart(ps.span as any, ps.limit!); return await usersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
}); });

View File

@ -60,9 +60,9 @@ export default define(meta, async () => {
Notes.count({ where: { userHost: null }, cache: 3600000 }), Notes.count({ where: { userHost: null }, cache: 3600000 }),
Users.count({ cache: 3600000 }), Users.count({ cache: 3600000 }),
Users.count({ where: { host: null }, cache: 3600000 }), Users.count({ where: { host: null }, cache: 3600000 }),
federationChart.getChart('hour', 1).then(chart => chart.instance.total[0]), federationChart.getChart('hour', 1, null).then(chart => chart.instance.total[0]),
driveChart.getChart('hour', 1).then(chart => chart.local.totalSize[0]), driveChart.getChart('hour', 1, null).then(chart => chart.local.totalSize[0]),
driveChart.getChart('hour', 1).then(chart => chart.remote.totalSize[0]), driveChart.getChart('hour', 1, null).then(chart => chart.remote.totalSize[0]),
]); ]);
return { return {

View File

@ -1,7 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../define'; import define from '../../define';
import { Users } from '../../../../models'; import { Users } from '../../../../models';
import { User } from '../../../../models/entities/user';
export const meta = { export const meta = {
desc: { desc: {
@ -73,14 +72,17 @@ export default define(meta, async (ps, me) => {
q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
} }
q.orderBy('user.updatedAt', 'DESC');
const users = await q.take(ps.limit!).skip(ps.offset).getMany(); const users = await q.take(ps.limit!).skip(ps.offset).getMany();
return await Users.packMany(users, me, { detail: ps.detail }); return await Users.packMany(users, me, { detail: ps.detail });
} else { } else if (ps.username) {
let users = await Users.createQueryBuilder('user') let users = await Users.createQueryBuilder('user')
.where('user.host IS NULL') .where('user.host IS NULL')
.andWhere('user.isSuspended = FALSE') .andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit!) .take(ps.limit!)
.skip(ps.offset) .skip(ps.offset)
.getMany(); .getMany();
@ -90,6 +92,7 @@ export default define(meta, async (ps, me) => {
.where('user.host IS NOT NULL') .where('user.host IS NOT NULL')
.andWhere('user.isSuspended = FALSE') .andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit! - users.length) .take(ps.limit! - users.length)
.getMany(); .getMany();

View File

@ -74,6 +74,7 @@ export default define(meta, async (ps, me) => {
.where('user.host IS NULL') .where('user.host IS NULL')
.andWhere('user.isSuspended = FALSE') .andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit!) .take(ps.limit!)
.skip(ps.offset) .skip(ps.offset)
.getMany(); .getMany();
@ -83,6 +84,7 @@ export default define(meta, async (ps, me) => {
.where('user.host IS NOT NULL') .where('user.host IS NOT NULL')
.andWhere('user.isSuspended = FALSE') .andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit! - users.length) .take(ps.limit! - users.length)
.getMany(); .getMany();

View File

@ -13,7 +13,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.Context) { function getUserToken(ctx: Koa.Context) {
return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1]; return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
} }
function compareOrigin(ctx: Koa.Context) { function compareOrigin(ctx: Koa.Context) {
@ -113,14 +113,10 @@ router.get('/signin/discord', async ctx => {
response_type: 'code' response_type: 'code'
}; };
const expires = 1000 * 60 * 60; // 1h ctx.cookies.set('signin_with_discord_sid', sessid, {
ctx.cookies.set('signin_with_discord_session_id', sessid, {
path: '/', path: '/',
domain: config.host,
secure: config.url.startsWith('https'), secure: config.url.startsWith('https'),
httpOnly: true, httpOnly: true
expires: new Date(Date.now() + expires),
maxAge: expires
}); });
redis.set(sessid, JSON.stringify(params)); redis.set(sessid, JSON.stringify(params));
@ -135,7 +131,7 @@ router.get('/dc/cb', async ctx => {
const oauth2 = await getOAuth2(); const oauth2 = await getOAuth2();
if (!userToken) { if (!userToken) {
const sessid = ctx.cookies.get('signin_with_discord_session_id'); const sessid = ctx.cookies.get('signin_with_discord_sid');
if (!sessid) { if (!sessid) {
ctx.throw(400, 'invalid session'); ctx.throw(400, 'invalid session');
@ -199,7 +195,7 @@ router.get('/dc/cb', async ctx => {
} }
const profile = await UserProfiles.createQueryBuilder() const profile = await UserProfiles.createQueryBuilder()
.where('"integrations"->"discord"->"id" = :id', { id: id }) .where(`"integrations"->'discord'->>'id' = :id`, { id: id })
.andWhere('"userHost" IS NULL') .andWhere('"userHost" IS NULL')
.getOne(); .getOne();
@ -212,6 +208,7 @@ router.get('/dc/cb', async ctx => {
integrations: { integrations: {
...profile.integrations, ...profile.integrations,
discord: { discord: {
id: id,
accessToken: accessToken, accessToken: accessToken,
refreshToken: refreshToken, refreshToken: refreshToken,
expiresDate: expiresDate, expiresDate: expiresDate,

View File

@ -13,7 +13,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.Context) { function getUserToken(ctx: Koa.Context) {
return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1]; return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
} }
function compareOrigin(ctx: Koa.Context) { function compareOrigin(ctx: Koa.Context) {
@ -111,14 +111,10 @@ router.get('/signin/github', async ctx => {
state: uuid() state: uuid()
}; };
const expires = 1000 * 60 * 60; // 1h ctx.cookies.set('signin_with_github_sid', sessid, {
ctx.cookies.set('signin_with_github_session_id', sessid, {
path: '/', path: '/',
domain: config.host,
secure: config.url.startsWith('https'), secure: config.url.startsWith('https'),
httpOnly: true, httpOnly: true
expires: new Date(Date.now() + expires),
maxAge: expires
}); });
redis.set(sessid, JSON.stringify(params)); redis.set(sessid, JSON.stringify(params));
@ -133,7 +129,7 @@ router.get('/gh/cb', async ctx => {
const oauth2 = await getOath2(); const oauth2 = await getOath2();
if (!userToken) { if (!userToken) {
const sessid = ctx.cookies.get('signin_with_github_session_id'); const sessid = ctx.cookies.get('signin_with_github_sid');
if (!sessid) { if (!sessid) {
ctx.throw(400, 'invalid session'); ctx.throw(400, 'invalid session');
@ -192,7 +188,7 @@ router.get('/gh/cb', async ctx => {
} }
const link = await UserProfiles.createQueryBuilder() const link = await UserProfiles.createQueryBuilder()
.where('"integrations"->"github"->"id" = :id', { id: id }) .where(`"integrations"->'github'->>'id' = :id`, { id: id })
.andWhere('"userHost" IS NULL') .andWhere('"userHost" IS NULL')
.getOne(); .getOne();

View File

@ -12,7 +12,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.Context) { function getUserToken(ctx: Koa.Context) {
return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1]; return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
} }
function compareOrigin(ctx: Koa.Context) { function compareOrigin(ctx: Koa.Context) {
@ -102,14 +102,10 @@ router.get('/signin/twitter', async ctx => {
redis.set(sessid, JSON.stringify(twCtx)); redis.set(sessid, JSON.stringify(twCtx));
const expires = 1000 * 60 * 60; // 1h ctx.cookies.set('signin_with_twitter_sid', sessid, {
ctx.cookies.set('signin_with_twitter_session_id', sessid, {
path: '/', path: '/',
domain: config.host,
secure: config.url.startsWith('https'), secure: config.url.startsWith('https'),
httpOnly: true, httpOnly: true
expires: new Date(Date.now() + expires),
maxAge: expires
}); });
ctx.redirect(twCtx.url); ctx.redirect(twCtx.url);
@ -121,7 +117,7 @@ router.get('/tw/cb', async ctx => {
const twAuth = await getTwAuth(); const twAuth = await getTwAuth();
if (userToken == null) { if (userToken == null) {
const sessid = ctx.cookies.get('signin_with_twitter_session_id'); const sessid = ctx.cookies.get('signin_with_twitter_sid');
if (sessid == null) { if (sessid == null) {
ctx.throw(400, 'invalid session'); ctx.throw(400, 'invalid session');
@ -139,7 +135,7 @@ router.get('/tw/cb', async ctx => {
const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier); const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
const link = await UserProfiles.createQueryBuilder() const link = await UserProfiles.createQueryBuilder()
.where('"integrations"->"twitter"->"userId" = :id', { id: result.userId }) .where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId })
.andWhere('"userHost" IS NULL') .andWhere('"userHost" IS NULL')
.getOne(); .getOne();

View File

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import Channel from '../channel'; import Channel from '../channel';
import { Mutings, Notes } from '../../../../models'; import { Notes } from '../../../../models';
export default class extends Channel { export default class extends Channel {
public readonly chName = 'main'; public readonly chName = 'main';
@ -9,15 +9,14 @@ export default class extends Channel {
@autobind @autobind
public async init(params: any) { public async init(params: any) {
const mute = await Mutings.find({ muterId: this.user!.id });
// Subscribe main stream channel // Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => { this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
let { type, body } = data; const { type } = data;
let { body } = data;
switch (type) { switch (type) {
case 'notification': { case 'notification': {
if (mute.map(m => m.muteeId).includes(body.userId)) return; if (this.muting.includes(body.userId)) return;
if (body.note && body.note.isHidden) { if (body.note && body.note.isHidden) {
body.note = await Notes.pack(body.note.id, this.user, { body.note = await Notes.pack(body.note.id, this.user, {
detail: true detail: true
@ -26,7 +25,7 @@ export default class extends Channel {
break; break;
} }
case 'mention': { case 'mention': {
if (mute.map(m => m.muteeId).includes(body.userId)) return; if (this.muting.includes(body.userId)) return;
if (body.isHidden) { if (body.isHidden) {
body = await Notes.pack(body.id, this.user, { body = await Notes.pack(body.id, this.user, {
detail: true detail: true

View File

@ -8,9 +8,9 @@ import * as nestedProperty from 'nested-property';
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import Logger from '../logger'; import Logger from '../logger';
import { Schema } from '../../misc/schema'; import { Schema } from '../../misc/schema';
import { EntitySchema, getRepository, Repository, LessThan, MoreThanOrEqual } from 'typeorm'; import { EntitySchema, getRepository, Repository, LessThan, Between } from 'typeorm';
import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '../../prelude/time';
import { DateUTC, isTimeSame, isTimeBefore, subtractTimespan } from '../../prelude/time'; import { getChartInsertLock } from '../../misc/app-lock';
const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test'); const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test');
@ -133,6 +133,24 @@ export default abstract class Chart<T extends Record<string, any>> {
return Math.floor(x.getTime() / 1000); return Math.floor(x.getTime() / 1000);
} }
@autobind
private static parseDate(date: Date): [number, number, number, number, number, number, number] {
const y = date.getUTCFullYear();
const m = date.getUTCMonth();
const d = date.getUTCDate();
const h = date.getUTCHours();
const _m = date.getUTCMinutes();
const _s = date.getUTCSeconds();
const _ms = date.getUTCMilliseconds();
return [y, m, d, h, _m, _s, _ms];
}
@autobind
private static getCurrentDate() {
return Chart.parseDate(new Date());
}
@autobind @autobind
public static schemaToEntity(name: string, schema: Schema): EntitySchema { public static schemaToEntity(name: string, schema: Schema): EntitySchema {
return new EntitySchema({ return new EntitySchema({
@ -211,18 +229,6 @@ export default abstract class Chart<T extends Record<string, any>> {
return log as T; return log as T;
} }
@autobind
private getCurrentDate(): [number, number, number, number] {
const now = new Date();
const y = now.getUTCFullYear();
const m = now.getUTCMonth();
const d = now.getUTCDate();
const h = now.getUTCHours();
return [y, m, d, h];
}
@autobind @autobind
private getLatestLog(span: Span, group: string | null = null): Promise<Log | null> { private getLatestLog(span: Span, group: string | null = null): Promise<Log | null> {
return this.repository.findOne({ return this.repository.findOne({
@ -237,11 +243,11 @@ export default abstract class Chart<T extends Record<string, any>> {
@autobind @autobind
private async getCurrentLog(span: Span, group: string | null = null): Promise<Log> { private async getCurrentLog(span: Span, group: string | null = null): Promise<Log> {
const [y, m, d, h] = this.getCurrentDate(); const [y, m, d, h] = Chart.getCurrentDate();
const current = const current =
span == 'day' ? DateUTC([y, m, d]) : span == 'day' ? dateUTC([y, m, d, 0]) :
span == 'hour' ? DateUTC([y, m, d, h]) : span == 'hour' ? dateUTC([y, m, d, h]) :
null as never; null as never;
// 現在(今日または今のHour)のログ // 現在(今日または今のHour)のログ
@ -283,30 +289,35 @@ export default abstract class Chart<T extends Record<string, any>> {
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Initial commit created`); logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Initial commit created`);
} }
const date = Chart.dateToTimestamp(current);
const lockKey = `${this.name}:${date}:${group}:${span}`;
const unlock = await getChartInsertLock(lockKey);
try { try {
// ロック内でもう1回チェックする
const currentLog = await this.repository.findOne({
span: span,
date: date,
...(group ? { group: group } : {})
});
// ログがあればそれを返して終了
if (currentLog != null) return currentLog;
// 新規ログ挿入 // 新規ログ挿入
log = await this.repository.save({ log = await this.repository.save({
group: group, group: group,
span: span, span: span,
date: Chart.dateToTimestamp(current), date: date,
...Chart.convertObjectToFlattenColumns(data) ...Chart.convertObjectToFlattenColumns(data)
}); });
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): New commit created`); logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): New commit created`);
} catch (e) {
// duplicate key error
// 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
// その場合は再度最も新しいログを持ってくる
if (isDuplicateKeyValueError(e)) {
log = await this.getLatestLog(span, group) as Log;
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Commit duplicated`);
} else {
logger.error(e);
throw e;
}
}
return log; return log;
} finally {
unlock();
}
} }
@autobind @autobind
@ -373,12 +384,15 @@ export default abstract class Chart<T extends Record<string, any>> {
} }
@autobind @autobind
public async getChart(span: Span, range: number, group: string | null = null): Promise<ArrayValue<T>> { public async getChart(span: Span, amount: number, begin: Date | null, group: string | null = null): Promise<ArrayValue<T>> {
const [y, m, d, h] = this.getCurrentDate(); const [y, m, d, h, _m, _s, _ms] = begin ? Chart.parseDate(subtractTime(addTime(begin, 1, span), 1)) : Chart.getCurrentDate();
const [y2, m2, d2, h2] = begin ? Chart.parseDate(addTime(begin, 1, span)) : [] as never;
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
const gt = const gt =
span == 'day' ? subtractTimespan(DateUTC([y, m, d]), range, 'days') : span === 'day' ? subtractTime(begin ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
span == 'hour' ? subtractTimespan(DateUTC([y, m, d, h]), range, 'hours') : span === 'hour' ? subtractTime(begin ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
null as never; null as never;
// ログ取得 // ログ取得
@ -386,7 +400,7 @@ export default abstract class Chart<T extends Record<string, any>> {
where: { where: {
group: group, group: group,
span: span, span: span,
date: MoreThanOrEqual(Chart.dateToTimestamp(gt)) date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt))
}, },
order: { order: {
date: -1 date: -1
@ -432,10 +446,10 @@ export default abstract class Chart<T extends Record<string, any>> {
const chart: T[] = []; const chart: T[] = [];
// 整形 // 整形
for (let i = (range - 1); i >= 0; i--) { for (let i = (amount - 1); i >= 0; i--) {
const current = const current =
span == 'day' ? subtractTimespan(DateUTC([y, m, d]), i, 'days') : span === 'day' ? subtractTime(dateUTC([y, m, d, 0]), i, 'day') :
span == 'hour' ? subtractTimespan(DateUTC([y, m, d, h]), i, 'hours') : span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') :
null as never; null as never;
const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current)); const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));

View File

@ -217,7 +217,8 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
const upload = s3.upload(params); const upload = s3.upload(params);
await upload.promise(); const result = await upload.promise();
if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
} }
async function deleteOldFile(user: IRemoteUser) { async function deleteOldFile(user: IRemoteUser) {

View File

@ -1,23 +1,23 @@
import * as S3 from 'aws-sdk/clients/s3'; import * as S3 from 'aws-sdk/clients/s3';
import config from '../../config'; import config from '../../config';
import { Meta } from '../../models/entities/meta'; import { Meta } from '../../models/entities/meta';
import * as httpsProxyAgent from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import * as agentkeepalive from 'agentkeepalive'; import * as agentkeepalive from 'agentkeepalive';
const httpsAgent = config.proxy const httpsAgent = config.proxy
? new httpsProxyAgent(config.proxy) ? new HttpsProxyAgent(config.proxy)
: new agentkeepalive.HttpsAgent({ : new agentkeepalive.HttpsAgent({
freeSocketTimeout: 30 * 1000 freeSocketTimeout: 30 * 1000
}); });
export function getS3(meta: Meta) { export function getS3(meta: Meta) {
const conf = { const conf = {
endpoint: meta.objectStorageEndpoint, endpoint: meta.objectStorageEndpoint || undefined,
accessKeyId: meta.objectStorageAccessKey, accessKeyId: meta.objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey, secretAccessKey: meta.objectStorageSecretKey,
region: meta.objectStorageRegion, region: meta.objectStorageRegion || undefined,
sslEnabled: meta.objectStorageUseSSL, sslEnabled: meta.objectStorageUseSSL,
s3ForcePathStyle: true, s3ForcePathStyle: !!meta.objectStorageEndpoint,
httpOptions: { httpOptions: {
} }
} as S3.ClientConfiguration; } as S3.ClientConfiguration;

View File

@ -30,6 +30,7 @@ import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-erro
import { ensure } from '../../prelude/ensure'; import { ensure } from '../../prelude/ensure';
import { checkHitAntenna } from '../../misc/check-hit-antenna'; import { checkHitAntenna } from '../../misc/check-hit-antenna';
import { addNoteToAntenna } from '../add-note-to-antenna'; import { addNoteToAntenna } from '../add-note-to-antenna';
import { countSameRenotes } from '../../misc/count-same-renotes';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -236,7 +237,8 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
saveReply(data.reply, note); saveReply(data.reply, note);
} }
if (data.renote) { // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
incRenoteCount(data.renote); incRenoteCount(data.renote);
} }

View File

@ -11,6 +11,7 @@ import { Note } from '../../models/entities/note';
import { Notes, Users, Instances } from '../../models'; import { Notes, Users, Instances } from '../../models';
import { notesChart, perUserNotesChart, instanceChart } from '../chart'; import { notesChart, perUserNotesChart, instanceChart } from '../chart';
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager'; import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
import { countSameRenotes } from '../../misc/count-same-renotes';
/** /**
* 稿 * 稿
@ -20,7 +21,8 @@ import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
export default async function(user: User, note: Note, quiet = false) { export default async function(user: User, note: Note, quiet = false) {
const deletedAt = new Date(); const deletedAt = new Date();
if (note.renoteId) { // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
Notes.decrement({ id: note.renoteId }, 'renoteCount', 1); Notes.decrement({ id: note.renoteId }, 'renoteCount', 1);
Notes.decrement({ id: note.renoteId }, 'score', 1); Notes.decrement({ id: note.renoteId }, 'score', 1);
} }

View File

@ -2,14 +2,10 @@
* Tests of API (visibility) * Tests of API (visibility)
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/api-visibility.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/api-visibility.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/api-visibility.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/api-visibility.ts --require ts-node/register -g 'test name'
*
* If the tests not start, try set following enviroment variables:
* TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
* for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/ */
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';

View File

@ -2,14 +2,10 @@
* Tests of API * Tests of API
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/api.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/api.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/api.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/api.ts --require ts-node/register -g 'test name'
*
* If the tests not start, try set following enviroment variables:
* TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
* for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/ */
/* /*
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';

View File

@ -2,14 +2,10 @@
* Tests of chart engine * Tests of chart engine
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/chart.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/chart.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/chart.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/chart.ts --require ts-node/register -g 'test name'
*
* If the tests not start, try set following enviroment variables:
* TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
* for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/ */
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
@ -52,9 +48,9 @@ function initChartDb() {
} }
describe('Chart', () => { describe('Chart', () => {
let testChart: any; let testChart: TestChart;
let testGroupedChart: any; let testGroupedChart: TestGroupedChart;
let testUniqueChart: any; let testUniqueChart: TestUniqueChart;
let clock: lolex.InstalledClock<lolex.Clock>; let clock: lolex.InstalledClock<lolex.Clock>;
let connection: Connection; let connection: Connection;
@ -90,8 +86,8 @@ describe('Chart', () => {
it('Can updates', async(async () => { it('Can updates', async(async () => {
await testChart.increment(); await testChart.increment();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -113,8 +109,8 @@ describe('Chart', () => {
it('Can updates (dec)', async(async () => { it('Can updates (dec)', async(async () => {
await testChart.decrement(); await testChart.decrement();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -134,8 +130,8 @@ describe('Chart', () => {
})); }));
it('Empty chart', async(async () => { it('Empty chart', async(async () => {
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -159,8 +155,8 @@ describe('Chart', () => {
await testChart.increment(); await testChart.increment();
await testChart.increment(); await testChart.increment();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -186,8 +182,8 @@ describe('Chart', () => {
await testChart.increment(); await testChart.increment();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -213,8 +209,8 @@ describe('Chart', () => {
await testChart.increment(); await testChart.increment();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -239,8 +235,8 @@ describe('Chart', () => {
clock.tick('05:00:00'); clock.tick('05:00:00');
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -266,8 +262,8 @@ describe('Chart', () => {
clock.tick('05:00:00'); clock.tick('05:00:00');
await testChart.increment(); await testChart.increment();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -286,14 +282,70 @@ describe('Chart', () => {
}); });
})); }));
it('Can specify offset', async(async () => {
await testChart.increment();
clock.tick('01:00:00');
await testChart.increment();
const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
it('Can specify offset (floor time)', async(async () => {
clock.tick('00:30:00');
await testChart.increment();
clock.tick('01:30:00');
await testChart.increment();
const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
describe('Grouped', () => { describe('Grouped', () => {
it('Can updates', async(async () => { it('Can updates', async(async () => {
await testGroupedChart.increment('alice'); await testGroupedChart.increment('alice');
const aliceChartHours = await testGroupedChart.getChart('hour', 3, 'alice'); const aliceChartHours = await testGroupedChart.getChart('hour', 3, null, 'alice');
const aliceChartDays = await testGroupedChart.getChart('day', 3, 'alice'); const aliceChartDays = await testGroupedChart.getChart('day', 3, null, 'alice');
const bobChartHours = await testGroupedChart.getChart('hour', 3, 'bob'); const bobChartHours = await testGroupedChart.getChart('hour', 3, null, 'bob');
const bobChartDays = await testGroupedChart.getChart('day', 3, 'bob'); const bobChartDays = await testGroupedChart.getChart('day', 3, null, 'bob');
assert.deepStrictEqual(aliceChartHours, { assert.deepStrictEqual(aliceChartHours, {
foo: { foo: {
@ -335,8 +387,8 @@ describe('Chart', () => {
await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('bob'); await testUniqueChart.uniqueIncrement('bob');
const chartHours = await testUniqueChart.getChart('hour', 3); const chartHours = await testUniqueChart.getChart('hour', 3, null);
const chartDays = await testUniqueChart.getChart('day', 3); const chartDays = await testUniqueChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: [2, 0, 0], foo: [2, 0, 0],
@ -354,8 +406,8 @@ describe('Chart', () => {
await testChart.resync(); await testChart.resync();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {
@ -383,8 +435,8 @@ describe('Chart', () => {
await testChart.resync(); await testChart.resync();
const chartHours = await testChart.getChart('hour', 3); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3); const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
foo: { foo: {

View File

@ -2,14 +2,10 @@
* Tests of MFM * Tests of MFM
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/mfm.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/mfm.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/mfm.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/mfm.ts --require ts-node/register -g 'test name'
*
* If the tests not start, try set following enviroment variables:
* TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
* for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/ */
import * as assert from 'assert'; import * as assert from 'assert';

View File

@ -1,2 +0,0 @@
--timeout 30000
--slow 1000

View File

@ -2,14 +2,10 @@
* Tests of mute * Tests of mute
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/mute.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/mute.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/mute.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/mute.ts --require ts-node/register -g 'test name'
*
* If the tests not start, try set following enviroment variables:
* TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
* for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/ */
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';

View File

@ -2,14 +2,10 @@
* Tests of Note * Tests of Note
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/note.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/note.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/note.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/note.ts --require ts-node/register -g 'test name'
*
* If the tests not start, try set following enviroment variables:
* TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
* for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/ */
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';

View File

@ -2,10 +2,10 @@
* Tests of Maybe * Tests of Maybe
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/prelude/maybe.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/prelude/maybe.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/prelude/maybe.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/prelude/maybe.ts --require ts-node/register -g 'test name'
*/ */
import * as assert from 'assert'; import * as assert from 'assert';

View File

@ -2,14 +2,10 @@
* Tests of MFM * Tests of MFM
* *
* How to run the tests: * How to run the tests:
* > npx mocha test/reaction-lib.ts --require ts-node/register * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/reaction-lib.ts --require ts-node/register
* *
* To specify test: * To specify test:
* > npx mocha test/reaction-lib.ts --require ts-node/register -g 'test name' * > npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true npx mocha test/reaction-lib.ts --require ts-node/register -g 'test name'
*
* If the tests not start, try set following enviroment variables:
* TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
* for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
*/ */
/* /*

Some files were not shown because too many files have changed in this diff Show More