Compare commits

...

30 Commits

Author SHA1 Message Date
05d0620491 Merge branch 'develop' 2019-05-19 02:51:49 +09:00
5225139284 11.16.0 2019-05-19 02:51:28 +09:00
9b2e996cae 🎨 2019-05-19 02:47:31 +09:00
6a561342a4 🎨 2019-05-19 02:39:34 +09:00
429bed2f91 Refactor 2019-05-18 23:16:09 +09:00
200d593414 Add group menu 2019-05-18 22:57:34 +09:00
9dbe12135d Fix bug 2019-05-18 22:54:02 +09:00
318d7f2652 🎨 2019-05-18 22:50:57 +09:00
5a653531e2 Avoid error 2019-05-18 21:22:37 +09:00
c7cc3dcdfd ユーザーグループ
Resolve #3218
2019-05-18 20:36:33 +09:00
61f54f8f74 Fix bug 2019-05-18 00:38:33 +09:00
a3b27fe3a1 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-05-17 19:56:56 +09:00
380749051d ページにいいねできるように 2019-05-17 19:56:47 +09:00
ql3
81625f9fc5 Update gitignore to ignore config files for Intelij-IDEA (#4933) 2019-05-17 02:31:00 +09:00
d6ccb1725b Update API docs 2019-05-17 00:19:23 +09:00
5ce412aeda Merge branch 'develop' 2019-05-16 16:14:00 +09:00
fe6d88e410 11.15.0 2019-05-16 16:13:46 +09:00
a21357248f Dockerを使用している場合、アプデの際にマイグレを自動実行するように 2019-05-16 16:12:30 +09:00
70d710c9a9 管理画面でreCAPTCHAのプレビューを表示するように 2019-05-16 16:08:50 +09:00
183c82fb8d New Crowdin translations (#4924)
* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)
2019-05-16 13:11:56 +09:00
62cbb92154 Fix #4930 2019-05-16 12:59:09 +09:00
7d70126072 Merge branch 'develop' 2019-05-16 01:18:06 +09:00
54bfffa7b9 11.14.0 2019-05-16 01:16:41 +09:00
3f5b96bf62 Resolve #4928 2019-05-16 01:07:32 +09:00
3d8bbedf1b GIFのサムネイルが生成されないのを修正
#4728
2019-05-15 21:27:20 +09:00
23c9f6a6ca Resolve #4833 2019-05-15 20:41:01 +09:00
5ba8d4949d インスタンスの設定画面を整理 2019-05-15 20:29:47 +09:00
a6befdd541 Fix bug 2019-05-15 17:05:41 +09:00
e5409db0e8 Resolve #4925 2019-05-14 23:54:39 +09:00
678d610cd6 Update CHANGELOG.md 2019-05-14 21:27:20 +09:00
116 changed files with 3082 additions and 1163 deletions

View File

@ -6,8 +6,6 @@ mongodb:
db: misskey
user: syuilo
pass: ''
drive:
storage: 'db'
redis:
host: localhost
port: 6379

View File

@ -6,8 +6,6 @@ mongodb:
db: test-misskey
user: admin
pass: ''
drive:
storage: 'db'
# __REDIS__
redis:
host: localhost

View File

@ -78,61 +78,6 @@ redis:
# port: 9200
# pass: null
# ┌────────────────────────────────────┐
#───┘ File storage (Drive) configuration └──────────────────────
drive:
storage: 'fs'
# OR
#drive:
# storage: 'minio'
# bucket:
# prefix:
# config:
# endPoint:
# port:
# useSSL:
# accessKey:
# secretKey:
# S3/GCS example
#
# * Replace <endpoint> to
# S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
# GCS: use 'storage.googleapis.com'
#
# * Replace <region> to
# S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
# GCS: not needed (just delete the region line)
#
#drive:
# storage: 'minio'
# bucket: bucket-name
# prefix: files
# baseUrl: https://bucket-name.<endpoint>
# config:
# endPoint: <endpoint>
# region: <region>
# useSSL: true
# accessKey: XXX
# secretKey: YYY
# S3/GCS example (with CDN, custom domain)
#
#drive:
# storage: 'minio'
# bucket: drive.example.com
# prefix: files
# baseUrl: https://drive.example.com
# config:
# endPoint: <endpoint>
# region: <region>
# useSSL: true
# accessKey: XXX
# secretKey: YYY
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

19
.gitignore vendored
View File

@ -1,11 +1,23 @@
# Visual Studio Code
/.vscode
# Intelij-IDEA
/.idea
# Node.js
/node_modules
# yarn
yarn.lock
# config
/.config/*
!/.config/example.yml
!/.config/mongo_initdb_example.js
/.vscode
/node_modules
# misskey
/build
/built
built
/data
/.cache-loader
/db
@ -17,7 +29,6 @@ api-docs.json
*.log
/redis
*.code-workspace
yarn.lock
.DS_Store
/files
ormconfig.json

View File

@ -8,32 +8,13 @@ If you encounter any problems with updating, please try the following:
Migration
------------------------------
#### 1
`ormconfig.json`という名前で、Misskeyのインストール場所(package.jsonとかがあるディレクトリ)に新たなファイルを作る。中身は次のようにします:
``` json
{
"type": "postgres",
"host": "PostgreSQLのホスト",
"port": 5432,
"username": "PostgreSQLのユーザー名",
"password": "PostgreSQLのパスワード",
"database": "PostgreSQLのデータベース名",
"entities": ["src/models/entities/*.ts"],
"migrations": ["migration/*.ts"],
"cli": {
"migrationsDir": "migration"
}
}
```
上記の各種PostgreSQLの設定(ポートも)は、設定ファイルに書いてあるものをコピーしてください。
#### 2
```
npm i -g ts-node
```
#### 3
#### 2
```
ts-node ./node_modules/typeorm/cli.js migration:run
npm run migrate
```
How to migrate to v11 from v10
@ -73,6 +54,41 @@ mongodb:
8. master ブランチに戻す
9. enjoy
11.16.0 (2019/05/19)
--------------------
### 注意
このアップデートを適用した後、プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください
### ✨Improvements
* ユーザーグループ機能を追加
* ページに「いいね」できるように
* UIの改善
### 🐛Fixes
* トークを読み込むときに最大数指定できなかった問題を修正
11.15.0 (2019/05/16)
--------------------
### ✨Improvements
* 管理画面でreCAPTCHAのプレビューを表示するように
### 🐛Fixes
* オブジェクトストレージのリージョンの設定が反映されない問題を修正
11.14.0 (2019/05/16)
--------------------
### 注意
このバージョンからオブジェクトストレージの設定は設定ファイルではなく管理画面から行うようになりました。
オブジェクトストレージを使用している場合、アップデートした後管理画面にアクセスしオブジェクトストレージの設定を再度行ってください。
### ✨Improvements
* 特定のユーザーのファイルをすべて削除できるように
* インスタンスの設定画面を整理
### 🐛Fixes
* GIF画像のサムネイルが生成されないのを修正
* 管理画面の「ログ」で複数の除外条件を設定できない問題を修正
11.13.0 (2019/05/14)
--------------------
### 注意
@ -85,12 +101,13 @@ mongodb:
* ユーザーや外部インスタンスが生成するリンクにnofollowを追加
* リモートのユーザーページやートページにnoindexを追加
* 自分のユーザーメニューにはミュートなどを表示しないように
* デザインの調整
### 🐛Fixes
* インスタンスブロックを設定できない問題を修正
* ピン留め投稿の表示順がおかしい問題を修正
* 設定の「アップデートを確認」でメッセージが正しく表示されない問題を修正
* FFirefoxで自分のメニューが開けない問題を修正
* Firefoxで自分のメニューが開けない問題を修正
* Welcomeページのタグクラウドが動かない問題を修正
11.12.0 (2019/05/10)

View File

@ -199,7 +199,7 @@ const user = await Users.findOne(userId).then(ensure);
```
### Migration作成方法
コードの変更をした後、`ormconfig.json`書き方はCONTRIBUTING.mdを参照)を用意し、
コードの変更をした後、`ormconfig.json``npm run ormconfig`で生成)を用意し、
```
npm i -g ts-node

View File

@ -39,4 +39,4 @@ COPY --from=builder /misskey/node_modules ./node_modules
COPY --from=builder /misskey/built ./built
COPY . ./
CMD ["npm", "start"]
CMD ["npm", "migrateandstart"]

View File

@ -873,7 +873,6 @@ admin/views/index.vue:
users: "Uživatelé"
federation: "Z fedivesmíru"
announcements: "Oznámení"
hashtags: "Hashtagy"
queue: "Fronta úloh"
logs: "Logy"
back-to-misskey: "Zpět na Misskey"
@ -898,6 +897,7 @@ admin/views/instance.vue:
maintainer-config: "Informace o administrátorovi"
maintainer-name: "Jméno administrátora"
maintainer-email: "Kontakt na administrátora"
object-storage-endpoint: "Endpoint"
mb: "V megabajtech"
recaptcha-config: "nastavení služby reCAPTCHA"
recaptcha-info: "reCAPTCHA token je povinný. Můžete jej získat na https://www.google.com/recaptcha/intro/"

View File

@ -1063,7 +1063,6 @@ admin/views/index.vue:
users: "Users"
federation: "Federation"
announcements: "Announcements"
hashtags: "Hashtags"
abuse: "Abuse"
queue: "Job Queue"
logs: "Logs"
@ -1098,6 +1097,7 @@ admin/views/instance.vue:
maintainer-name: "Administrator name"
maintainer-email: "Contact Administrator"
drive-config: "Drive settings"
object-storage-endpoint: "Endpoint"
cache-remote-files: "Cache remote files"
cache-remote-files-desc: "Without this parameter, all remote files are linked to their host server directly. This will be an effective solution to save your server storage, however make remote files invisible to users who set direct-link disabled, since no thumbnail will be generated, increase traffic. It is recommended that this parameter set enabled."
local-drive-capacity-mb: "Volume of Drive per user"

View File

@ -910,7 +910,6 @@ admin/views/index.vue:
moderators: "Moderadores"
users: "Usuarios"
federation: "Federado"
hashtags: "Hashtags"
queue: "Cola de trabajos"
logs: "Registros"
back-to-misskey: "Volver a Misskey"

View File

@ -68,7 +68,7 @@ common:
explore: "Découvrir"
following: "Suit"
followers: "Abonné·e·s"
favorites: "Mettre cette note en favoris"
favorites: "Favorites"
permissions:
"read:account": "Afficher les informations du compte"
"write:account": "Mettre à jour les informations de votre compte"
@ -1046,7 +1046,6 @@ admin/views/index.vue:
users: "Utilisateurs"
federation: "Fédération"
announcements: "Annonces"
hashtags: "Hashtags"
abuse: "Abus"
queue: "File dattente"
logs: "Journaux"
@ -1073,14 +1072,19 @@ admin/views/instance.vue:
instance-name: "Nom de linstance"
instance-description: "Description de linstance"
host: "Hôte"
icon-url: "URL de l'icône"
logo-url: "URL do logo"
banner-url: "URL de limage de la bannière"
error-image-url: "URL de limage derreur"
languages: "Langue de linstance"
languages-desc: "Vous pouvez en définir plus dune, séparées par des espaces."
tos-url: "URL des conditions d'utilisation"
repository-url: "URL du dépôt"
maintainer-config: "Informations de ladministrateur"
maintainer-name: "Nom de ladministrateur"
maintainer-email: "Contact administratif"
drive-config: "Paramètres du lecteur"
object-storage-endpoint: "Point de terminaison"
cache-remote-files: "Mettre en cache des fichiers distants"
local-drive-capacity-mb: "Volume du lecteur par utilisateur"
remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant"
@ -1474,8 +1478,11 @@ mobile/views/components/ui.nav.vue:
mobile/views/pages/drive.vue:
contextmenu:
upload: "Téléverser un fichier"
url-upload: "Transférer un fichier depuis une URL"
create-folder: "Créer un dossier"
rename-folder: "Renommer le dossier"
move-folder: "Déplacer ce dossier"
delete-folder: "Supprimer ce dossier"
mobile/views/pages/user-lists.vue:
title: "Listes"
mobile/views/pages/signup.vue:
@ -1584,6 +1591,7 @@ dev/views/apps.vue:
create-app: "Créer une app"
app-missing: "Aucune application"
dev/views/new-app.vue:
new-app: "Nouvelle application"
create-app: "Création dune application"
app-name: "Nom de lapplication"
app-name-desc: "Le nom de votre application"
@ -1616,7 +1624,9 @@ pages:
enter-variable-name: "Veuillez choisir un nom de variable"
the-variable-name-is-already-used: "Cette variable est déjà utilisée"
content-blocks: "Contenu du cadre"
input-blocks: "Entrée"
special-blocks: "Spécial"
post-from-post-form: "Publier ce contenu"
posted-from-post-form: "Publié !"
blocks:
text: "Texte"
@ -1653,11 +1663,15 @@ pages:
_counter:
name: "Nom de la variable"
text: "Titre"
inc: "Augmenter le chiffre"
_button:
text: "Titre"
action: "L'opération lorsque le bouton sera pressé"
_action:
dialog: "Afficher une fenêtre de dialogue"
_dialog:
content: "Contenu"
resetRandom: "Réinitialiser le nombre aléatoire"
script:
categories:
flow: "Contrôle"
@ -1736,13 +1750,20 @@ pages:
arg1: "Listes"
_dailyRandomPick:
arg1: "Listes"
_seedRannum:
arg2: "Min"
arg3: "Max"
_seedRandomPick:
arg2: "Listes"
pick: "Sélectionner dans la liste"
_pick:
arg1: "Listes"
arg2: "Position"
number: "Numérique"
stringToNumber: "Chaîne en chiffres"
_stringToNumber:
arg1: "Texte"
numberToString: "Chiffres en chaîne"
_numberToString:
arg1: "Numérique"
_splitStrByLine:
@ -1750,6 +1771,7 @@ pages:
ref: "Variables"
fn: "Fonction"
_fn:
slots: "Emplacement"
arg1: "Sortie"
for: "Répéter"
types:

View File

@ -265,6 +265,7 @@ common:
my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
hide-password: "パスワードを隠す"
show-password: "パスワードを表示する"
enter-username: "ユーザー名を入力してください"
do-not-use-in-production: "これは開発ビルドです。本番環境で使用しないでください。"
user-suspended: "このユーザーは凍結されています。"
@ -480,20 +481,24 @@ common/views/components/messaging.vue:
search-user: "ユーザーを探す"
you: "あなた"
no-history: "履歴はありません"
user: "ユーザー"
group: "グループ"
start-with-user: "ユーザーとトークを開始"
start-with-group: "グループとトークを開始"
common/views/components/messaging-room.vue:
empty: "このユーザーと話したことはありません"
not-talked-user: "このユーザーとの会話はありません"
not-talked-group: "このグループでの会話はありません"
no-history: "これより過去の履歴はありません"
resize-form: "ドラッグしてフォームの広さを調整"
new-message: "新しいメッセージがあります"
only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです"
only-one-file-attached: "メッセージに添付できるファイルはひとつです"
common/views/components/messaging-room.form.vue:
input-message-here: "ここにメッセージを入力"
send: "送信"
attach-from-local: "PCからファイルを添付する"
attach-from-drive: "ドライブからファイルを添付する"
only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです"
only-one-file-attached: "メッセージに添付できるファイルはひとつです"
common/views/components/messaging-room.message.vue:
is-read: "既読"
@ -750,11 +755,27 @@ common/views/components/user-list-editor.vue:
remove-user: "このリストから削除"
delete-are-you-sure: "リスト「$1」を削除しますか"
deleted: "削除しました"
add-user: "ユーザーを追加"
common/views/components/user-group-editor.vue:
users: "メンバー"
rename: "グループ名を変更"
delete: "グループを削除"
remove-user: "このグループから削除"
delete-are-you-sure: "グループ「$1」を削除しますか"
deleted: "削除しました"
add-user: "メンバーを追加"
common/views/components/user-lists.vue:
user-lists: "リスト"
create-list: "リストを作成"
list-name: "リスト名"
common/views/components/user-groups.vue:
user-groups: "グループ"
create-group: "グループを作成"
group-name: "グループ名"
common/views/widgets/broadcast.vue:
fetching: "確認中"
no-broadcasts: "お知らせはありません"
@ -827,6 +848,11 @@ common/views/pages/follow.vue:
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
common/views/pages/follow-requests.vue:
received-follow-requests: "フォロー申請"
accept: "承認"
reject: "拒否"
desktop:
banner-crop-title: "バナーとして表示する部分を選択"
banner: "バナー"
@ -1139,6 +1165,7 @@ desktop/views/components/ui.header.vue:
desktop/views/components/ui.header.account.vue:
profile: "プロフィール"
lists: "リスト"
groups: "グループ"
follow-requests: "フォロー申請"
admin: "管理"
@ -1154,14 +1181,6 @@ desktop/views/components/ui.header.post.vue:
desktop/views/components/ui.header.search.vue:
placeholder: "検索"
desktop/views/components/received-follow-requests-window.vue:
title: "フォロー申請"
accept: "承認"
reject: "拒否"
desktop/views/components/user-lists-window.vue:
title: "リスト"
desktop/views/components/user-preview.vue:
notes: "投稿"
following: "フォロー"
@ -1187,7 +1206,6 @@ admin/views/index.vue:
users: "ユーザー"
federation: "連合"
announcements: "お知らせ"
hashtags: "ハッシュタグ"
abuse: "スパム報告"
queue: "ジョブキュー"
logs: "ログ"
@ -1230,7 +1248,22 @@ admin/views/instance.vue:
maintainer-config: "管理者情報"
maintainer-name: "管理者名"
maintainer-email: "管理者の連絡先"
advanced-config: "その他の設定"
note-and-tl: "投稿とタイムライン"
drive-config: "ドライブの設定"
use-object-storage: "オブジェクトストレージを使用する"
object-storage-base-url: "URL"
object-storage-bucket: "バケット名"
object-storage-prefix: "プレフィックス"
object-storage-endpoint: "エンドポイント"
object-storage-region: "リージョン"
object-storage-port: "ポート"
object-storage-access-key: "アクセスキー"
object-storage-secret-key: "シークレットキー"
object-storage-use-ssl: "SSLを使用"
object-storage-s3-info: "Amazon S3をオブジェクトストレージとして使用する場合の「エンドポイント」と「リージョン」の設定については{0}をご確認ください。"
object-storage-s3-info-here: "こちら"
object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージとして使用する場合、「エンドポイント」は storage.googleapis.com に設定し、「リージョン」は空欄にします。"
cache-remote-files: "リモートのファイルをキャッシュする"
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。"
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
@ -1239,8 +1272,12 @@ admin/views/instance.vue:
recaptcha-config: "reCAPTCHAの設定"
recaptcha-info: "reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。"
enable-recaptcha: "reCAPTCHAを有効にする"
recaptcha-site-key: "reCAPTCHA site key"
recaptcha-secret-key: "reCAPTCHA secret key"
recaptcha-site-key: "サイトキー"
recaptcha-secret-key: "シークレットキー"
recaptcha-preview: "プレビュー"
hidden-tags: "非表示ハッシュタグ"
hidden-tags-info: "集計から除外するハッシュタグを改行で区切って記述します。"
external-service-integration-config: "外部サービス連携"
twitter-integration-config: "Twitter連携の設定"
twitter-integration-info: "コールバックURLは {url} に設定します。"
enable-twitter-integration: "Twitter連携を有効にする"
@ -1361,6 +1398,8 @@ admin/views/users.vue:
unsilence-confirm: "サイレンスを解除しますか?"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
delete-all-files: "すべてのファイルを削除"
delete-all-files-confirm: "すべてのファイルを削除しますか?"
users:
title: "ユーザー"
sort:
@ -1680,6 +1719,7 @@ mobile/views/components/ui.nav.vue:
follow-requests: "フォロー申請"
search: "検索"
user-lists: "リスト"
user-groups: "グループ"
widgets: "ウィジェット"
game: "ゲーム"
admin: "管理"
@ -1694,9 +1734,6 @@ mobile/views/pages/drive.vue:
move-folder: "このフォルダを移動"
delete-folder: "このフォルダを削除"
mobile/views/pages/user-lists.vue:
title: "リスト"
mobile/views/pages/signup.vue:
lets-start: "📦 始めましょう"
@ -1729,11 +1766,6 @@ mobile/views/pages/widgets/activity.vue:
mobile/views/pages/share.vue:
share-with: "{name}で共有"
mobile/views/pages/received-follow-requests.vue:
title: "フォロー申請"
accept: "承認"
reject: "拒否"
mobile/views/pages/note.vue:
title: "投稿"
prev: "前の投稿"
@ -1854,6 +1886,10 @@ pages:
edit-this-page: "このページを編集"
view-source: "ソースを表示"
view-page: "ページを見る"
like: "いいね"
unlike: "いいね解除"
liked-pages: "いいねしたページ"
my-pages: "自分のページ"
inspector: "インスペクター"
content: "ページブロック"
variables: "変数"

View File

@ -836,7 +836,6 @@ admin/views/index.vue:
users: "ユーザー"
federation: "連合"
announcements: "知っといてや"
hashtags: "ハッシュタグ"
back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue:
dashboard: "ダッシュボード"
@ -861,6 +860,7 @@ admin/views/instance.vue:
maintainer-name: "管理者名"
maintainer-email: "管理者の連絡先"
drive-config: "ドライブの設定"
object-storage-endpoint: "エンドポイント"
cache-remote-files: "リモートのファイルをキャッシュする"
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをこっちで保管せずに直接リンク張るようになるで。サーバーのストレージは軽くやろうけど、プライバシー設定で直リンクを向こうにしとるユーザーはファイルが見れへんし、サムネイルが無いから通信量が増えたりするから、普通はオンにしといてな。"
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"

View File

@ -1065,7 +1065,6 @@ admin/views/index.vue:
users: "사용자"
federation: "연합"
announcements: "공지사항"
hashtags: "해시태그"
abuse: "스팸 신고"
queue: "작업 대기열"
logs: "로그"
@ -1105,6 +1104,7 @@ admin/views/instance.vue:
maintainer-name: "관리자 이름"
maintainer-email: "관리자 연락처"
drive-config: "드라이브 설정"
object-storage-endpoint: "엔드포인트"
cache-remote-files: "원격 파일을 캐시"
cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 일반적으로 이 설정을 ON으로 두는 것을 추천합니다."
local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량"

View File

@ -855,7 +855,6 @@ admin/views/index.vue:
moderators: "Moderatorzy"
users: "Użytkownicy"
announcements: "Ogłoszenia"
hashtags: "Hashtagi"
admin/views/dashboard.vue:
dashboard: "Kokpit"
accounts: "Konta"

View File

@ -145,7 +145,7 @@ common:
profile: "个人资料"
notification: "通知"
apps: "应用程序"
tags: "标签"
tags: "哈希标签"
mute-and-block: "屏蔽/拉黑"
blocking: "拉黑"
security: "安全性"
@ -168,7 +168,7 @@ common:
use-avatar-reversi-stones: "用头像作为黑白棋的棋子"
disable-animated-mfm: "在帖子中禁用动画文本"
disable-showing-animated-images: "不播放动画"
suggest-recent-hashtags: "在帖子表单上显示最近流行的主题标签"
suggest-recent-hashtags: "在帖子表单上显示最近流行的哈希标签"
always-show-nsfw: "总是显示 NSFW 的内容"
always-mark-nsfw: "总是用 NSFW 来标记附件"
show-full-acct: "不要从用户名中忽略主机名"
@ -297,7 +297,7 @@ common:
server: "服务器信息"
nav: "导航"
tips: "提示"
hashtags: "标签"
hashtags: "哈希标签"
queue: "队列"
dev: "构建应用程序失败,请再试一次。"
ai-chan-kawaii: "小蓝真可爱"
@ -469,9 +469,10 @@ common/views/components/nav.vue:
status: "状态"
wiki: "维基百科"
donors: "捐赠者"
repository: "码库"
repository: "码库"
develop: "开发人员"
feedback: "反馈"
tos: "服务条款"
common/views/components/note-menu.vue:
mention: "提到"
detail: "详细信息"
@ -584,6 +585,8 @@ common/views/components/signup.vue:
password-matched: "确认"
password-not-matched: "密码不一致"
recaptcha: "验证"
agree-to: "同意{0}"
tos: "服务条款"
create: "创建一个账户"
some-error: "由于某种原因,创建帐户失败。请再试一次。"
common/views/components/special-message.vue:
@ -713,7 +716,7 @@ common/views/widgets/posts-monitor.vue:
title: "投稿表格"
toggle: "切换视图"
common/views/widgets/hashtags.vue:
title: "标签"
title: "哈希标签"
common/views/widgets/server.vue:
title: "服务器信息"
toggle: "切换显示"
@ -1016,8 +1019,8 @@ desktop/views/components/timeline.vue:
mentions: "提到的"
messages: "直接发布"
list: "列表"
hashtag: "标签"
add-tag-timeline: "添加标签"
hashtag: "哈希标签"
add-tag-timeline: "添加哈希标签"
add-list: "添加列表"
list-name: "列表名称"
desktop/views/components/ui.header.vue:
@ -1063,7 +1066,6 @@ admin/views/index.vue:
users: "用户"
federation: "联合"
announcements: "公告"
hashtags: "标签"
abuse: "举报垃圾信息"
queue: "作业队列"
logs: "登录"
@ -1090,14 +1092,34 @@ admin/views/instance.vue:
instance-name: "实例名称"
instance-description: "实例介绍"
host: "主机名"
icon-url: "图标URL"
logo-url: "Logo URL"
banner-url: "背景图片地址"
error-image-url: "无效的图像URL"
languages: "实例语言"
languages-desc: "您可以添加多个,以空格分隔。"
tos-url: "服务条款URL"
repository-url: "源码库URL"
feedback-url: "反馈URL"
maintainer-config: "管理员信息"
maintainer-name: "管理员名称"
maintainer-email: "联系管理员"
advanced-config: "其他设置"
note-and-tl: "帖子和时间线"
drive-config: "网盘设置"
use-object-storage: "使用对象存储"
object-storage-base-url: "URL"
object-storage-bucket: "存储空间名"
object-storage-prefix: "前缀"
object-storage-endpoint: "端点"
object-storage-region: "区域"
object-storage-port: "端口"
object-storage-access-key: "访问密钥"
object-storage-secret-key: "密钥"
object-storage-use-ssl: "使用 SSL"
object-storage-s3-info: "使用Amazon S3作为对象存储时请确认{0}相关“终端”和“区域”的设置。"
object-storage-s3-info-here: "这里"
object-storage-gcs-info: "将Google Cloud Storage用作对象存储时请将“终端”设置为storage.googleapis.com并将“区域”留空。"
cache-remote-files: "远程文件缓存"
cache-remote-files-desc: "如果没有此参数,则所有远程文件都将直接链接到其主机服务器。 这将是保存服务器存储的有效解决方案,但是对于设置禁用直接链接的用户而言,远程文件不可见,因为不会生成缩略图,从而增加流量。 建议启用此参数集。"
local-drive-capacity-mb: "每个用户的网盘空间"
@ -1108,6 +1130,9 @@ admin/views/instance.vue:
enable-recaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptcha-site-key: "reCAPTCHA site key"
recaptcha-secret-key: "reCAPTCHA secret key"
hidden-tags: "隐藏哈希标签"
hidden-tags-info: "使用换行符分隔要从集合中排除的哈希标签。"
external-service-integration-config: "连接外部服务"
twitter-integration-config: "连接到Twitter的设置"
twitter-integration-info: "设置返回的URL{url}。"
enable-twitter-integration: "启用连接到Twitter"
@ -1139,6 +1164,7 @@ admin/views/instance.vue:
save: "保存"
saved: "保存完毕"
pinned-users: "置顶用户"
pinned-users-info: "描述您要置顶的用户,以换行符分隔。"
email-config: "电子邮件服务器设置"
email-config-info: "用于确认电子邮件和密码重置等。"
enable-email: "启用电子邮件送递"
@ -1224,6 +1250,8 @@ admin/views/users.vue:
unsilence-confirm: "解除屏蔽?"
update-remote-user: "更新远程用户信息"
remote-user-updated: "远程用户信息已更新"
delete-all-files: "删除所有文件"
delete-all-files-confirm: "删除所有文件吗?"
users:
title: "用户"
sort:
@ -1299,6 +1327,7 @@ admin/views/federation.vue:
latest-request-received-at: "上次收到的请求"
remove-all-following: "取消所有关注"
remove-all-following-info: "取消{host}的所有关注者。当实例不存在时执行。"
delete-all-files: "删除所有文件"
block: "拉黑"
marked-as-closed: "标记为已关闭"
lookup: "查询"
@ -1346,6 +1375,7 @@ admin/views/federation.vue:
hour: "每小时"
day: "每天"
blocked-hosts: "拉黑"
blocked-hosts-info: "描述您要阻止的主机,以换行符分隔。"
desktop/views/pages/welcome.vue:
about: "更多信息..."
timeline: "时间线"
@ -1367,7 +1397,7 @@ desktop/views/pages/search.vue:
not-available: "在此实例的设置中关闭搜索功能。"
not-found: "没有找到“{q}”的帖子"
desktop/views/pages/tag.vue:
no-posts-found: "没有找到带有主题标签“{q}”的帖子"
no-posts-found: "没有找到带有哈希标签“{q}”的帖子"
desktop/views/pages/user-list.users.vue:
users: "用户"
add-user: "添加用户"
@ -1443,7 +1473,7 @@ mobile/views/components/drive.file-detail.vue:
download: "下载"
rename: "重命名"
move: "移动"
hash: "Hash (md5)"
hash: "哈希(md5)"
exif: "EXIF"
nsfw: "阅读注意"
mark-as-sensitive: "标记为“敏感”"
@ -1530,7 +1560,7 @@ mobile/views/pages/home.vue:
mentions: "Mentions"
messages: "直接发布"
mobile/views/pages/tag.vue:
no-posts-found: "没有找到带有主题标签“{q}”的帖子"
no-posts-found: "没有找到带有哈希标签“{q}”的帖子"
mobile/views/pages/widgets.vue:
dashboard: "仪表盘"
widgets-hints: "您可以添加/删除/重新排列小部件。 要移动小部件,请拖动“三”。 点击“×”删除小部件。 某些小部件可以通过点击来更改显示。"
@ -1582,7 +1612,7 @@ deck:
home: "首页"
local: "Local"
hybrid: "社交"
hashtag: "标签"
hashtag: "哈希标签"
global: "Global"
mentions: "Mentions"
direct: "直接发布"

View File

@ -0,0 +1,31 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class ObjectStorageSetting1557932705754 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "meta" ADD "useObjectStorage" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBucket" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePrefix" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBaseUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageEndpoint" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageRegion" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageAccessKey" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSecretKey" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePort" integer`);
await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseSSL" boolean NOT NULL DEFAULT true`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseSSL"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePort"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSecretKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageAccessKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRegion"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageEndpoint"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBaseUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePrefix"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBucket"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useObjectStorage"`);
}
}

View File

@ -0,0 +1,23 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class PageLike1558072954435 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TABLE "page_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "pageId" character varying(32) NOT NULL, CONSTRAINT "PK_813f034843af992d3ae0f43c64c" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_0e61efab7f88dbb79c9166dbb4" ON "page_like" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa" ON "page_like" ("userId", "pageId") `);
await queryRunner.query(`ALTER TABLE "page" ADD "likedCount" integer NOT NULL DEFAULT 0`);
await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_cf8782626dced3176038176a847" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_cf8782626dced3176038176a847"`);
await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48"`);
await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "likedCount"`);
await queryRunner.query(`DROP INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa"`);
await queryRunner.query(`DROP INDEX "IDX_0e61efab7f88dbb79c9166dbb4"`);
await queryRunner.query(`DROP TABLE "page_like"`);
}
}

View File

@ -0,0 +1,41 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class UserGroup1558103093633 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TABLE "user_group" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "isPrivate" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c29fba6fe013ec8724378ce7c9" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_20e30aa35180e317e133d75316" ON "user_group" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_3d6b372788ab01be58853003c9" ON "user_group" ("userId") `);
await queryRunner.query(`CREATE TABLE "user_group_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_15f2425885253c5507e1599cfe7" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23" ON "user_group_joining" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_67dc758bc0566985d1b3d39986" ON "user_group_joining" ("userGroupId") `);
await queryRunner.query(`ALTER TABLE "messaging_message" ADD "groupId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "messaging_message" ADD "reads" character varying(32) array NOT NULL DEFAULT '{}'::varchar[]`);
await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" DROP NOT NULL`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS 'The recipient user ID.'`);
await queryRunner.query(`CREATE INDEX "IDX_2c4be03b446884f9e9c502135b" ON "messaging_message" ("groupId") `);
await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_2c4be03b446884f9e9c502135be" FOREIGN KEY ("groupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_group" ADD CONSTRAINT "FK_3d6b372788ab01be58853003c93" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_67dc758bc0566985d1b3d399865" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_67dc758bc0566985d1b3d399865"`);
await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231"`);
await queryRunner.query(`ALTER TABLE "user_group" DROP CONSTRAINT "FK_3d6b372788ab01be58853003c93"`);
await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_2c4be03b446884f9e9c502135be"`);
await queryRunner.query(`DROP INDEX "IDX_2c4be03b446884f9e9c502135b"`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS ''`);
await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "reads"`);
await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "groupId"`);
await queryRunner.query(`DROP INDEX "IDX_67dc758bc0566985d1b3d39986"`);
await queryRunner.query(`DROP INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23"`);
await queryRunner.query(`DROP TABLE "user_group_joining"`);
await queryRunner.query(`DROP INDEX "IDX_3d6b372788ab01be58853003c9"`);
await queryRunner.query(`DROP INDEX "IDX_20e30aa35180e317e133d75316"`);
await queryRunner.query(`DROP TABLE "user_group"`);
}
}

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "11.13.0",
"version": "11.16.0",
"codename": "daybreak",
"repository": {
"type": "git",
@ -12,6 +12,9 @@
"scripts": {
"start": "node ./index.js",
"init": "node ./built/init.js",
"ormconfig": "node ./built/ormconfig.js",
"migrate": "npm run ormconfig && ts-node ./node_modules/typeorm/cli.js migration:run",
"migrateandstart": "npm run migrate && npm run start",
"build": "webpack && gulp build",
"webpack": "webpack",
"watch": "webpack --watch",

View File

@ -1,7 +1,7 @@
<template>
<div>
<ui-card>
<template #title><fa icon="broadcast-tower"/> {{ $t('announcements') }}</template>
<template #title><fa :icon="faBroadcastTower"/> {{ $t('announcements') }}</template>
<section v-for="(announcement, i) in announcements" class="fit-top">
<ui-input v-model="announcement.title" @change="save">
<span>{{ $t('title') }}</span>
@ -18,7 +18,7 @@
</ui-horizon-group>
</section>
<section>
<ui-button @click="add"><fa icon="plus"/> {{ $t('add') }}</ui-button>
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add') }}</ui-button>
</section>
</ui-card>
</div>
@ -27,12 +27,14 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/announcements.vue'),
data() {
return {
announcements: [],
faBroadcastTower, faPlus
};
},

View File

@ -1,41 +0,0 @@
<template>
<div>
<ui-card>
<template #title>{{ $t('hided-tags') }}</template>
<section>
<textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea>
<ui-button @click="save">{{ $t('save') }}</ui-button>
</section>
</ui-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
export default Vue.extend({
i18n: i18n('admin/views/hashtags.vue'),
data() {
return {
hiddenTags: '',
};
},
created() {
this.$root.getMeta().then(meta => {
this.hiddenTags = meta.hiddenTags.join('\n');
});
},
methods: {
save() {
this.$root.api('admin/update-meta', {
hiddenTags: this.hiddenTags.split('\n')
}).then(() => {
//this.$root.os.apis.dialog({ text: `Saved` });
}).catch(e => {
//this.$root.os.apis.dialog({ text: `Failed ${e}` });
});
}
}
});
</script>

View File

@ -28,7 +28,6 @@
<li @click="nav('federation')" :class="{ active: page == 'federation' }"><fa :icon="faGlobe" fixed-width/>{{ $t('federation') }}</li>
<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li>
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li>
<li @click="nav('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li>
</ul>
<div class="back-to-misskey">
@ -48,7 +47,6 @@
<div v-if="page == 'users'"><x-users/></div>
<div v-if="page == 'emoji'"><x-emoji/></div>
<div v-if="page == 'announcements'"><x-announcements/></div>
<div v-if="page == 'hashtags'"><x-hashtags/></div>
<div v-if="page == 'drive'"><x-drive/></div>
<div v-if="page == 'federation'"><x-federation/></div>
<div v-if="page == 'abuse'"><x-abuse/></div>
@ -68,7 +66,6 @@ import XLogs from "./logs.vue";
import XModerators from "./moderators.vue";
import XEmoji from "./emoji.vue";
import XAnnouncements from "./announcements.vue";
import XHashtags from "./hashtags.vue";
import XUsers from "./users.vue";
import XDrive from "./drive.vue";
import XAbuse from "./abuse.vue";
@ -91,7 +88,6 @@ export default Vue.extend({
XModerators,
XEmoji,
XAnnouncements,
XHashtags,
XUsers,
XDrive,
XAbuse,

View File

@ -2,7 +2,7 @@
<div>
<ui-card>
<template #title><fa icon="cog"/> {{ $t('instance') }}</template>
<section class="fit-top fit-bottom">
<section class="fit-top">
<ui-input :value="host" readonly>{{ $t('host') }}</ui-input>
<ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
<ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea>
@ -11,77 +11,83 @@
<ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input>
<ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input>
<ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input>
<ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input>
<ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input>
<ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input>
<details>
<summary>{{ $t('advanced-config') }}</summary>
<ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input>
<ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input>
</details>
</section>
<section class="fit-bottom">
<header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header>
<ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input>
<ui-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="farEnvelope"/></template>{{ $t('maintainer-email') }}</ui-input>
</section>
<section>
<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch>
<ui-button v-if="disableRegistration" @click="invite">{{ $t('invite') }}</ui-button>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faPencilAlt"/> {{ $t('note-and-tl') }}</template>
<section class="fit-top fit-bottom">
<ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input>
</section>
<section>
<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch>
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
<ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch>
<ui-info>{{ $t('disabling-timelines-info') }}</ui-info>
</section>
<section>
<ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch>
<ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch>
</section>
<section class="fit-bottom">
<header><fa icon="cloud"/> {{ $t('drive-config') }}</header>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa icon="cloud"/> {{ $t('drive-config') }}</template>
<section>
<ui-switch v-model="useObjectStorage">{{ $t('use-object-storage') }}</ui-switch>
<template v-if="useObjectStorage">
<ui-info>
<i18n path="object-storage-s3-info">
<a href="https://docs.aws.amazon.com/general/latest/gr/rande.html" target="_blank">{{ $t('object-storage-s3-info-here') }}</a>
</i18n>
</ui-info>
<ui-info>{{ $t('object-storage-gcs-info') }}</ui-info>
<ui-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('object-storage-base-url') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('object-storage-bucket') }}</ui-input>
<ui-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('object-storage-prefix') }}</ui-input>
</ui-horizon-group>
<ui-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('object-storage-endpoint') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('object-storage-region') }}</ui-input>
<ui-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">{{ $t('object-storage-port') }}</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-access-key') }}</ui-input>
<ui-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-secret-key') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('object-storage-use-ssl') }}</ui-switch>
</template>
</section>
<section>
<ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch>
</section>
<section class="fit-top fit-bottom">
<ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input>
<ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input>
</section>
<section class="fit-bottom">
<header><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</header>
<ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch>
<ui-info>{{ $t('recaptcha-info') }}</ui-info>
<ui-horizon-group inputs>
<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input>
<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input>
</ui-horizon-group>
</section>
<section>
<header><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</header>
<ui-info>{{ $t('proxy-account-info') }}</ui-info>
<ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input>
<ui-info warn>{{ $t('proxy-account-warn') }}</ui-info>
</section>
<section>
<header><fa :icon="farEnvelope"/> {{ $t('email-config') }}</header>
<ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch>
<ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input>
<ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch>
<ui-horizon-group inputs>
<ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input>
<ui-input v-model="smtpPass" type="password" :withPasswordToggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch>
</section>
<section>
<header><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</header>
<ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch>
<ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info>
<ui-horizon-group inputs class="fit-bottom">
<ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input>
<ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input>
</ui-horizon-group>
</section>
<section>
<header>summaly Proxy</header>
<ui-input v-model="summalyProxy">URL</ui-input>
</section>
<section>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
@ -91,56 +97,146 @@
<ui-textarea v-model="pinnedUsers">
<template #desc>{{ $t('pinned-users-info') }}</template>
</ui-textarea>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title>{{ $t('invite') }}</template>
<template #title><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</template>
<section>
<ui-button @click="invite">{{ $t('invite') }}</ui-button>
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
<ui-info>{{ $t('proxy-account-info') }}</ui-info>
<ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input>
<ui-info warn>{{ $t('proxy-account-warn') }}</ui-info>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</template>
<template #title><fa :icon="farEnvelope"/> {{ $t('email-config') }}</template>
<section>
<ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch>
<template v-if="enableEmail">
<ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input>
<ui-horizon-group inputs>
<ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input>
<ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch>
<ui-horizon-group inputs>
<ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input>
<ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch>
</template>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</template>
<section>
<ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch>
<template v-if="enableServiceWorker">
<ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info>
<ui-horizon-group inputs class="fit-bottom">
<ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input>
<ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input>
</ui-horizon-group>
</template>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</template>
<section :class="enableRecaptcha ? 'fit-bottom' : ''">
<ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch>
<template v-if="enableRecaptcha">
<ui-info>{{ $t('recaptcha-info') }}</ui-info>
<ui-horizon-group inputs>
<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input>
<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input>
</ui-horizon-group>
</template>
</section>
<section v-if="enableRecaptcha && recaptchaSiteKey">
<header>{{ $t('recaptcha-preview') }}</header>
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faShieldAlt"/> {{ $t('external-service-integration-config') }}</template>
<section>
<header><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</header>
<ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch>
<ui-horizon-group>
<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input>
<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
</ui-horizon-group>
<ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
<template v-if="enableTwitterIntegration">
<ui-horizon-group>
<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input>
<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
</ui-horizon-group>
<ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info>
</template>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</template>
<section>
<header><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</header>
<ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch>
<ui-horizon-group>
<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input>
<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input>
</ui-horizon-group>
<ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
<template v-if="enableGithubIntegration">
<ui-horizon-group>
<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input>
<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input>
</ui-horizon-group>
<ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info>
</template>
</section>
<section>
<header><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</header>
<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch>
<template v-if="enableDiscordIntegration">
<ui-horizon-group>
<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input>
<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input>
</ui-horizon-group>
<ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info>
</template>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</template>
<section>
<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch>
<ui-horizon-group>
<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input>
<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input>
</ui-horizon-group>
<ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
</section>
</ui-card>
<details>
<summary style="color:var(--text);">{{ $t('advanced-config') }}</summary>
<ui-card>
<template #title><fa :icon="faHashtag"/> {{ $t('hidden-tags') }}</template>
<section class="fit-top">
<ui-textarea v-model="hiddenTags">
<template #desc>{{ $t('hidden-tags-info') }}</template>
</ui-textarea>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title>summaly Proxy</template>
<section class="fit-top fit-bottom">
<ui-input v-model="summalyProxy">URL</ui-input>
</section>
<section>
<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</section>
</ui-card>
</details>
</div>
</template>
@ -149,8 +245,8 @@ import Vue from 'vue';
import i18n from '../../i18n';
import { url, host } from '../../config';
import { toUnicode } from 'punycode';
import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack } from '@fortawesome/free-solid-svg-icons';
import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack, faPencilAlt, faHashtag } from '@fortawesome/free-solid-svg-icons';
import { faEnvelope as farEnvelope, faSave } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/instance.vue'),
@ -193,7 +289,6 @@ export default Vue.extend({
discordClientId: null,
discordClientSecret: null,
proxyAccount: null,
inviteCode: null,
summalyProxy: null,
enableEmail: false,
email: null,
@ -207,7 +302,18 @@ export default Vue.extend({
swPublicKey: null,
swPrivateKey: null,
pinnedUsers: '',
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack
hiddenTags: '',
useObjectStorage: false,
objectStorageBaseUrl: null,
objectStorageBucket: null,
objectStoragePrefix: null,
objectStorageEndpoint: null,
objectStorageRegion: null,
objectStoragePort: null,
objectStorageAccessKey: null,
objectStorageSecretKey: null,
objectStorageUseSSL: false,
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag
};
},
@ -260,13 +366,55 @@ export default Vue.extend({
this.swPublicKey = meta.swPublickey;
this.swPrivateKey = meta.swPrivateKey;
this.pinnedUsers = meta.pinnedUsers.join('\n');
this.hiddenTags = meta.hiddenTags.join('\n');
this.useObjectStorage = meta.useObjectStorage;
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
this.objectStorageBucket = meta.objectStorageBucket;
this.objectStoragePrefix = meta.objectStoragePrefix;
this.objectStorageEndpoint = meta.objectStorageEndpoint;
this.objectStorageRegion = meta.objectStorageRegion;
this.objectStoragePort = meta.objectStoragePort;
this.objectStorageAccessKey = meta.objectStorageAccessKey;
this.objectStorageSecretKey = meta.objectStorageSecretKey;
this.objectStorageUseSSL = meta.objectStorageUseSSL;
});
},
mounted() {
const renderRecaptchaPreview = () => {
if (!(window as any).grecaptcha) return;
if (!this.$refs.recaptcha) return;
if (!this.recaptchaSiteKey) return;
(window as any).grecaptcha.render(this.$refs.recaptcha, {
sitekey: this.recaptchaSiteKey
});
};
window.onRecaotchaLoad = () => {
renderRecaptchaPreview();
};
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
head.appendChild(script);
this.$watch('enableRecaptcha', () => {
renderRecaptchaPreview();
});
this.$watch('recaptchaSiteKey', () => {
renderRecaptchaPreview();
});
},
methods: {
invite() {
this.$root.api('admin/invite').then(x => {
this.inviteCode = x.code;
this.$root.dialog({
type: 'info',
text: x.code
});
}).catch(e => {
this.$root.dialog({
type: 'error',
@ -322,7 +470,18 @@ export default Vue.extend({
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
pinnedUsers: this.pinnedUsers.split('\n')
pinnedUsers: this.pinnedUsers.split('\n'),
hiddenTags: this.hiddenTags.split('\n'),
useObjectStorage: this.useObjectStorage,
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
objectStorageUseSSL: this.objectStorageUseSSL,
}).then(() => {
this.$root.dialog({
type: 'success',

View File

@ -9,8 +9,9 @@
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<div class="user" v-if="user">
<x-user :user='user'/>
<x-user :user="user"/>
<div class="actions">
<ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button>
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
<ui-horizon-group>
<ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button>
@ -20,7 +21,7 @@
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
</ui-horizon-group>
<ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button>
<ui-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button>
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
</div>
</div>
@ -67,7 +68,7 @@ import Vue from 'vue';
import i18n from '../../i18n';
import parseAcct from "../../../../misc/acct/parse";
import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
import { faSnowflake, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import XUser from './users.user.vue';
export default Vue.extend({
@ -88,7 +89,7 @@ export default Vue.extend({
offset: 0,
users: [],
existMore: false,
faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash
faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash, faTrashAlt
};
},
@ -277,6 +278,25 @@ export default Vue.extend({
this.refreshUser();
},
async deleteAllFiles() {
if (!await this.getConfirmed(this.$t('delete-all-files-confirm'))) return;
const process = async () => {
await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
splash: true
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
},
async getConfirmed(text: string): Promise<Boolean> {
const confirm = await this.$root.dialog({
type: 'warning',

View File

@ -18,6 +18,7 @@
<fa icon="spinner" pulse v-if="type === 'waiting'"/>
</div>
<header v-if="title" v-html="title"></header>
<header v-if="title == null && user">{{ $t('@.enter-username') }}</header>
<div class="body" v-if="text" v-html="text"></div>
<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input>

View File

@ -44,6 +44,8 @@ import uiSwitch from './ui/switch.vue';
import uiRadio from './ui/radio.vue';
import uiSelect from './ui/select.vue';
import uiInfo from './ui/info.vue';
import uiMargin from './ui/margin.vue';
import uiHr from './ui/hr.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
@ -91,5 +93,7 @@ Vue.component('ui-switch', uiSwitch);
Vue.component('ui-radio', uiRadio);
Vue.component('ui-select', uiSelect);
Vue.component('ui-info', uiInfo);
Vue.component('ui-margin', uiMargin);
Vue.component('ui-hr', uiHr);
Vue.component('form-button', formButton);
Vue.component('form-radio', formRadio);

View File

@ -33,7 +33,16 @@ import * as autosize from 'autosize';
export default Vue.extend({
i18n: i18n('common/views/components/messaging-room.form.vue'),
props: ['user'],
props: {
user: {
type: Object,
requird: false,
},
group: {
type: Object,
requird: false,
},
},
data() {
return {
text: null,
@ -43,7 +52,7 @@ export default Vue.extend({
},
computed: {
draftId(): string {
return this.user.id;
return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
},
canSend(): boolean {
return (this.text != null && this.text != '') || this.file != null;
@ -159,7 +168,8 @@ export default Vue.extend({
send() {
this.sending = true;
this.$root.api('messaging/messages/create', {
userId: this.user.id,
userId: this.user ? this.user.id : undefined,
groupId: this.group ? this.group.id : undefined,
text: this.text ? this.text : undefined,
fileId: this.file ? this.file.id : undefined
}).then(message => {

View File

@ -23,7 +23,12 @@
<div></div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<footer>
<span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span>
<template v-if="isGroup">
<span class="read" v-if="message.reads.length > 0">{{ $t('is-read') }} {{ message.reads.length }}</span>
</template>
<template v-else>
<span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span>
</template>
<mk-time :time="message.createdAt"/>
<template v-if="message.is_edited"><fa icon="pencil-alt"/></template>
</footer>
@ -42,6 +47,9 @@ export default Vue.extend({
props: {
message: {
required: true
},
isGroup: {
required: false
}
},
computed: {

View File

@ -4,14 +4,14 @@
@drop.prevent.stop="onDrop"
>
<div class="body">
<p class="init" v-if="init"><fa icon="spinner .spin"/>{{ $t('@.loading') }}</p>
<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ $t('empty') }}</p>
<p class="init" v-if="init"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}</p>
<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ user ? $t('not-talked-user') : $t('not-talked-group') }}</p>
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p>
<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }}
</button>
<template v-for="(message, i) in _messages">
<x-message :message="message" :key="message.id"/>
<x-message :message="message" :key="message.id" :is-group="group != null"/>
<p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date">
<span>{{ _messages[i + 1]._datetext }}</span>
</p>
@ -23,7 +23,7 @@
<button @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button>
</div>
</transition>
<x-form :user="user" ref="form"/>
<x-form :user="user" :group="group" ref="form"/>
</footer>
</div>
</template>
@ -34,17 +34,30 @@ import i18n from '../../../i18n';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import { url } from '../../../config';
import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons';
import { faFlag } from '@fortawesome/free-regular-svg-icons';
import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/messaging-room.vue'),
components: {
XMessage,
XForm
},
props: ['user', 'isNaked'],
props: {
user: {
type: Object,
requird: false,
},
group: {
type: Object,
requird: false,
},
isNaked: {
type: Boolean,
requird: false,
},
},
data() {
return {
@ -76,7 +89,10 @@ export default Vue.extend({
},
mounted() {
this.connection = this.$root.stream.connectToChannel('messaging', { otherparty: this.user.id });
this.connection = this.$root.stream.connectToChannel('messaging', {
otherparty: this.user ? this.user.id : undefined,
group: this.group ? this.group.id : undefined,
});
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
@ -147,7 +163,8 @@ export default Vue.extend({
const max = this.existMoreMessages ? 20 : 10;
this.$root.api('messaging/messages', {
userId: this.user.id,
userId: this.user ? this.user.id : undefined,
groupId: this.group ? this.group.id : undefined,
limit: max + 1,
untilId: this.existMoreMessages ? this.messages[0].id : undefined
}).then(messages => {
@ -199,12 +216,21 @@ export default Vue.extend({
}
},
onRead(ids) {
if (!Array.isArray(ids)) ids = [ids];
for (const id of ids) {
if (this.messages.some(x => x.id == id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist].isRead = true;
onRead(x) {
if (this.user) {
if (!Array.isArray(x)) x = [x];
for (const id of x) {
if (this.messages.some(x => x.id == id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist].isRead = true;
}
}
} else if (this.group) {
for (const id of x.ids) {
if (this.messages.some(x => x.id == id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist].reads.push(x.userId);
}
}
}
},

View File

@ -21,36 +21,62 @@
</div>
</div>
<div class="history" v-if="messages.length > 0">
<template>
<a v-for="message in messages"
class="user"
:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-is-me="isMe(message)"
:data-is-read="message.isRead"
@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
:key="message.id"
>
<div>
<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
<header>
<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
<mk-time :time="message.createdAt"/>
</header>
<div class="body">
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
</div>
<div class="title">{{ $t('user') }}</div>
<a v-for="message in messages"
class="user"
:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-is-me="isMe(message)"
:data-is-read="message.isRead"
@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
:key="message.id"
>
<div>
<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
<header>
<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
<mk-time :time="message.createdAt"/>
</header>
<div class="body">
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
</div>
</a>
</template>
</div>
</a>
</div>
<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p>
<div class="history" v-if="groupMessages.length > 0">
<div class="title">{{ $t('group') }}</div>
<a v-for="message in groupMessages"
class="user"
:href="`/i/messaging/group/${message.groupId}`"
:data-is-me="isMe(message)"
:data-is-read="message.reads.includes($store.state.i.id)"
@click.prevent="navigateGroup(message.group)"
:key="message.id"
>
<div>
<mk-avatar class="avatar" :user="message.user"/>
<header>
<span class="name">{{ message.group.name }}</span>
<mk-time :time="message.createdAt"/>
</header>
<div class="body">
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
</div>
</div>
</a>
</div>
<p class="no-history" v-if="!fetching && (messages.length == 0 && groupMessages.length == 0)">{{ $t('no-history') }}</p>
<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
<ui-margin>
<ui-button @click="startUser()"><fa :icon="faUser"/> {{ $t('start-with-user') }}</ui-button>
<ui-button @click="startGroup()"><fa :icon="faUsers"/> {{ $t('start-with-group') }}</ui-button>
</ui-margin>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faUser, faUsers } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../i18n';
import getAcct from '../../../../../misc/acct/render';
@ -71,9 +97,11 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
messages: [],
groupMessages: [],
q: null,
result: [],
connection: null
connection: null,
faUser, faUsers
};
},
mounted() {
@ -82,9 +110,12 @@ export default Vue.extend({
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
this.$root.api('messaging/history').then(messages => {
this.messages = messages;
this.fetching = false;
this.$root.api('messaging/history', { group: false }).then(messages => {
this.$root.api('messaging/history', { group: true }).then(groupMessages => {
this.messages = messages;
this.groupMessages = groupMessages;
this.fetching = false;
});
});
},
beforeDestroy() {
@ -96,16 +127,27 @@ export default Vue.extend({
return message.userId == this.$store.state.i.id;
},
onMessage(message) {
this.messages = this.messages.filter(m => !(
(m.recipientId == message.recipientId && m.userId == message.userId) ||
(m.recipientId == message.userId && m.userId == message.recipientId)));
if (message.recipientId) {
this.messages = this.messages.filter(m => !(
(m.recipientId == message.recipientId && m.userId == message.userId) ||
(m.recipientId == message.userId && m.userId == message.recipientId)));
this.messages.unshift(message);
this.messages.unshift(message);
} else if (message.groupId) {
this.groupMessages = this.groupMessages.filter(m => m.groupId !== message.groupId);
this.groupMessages.unshift(message);
}
},
onRead(ids) {
for (const id of ids) {
const found = this.messages.find(m => m.id == id);
if (found) found.isRead = true;
if (found) {
if (found.recipientId) {
found.isRead = true;
} else if (found.groupId) {
found.reads.push(this.$store.state.i.id);
}
}
}
},
search() {
@ -125,6 +167,9 @@ export default Vue.extend({
navigate(user) {
this.$emit('navigate', user);
},
navigateGroup(group) {
this.$emit('navigateGroup', group);
},
onSearchKeydown(e) {
switch (e.which) {
case 9: // [TAB]
@ -161,6 +206,30 @@ export default Vue.extend({
(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus();
break;
}
},
async startUser() {
const { result: user } = await this.$root.dialog({
user: {
local: true
}
});
if (user == null) return;
this.navigate(user);
},
async startGroup() {
const groups = await this.$root.api('users/groups/joined');
const { canceled, result: group } = await this.$root.dialog({
type: null,
title: this.$t('select-group'),
select: {
items: groups.map(group => ({
value: group, text: group.name
}))
},
showCancelButton: true
});
if (canceled) return;
this.navigateGroup(group);
}
}
});
@ -173,6 +242,9 @@ export default Vue.extend({
font-size 0.8em
> .history
> .title
padding 8px
> a
&:last-child
border-bottom none
@ -311,6 +383,13 @@ export default Vue.extend({
color rgba(#000, 0.3)
> .history
> .title
padding 6px 16px
margin 0 auto
max-width 500px
background rgba(0, 0, 0, 0.05)
color var(--text)
font-size 85%
> a
display block

View File

@ -1,5 +1,5 @@
<template>
<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
<article>
<header>
@ -32,16 +32,13 @@ export default Vue.extend({
display block
overflow hidden
width 100%
background var(--face)
border solid var(--lineWidth) var(--urlPreviewBorder)
border-radius 4px
overflow hidden
&.round
border-radius 8px
&.shadow
box-shadow 0 4px 16px rgba(#000, 0.1)
@media (min-width 500px)
box-shadow 0 8px 32px rgba(#000, 0.1)
&:hover
text-decoration none
border-color var(--urlPreviewBorderHover)
> .thumbnail
position absolute

View File

@ -113,7 +113,7 @@ export default Vue.extend({
padding 8px 10px
text-align center
font-weight normal
font-size 16px
font-size 14px
line-height 24px
border none
outline none

View File

@ -0,0 +1,15 @@
<template>
<div class="evrzpitu"></div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({});
</script>
<style lang="stylus" scoped>
.evrzpitu
margin 16px 0
border-bottom solid var(--lineWidth) var(--faceDivider)
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="zdcrxcne">
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({});
</script>
<style lang="stylus" scoped>
.zdcrxcne
margin 16px
</style>

View File

@ -1,95 +0,0 @@
<template>
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
<button class="ui" @click="add">{{ $t('create-list') }}</button>
<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/components/user-lists.vue'),
data() {
return {
fetching: true,
lists: []
};
},
mounted() {
this.$root.api('users/lists/list').then(lists => {
this.fetching = false;
this.lists = lists;
});
},
methods: {
add() {
this.$root.dialog({
title: this.$t('list-name'),
input: true
}).then(async ({ canceled, result: name }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
name
});
this.lists.push(list)
this.$emit('choosen', list);
});
},
choice(list) {
this.$emit('choosen', list);
}
}
});
</script>
<style lang="stylus" scoped>
.xkxvokkjlptzyewouewmceqcxhpgzprp
padding 16px
background: var(--bg)
> button
display block
margin-bottom 16px
color var(--primaryForeground)
background var(--primary)
width 100%
border-radius 38px
user-select none
cursor pointer
padding 0 16px
min-width 100px
line-height 38px
font-size 14px
font-weight 700
&:hover
background var(--primaryLighten10)
&:active
background var(--primaryDarken10)
a
display block
margin 8px 0
padding 8px
color var(--text)
background var(--face)
box-shadow 0 2px 16px var(--reversiListItemShadow)
border-radius 6px
cursor pointer
line-height 32px
*
pointer-events none
user-select none
&:hover
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
</style>

View File

@ -27,7 +27,7 @@ export default Vue.extend({
text: this.$t('push-to-list'),
action: this.pushList
}] as any;
if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) {
menu = menu.concat([null, {
icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],

View File

@ -0,0 +1,45 @@
<template>
<x-column>
<template #header>
<fa v-if="icon" :icon="icon"/>{{ title }}
</template>
<div>
<component :is="component" @init="init" v-bind="$attrs"/>
</div>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import XColumn from './deck.column.vue';
export default Vue.extend({
components: {
XColumn,
},
props: {
component: {
required: true
}
},
data() {
return {
title: null,
icon: null,
};
},
mounted() {
},
methods: {
init(v) {
this.title = v.title;
this.icon = v.icon;
}
}
});
</script>

View File

@ -1,34 +0,0 @@
<template>
<x-column>
<template #header>
<fa :icon="faHashtag"/>{{ $t('@.explore') }}
</template>
<div>
<x-explore v-bind="$attrs"/>
</div>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XExplore from '../../../common/views/pages/explore.vue';
import { faHashtag } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n(),
components: {
XColumn,
XExplore,
},
data() {
return {
faHashtag
};
}
});
</script>

View File

@ -116,6 +116,10 @@ export default Vue.extend({
},
created() {
this.$emit('init', {
title: this.$t('@.explore'),
icon: faHashtag
});
this.$root.api('hashtags/list', {
sort: '+attachedLocalUsers',
attachedToLocalUserOnly: true,

View File

@ -1,27 +1,30 @@
<template>
<mk-ui>
<template #header><fa :icon="['far', 'envelope']"/>{{ $t('title') }}</template>
<main>
<div v-for="req in requests">
<router-link :key="req.id" :to="req.follower | userPage">
<mk-user-name :user="req.follower"/>
</router-link>
<span>
<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a>
</span>
<div>
<ui-container :body-togglable="true">
<template #header>{{ $t('received-follow-requests') }}</template>
<div v-if="!fetching">
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="mcbzkkaw">
<div v-for="req in requests">
<router-link :key="req.id" :to="req.follower | userPage">
<mk-user-name :user="req.follower"/>
</router-link>
<span>
<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a>
</span>
</div>
</sequential-entrance>
</div>
</main>
</mk-ui>
</ui-container>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading';
import Progress from '../../scripts/loading';
export default Vue.extend({
i18n: i18n('mobile/views/pages/received-follow-requests.vue'),
i18n: i18n('common/views/pages/follow-requests.vue'),
data() {
return {
fetching: true,
@ -29,14 +32,10 @@ export default Vue.extend({
};
},
mounted() {
document.title = this.$t('title');
Progress.start();
this.$root.api('following/requests/list').then(requests => {
this.fetching = false;
this.requests = requests;
Progress.done();
});
},
@ -56,7 +55,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
main
.mcbzkkaw
> div
display flex
padding 16px

View File

@ -12,6 +12,11 @@
<small>@{{ page.user.username }}</small>
<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link>
<div class="like">
<button @click="unlike()" v-if="page.isLiked" :title="$t('unlike')"><fa :icon="faHeartS"/></button>
<button @click="like()" v-else :title="$t('like')"><fa :icon="faHeart"/></button>
<span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span>
</div>
</footer>
</div>
</template>
@ -19,8 +24,8 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import { faICursor, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faSave, faStickyNote } from '@fortawesome/free-regular-svg-icons';
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
import { faHeart } from '@fortawesome/free-regular-svg-icons';
import XBlock from './page.block.vue';
import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
import { collectPageVars } from '../../../scripts/collect-page-vars';
@ -76,7 +81,7 @@ export default Vue.extend({
return {
page: null,
script: null,
faPlus, faICursor, faSave, faStickyNote
faHeartS, faHeart
};
},
@ -103,6 +108,24 @@ export default Vue.extend({
getPageVars() {
return collectPageVars(this.page.content);
},
like() {
this.$root.api('pages/like', {
pageId: this.page.id,
}).then(() => {
this.page.isLiked = true;
this.page.likedCount++;
});
},
unlike() {
this.$root.api('pages/unlike', {
pageId: this.page.id,
}).then(() => {
this.page.isLiked = false;
this.page.likedCount--;
});
}
}
});
</script>
@ -161,4 +184,7 @@ export default Vue.extend({
> a + a
margin-left 8px
> .like
margin-top 16px
</style>

View File

@ -0,0 +1,143 @@
<template>
<div>
<ui-container :body-togglable="true">
<template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template>
<div class="rknalgpo" v-if="!fetching">
<ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button>
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages">
<x-page-preview v-for="page in pages" class="page" :page="page" :key="page.id"/>
</sequential-entrance>
<ui-button v-if="existMore" @click="fetchMore()" style="margin-top:16px;">{{ $t('@.load-more') }}</ui-button>
</div>
</ui-container>
<ui-container :body-togglable="true">
<template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template>
<div class="rknalgpo" v-if="!fetching">
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages">
<x-page-preview v-for="like in likes" class="page" :page="like.page" :key="like.page.id"/>
</sequential-entrance>
<ui-button v-if="existMoreLikes" @click="fetchMoreLiked()">{{ $t('@.load-more') }}</ui-button>
</div>
</ui-container>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../i18n';
import Progress from '../../scripts/loading';
import XPagePreview from '../../views/components/page-preview.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XPagePreview
},
data() {
return {
fetching: true,
pages: [],
existMore: false,
moreFetching: false,
likes: [],
existMoreLikes: false,
moreLikesFetching: false,
faStickyNote, faPlus, faEdit, faHeart
};
},
created() {
this.fetch();
this.$emit('init', {
title: this.$t('@.pages'),
icon: faStickyNote
});
},
methods: {
async fetch() {
Progress.start();
this.fetching = true;
const pages = await this.$root.api('i/pages', {
limit: 11
});
if (pages.length == 11) {
this.existMore = true;
pages.pop();
}
const likes = await this.$root.api('i/page-likes', {
limit: 11
});
if (likes.length == 11) {
this.existMoreLikes = true;
likes.pop();
}
this.pages = pages;
this.likes = likes;
this.fetching = false;
Progress.done();
},
fetchMore() {
this.moreFetching = true;
this.$root.api('i/pages', {
limit: 11,
untilId: this.pages[this.pages.length - 1].id
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
} else {
this.existMore = false;
}
this.pages = this.pages.concat(pages);
this.moreFetching = false;
});
},
fetchMoreLiked() {
this.moreLikesFetching = true;
this.$root.api('i/page-likes', {
limit: 11,
untilId: this.likes[this.likes.length - 1].id
}).then(pages => {
if (pages.length == 11) {
this.existMoreLikes = true;
pages.pop();
} else {
this.existMoreLikes = false;
}
this.likes = this.likes.concat(pages);
this.moreLikesFetching = false;
});
},
create() {
this.$router.push(`/i/pages/new`);
}
}
});
</script>
<style lang="stylus" scoped>
.rknalgpo
padding 16px
> .new
margin-bottom 16px
> * > .page:not(:last-child)
margin-bottom 8px
@media (min-width 500px)
> * > .page:not(:last-child)
margin-bottom 16px
</style>

View File

@ -0,0 +1,182 @@
<template>
<div class="ivrbakop">
<ui-container v-if="group">
<template #header><fa :icon="faUsers"/> {{ group.name }}</template>
<section>
<ui-margin>
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
</ui-margin>
</section>
</ui-container>
<ui-container>
<template #header><fa :icon="faUsers"/> {{ $t('users') }}</template>
<section>
<ui-margin>
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button>
</ui-margin>
<sequential-entrance animation="entranceFromTop" delay="25">
<div class="kjlrfbes" v-for="user in users">
<div>
<a :href="user | userPage">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div>
<header>
<b><mk-user-name :user="user"/></b>
<span class="username">@{{ user | acct }}</span>
</header>
<div>
<a @click="remove(user)">{{ $t('remove-user') }}</a>
</div>
</div>
</div>
</sequential-entrance>
</section>
</ui-container>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/user-group-editor.vue'),
props: {
groupId: {
required: true
}
},
data() {
return {
group: null,
users: [],
faICursor, faTrashAlt, faUsers, faPlus
};
},
created() {
this.$root.api('users/groups/show', {
groupId: this.groupId
}).then(group => {
this.group = group;
this.fetchUsers();
this.$emit('init', {
title: this.group.name,
icon: faUsers
});
});
},
methods: {
fetchUsers() {
this.$root.api('users/show', {
userIds: this.group.userIds
}).then(users => {
this.users = users;
});
},
rename() {
this.$root.dialog({
title: this.$t('rename'),
input: {
default: this.group.name
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('users/groups/update', {
groupId: this.group.id,
name: name
});
});
},
del() {
this.$root.dialog({
type: 'warning',
text: this.$t('delete-are-you-sure').replace('$1', this.group.name),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('users/groups/delete', {
groupId: this.group.id
}).then(() => {
this.$root.dialog({
type: 'success',
text: this.$t('deleted')
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
});
},
remove(user: any) {
this.$root.api('users/groups/pull', {
groupId: this.group.id,
userId: user.id
}).then(() => {
this.fetchUsers();
});
},
async add() {
const { result: user } = await this.$root.dialog({
user: {
local: true
}
});
if (user == null) return;
this.$root.api('users/groups/push', {
groupId: this.group.id,
userId: user.id
}).then(() => {
this.fetchUsers();
});
}
}
});
</script>
<style lang="stylus" scoped>
.ivrbakop
.kjlrfbes
display flex
padding 16px
border-top solid 1px var(--faceDivider)
> div:first-child
> a
> .avatar
width 64px
height 64px
> div:last-child
flex 1
padding-left 16px
@media (max-width 500px)
font-size 14px
> header
color var(--text)
> .username
margin-left 8px
opacity 0.7
</style>

View File

@ -0,0 +1,63 @@
<template>
<ui-container>
<template #header><fa :icon="faUsers"/> {{ $t('user-groups') }}</template>
<ui-margin>
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button>
</ui-margin>
<div class="hwgkdrbl" v-for="group in groups" :key="group.id">
<ui-hr/>
<ui-margin>
<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link>
</ui-margin>
</div>
</ui-container>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/user-groups.vue'),
data() {
return {
fetching: true,
groups: [],
faUsers, faPlus
};
},
mounted() {
this.$root.api('users/groups/owned').then(groups => {
this.fetching = false;
this.groups = groups;
});
this.$emit('init', {
title: this.$t('user-groups'),
icon: faUsers
});
},
methods: {
add() {
this.$root.dialog({
title: this.$t('group-name'),
input: true
}).then(async ({ canceled, result: name }) => {
if (canceled) return;
const list = await this.$root.api('users/groups/create', {
name
});
this.groups.push(list)
});
},
}
});
</script>
<style lang="stylus" scoped>
.hwgkdrbl
display block
</style>

View File

@ -1,18 +1,23 @@
<template>
<div class="cudqjmnl">
<ui-card>
<template #title><fa :icon="faList"/> {{ list.name }}</template>
<ui-container v-if="list">
<template #header><fa :icon="faListUl"/> {{ list.name }}</template>
<section>
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
<section class="fwvevrks">
<ui-margin>
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
</ui-margin>
</section>
</ui-card>
</ui-container>
<ui-card>
<template #title><fa :icon="faUsers"/> {{ $t('users') }}</template>
<ui-container>
<template #header><fa :icon="faUsers"/> {{ $t('users') }}</template>
<section>
<ui-margin>
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button>
</ui-margin>
<sequential-entrance animation="entranceFromTop" delay="25">
<div class="phcqulfl" v-for="user in users">
<div>
@ -32,34 +37,44 @@
</div>
</sequential-entrance>
</section>
</ui-card>
</ui-container>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { faList, faICursor, faUsers } from '@fortawesome/free-solid-svg-icons';
import { faListUl, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/user-list-editor.vue'),
props: {
list: {
listId: {
required: true
}
},
data() {
return {
list: null,
users: [],
faList, faICursor, faTrashAlt, faUsers
faListUl, faICursor, faTrashAlt, faUsers, faPlus
};
},
mounted() {
this.fetchUsers();
created() {
this.$root.api('users/lists/show', {
listId: this.listId
}).then(list => {
this.list = list;
this.fetchUsers();
this.$emit('init', {
title: this.list.name,
icon: faListUl
});
});
},
methods: {
@ -117,6 +132,21 @@ export default Vue.extend({
}).then(() => {
this.fetchUsers();
});
},
async add() {
const { result: user } = await this.$root.dialog({
user: {
local: true
}
});
if (user == null) return;
this.$root.api('users/lists/push', {
listId: this.list.id,
userId: user.id
}).then(() => {
this.fetchUsers();
});
}
}
});
@ -126,7 +156,7 @@ export default Vue.extend({
.cudqjmnl
.phcqulfl
display flex
padding 16px 0
padding 16px
border-top solid 1px var(--faceDivider)
> div:first-child
@ -143,6 +173,8 @@ export default Vue.extend({
font-size 14px
> header
color var(--text)
> .username
margin-left 8px
opacity 0.7

View File

@ -0,0 +1,63 @@
<template>
<ui-container>
<template #header><fa :icon="faListUl"/> {{ $t('user-lists') }}</template>
<ui-margin>
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-list') }}</ui-button>
</ui-margin>
<div class="cpqqyrst" v-for="list in lists" :key="list.id">
<ui-hr/>
<ui-margin>
<router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link>
</ui-margin>
</div>
</ui-container>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/user-lists.vue'),
data() {
return {
fetching: true,
lists: [],
faListUl, faPlus
};
},
mounted() {
this.$root.api('users/lists/list').then(lists => {
this.fetching = false;
this.lists = lists;
});
this.$emit('init', {
title: this.$t('user-lists'),
icon: faListUl
});
},
methods: {
add() {
this.$root.dialog({
title: this.$t('list-name'),
input: true
}).then(async ({ canceled, result: name }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
name
});
this.lists.push(list)
});
},
}
});
</script>
<style lang="stylus" scoped>
.cpqqyrst
display block
</style>

View File

@ -22,6 +22,7 @@ import MkShare from '../common/views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
import MkNotFound from '../common/views/pages/not-found.vue';
import MkSettings from './views/pages/settings.vue';
import DeckColumn from '../common/views/deck/deck.column-template.vue';
import Ctx from './views/components/context-menu.vue';
import PostFormWindow from './views/components/post-form-window.vue';
@ -138,9 +139,14 @@ init(async (launch, os) => {
{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) },
{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) },
{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) },
{ path: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) },
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }
{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) },
{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
]}
: { path: '/', component: MkHome, children: [
{ path: '', name: 'index', component: MkHomeTimeline },
@ -156,12 +162,18 @@ init(async (launch, os) => {
{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
{ path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) },
{ path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) },
{ path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) },
{ path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) },
{ path: '/i/groups', component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) },
{ path: '/i/groups/:groupId', props: true, component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default) },
{ path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) },
]},
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/i/messaging/group/:group', component: MkMessagingRoom },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },

View File

@ -1,7 +1,7 @@
<template>
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
<template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name :user="user"/></template>
<x-messaging-room :user="user" :class="$style.content"/>
<template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name v-if="user" :user="user"/><span v-else>{{ group.name }}</span></template>
<x-messaging-room :user="user" :group="group" :class="$style.content"/>
</mk-window>
</template>
@ -16,10 +16,14 @@ export default Vue.extend({
components: {
XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default)
},
props: ['user'],
props: ['user', 'group'],
computed: {
popout(): string {
return `${url}/i/messaging/${getAcct(this.user)}`;
if (this.user) {
return `${url}/i/messaging/${getAcct(this.user)}`;
} else if (this.group) {
return `${url}/i/messaging/group/${this.group.id}`;
}
}
}
});

View File

@ -1,7 +1,7 @@
<template>
<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
<template #header :class="$style.header"><fa icon="comments"/>{{ $t('@.messaging') }}</template>
<x-messaging :class="$style.content" @navigate="navigate"/>
<x-messaging :class="$style.content" @navigate="navigate" @navigateGroup="navigateGroup"/>
</mk-window>
</template>
@ -20,6 +20,11 @@ export default Vue.extend({
this.$root.new(MkMessagingRoomWindow, {
user: user
});
},
navigateGroup(group) {
this.$root.new(MkMessagingRoomWindow, {
group: group
});
}
}
});

View File

@ -1,70 +0,0 @@
<template>
<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<template #header><fa :icon="['far', 'envelope']"/> {{ $t('title') }}</template>
<div class="slpqaxdoxhvglersgjukmvizkqbmbokc">
<div v-for="req in requests">
<router-link :key="req.id" :to="req.follower | userPage">
<mk-user-name :user="req.follower"/>
</router-link>
<span>
<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a>
</span>
</div>
</div>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('desktop/views/components/received-follow-requests-window.vue'),
data() {
return {
fetching: true,
requests: []
};
},
mounted() {
this.$root.api('following/requests/list').then(requests => {
this.fetching = false;
this.requests = requests;
});
},
methods: {
accept(user) {
this.$root.api('following/requests/accept', { userId: user.id }).then(() => {
this.requests = this.requests.filter(r => r.follower.id != user.id);
});
},
reject(user) {
this.$root.api('following/requests/reject', { userId: user.id }).then(() => {
this.requests = this.requests.filter(r => r.follower.id != user.id);
});
},
close() {
(this as any).$refs.window.close();
}
}
});
</script>
<style lang="stylus" scoped>
.slpqaxdoxhvglersgjukmvizkqbmbokc
padding 16px
> button
margin-bottom 16px
> div
display flex
padding 16px
border solid 1px var(--faceDivider)
border-radius 4px
> span
margin 0 0 0 auto
</style>

View File

@ -28,12 +28,19 @@
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li @click="list">
<p>
<li>
<router-link to="/i/lists">
<i><fa icon="list" fixed-width/></i>
<span>{{ $t('lists') }}</span>
<i><fa icon="angle-right"/></i>
</p>
</router-link>
</li>
<li>
<router-link to="/i/groups">
<i><fa :icon="faUsers" fixed-width/></i>
<span>{{ $t('groups') }}</span>
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li>
<router-link to="/i/pages">
@ -42,12 +49,12 @@
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
<p>
<li v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
<router-link to="/i/follow-requests">
<i><fa :icon="['far', 'envelope']" fixed-width/></i>
<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
<i><fa icon="angle-right"/></i>
</p>
</router-link>
</li>
</ul>
<ul>
@ -96,12 +103,10 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import MkUserListsWindow from './user-lists-window.vue';
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
// import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import contains from '../../../common/scripts/contains';
import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faHome, faColumns, faUsers } from '@fortawesome/free-solid-svg-icons';
import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
@ -109,7 +114,7 @@ export default Vue.extend({
data() {
return {
isOpen: false,
faHome, faColumns, faMoon, faSun, faStickyNote
faHome, faColumns, faMoon, faSun, faStickyNote, faUsers
};
},
computed: {
@ -147,14 +152,6 @@ export default Vue.extend({
this.close();
this.$root.new(MkDriveWindow);
},
list() {
this.close();
this.$root.new(MkUserListsWindow);
},
followRequests() {
this.close();
this.$root.new(MkFollowRequestsWindow);
},
signout() {
this.$root.signout();
},

View File

@ -72,8 +72,6 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import MkUserListsWindow from './user-lists-window.vue';
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import MkMessagingWindow from './messaging-window.vue';

View File

@ -1,24 +0,0 @@
<template>
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
<template #header><fa icon="list"/> {{ list.name }}</template>
<x-editor :list="list"/>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
import XEditor from '../../../common/views/components/user-list-editor.vue';
export default Vue.extend({
components: {
XEditor
},
props: {
list: {
required: true
}
}
});
</script>

View File

@ -1,36 +0,0 @@
<template>
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
<template #header><fa icon="list"/> {{ $t('title') }}</template>
<x-lists :class="$style.content" @choosen="choosen"/>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import MkUserListWindow from './user-list-window.vue';
export default Vue.extend({
i18n: i18n('desktop/views/components/user-lists-window.vue'),
components: {
XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
},
methods: {
close() {
(this as any).$refs.window.close();
},
choosen(list) {
this.$root.new(MkUserListWindow, {
list
});
}
}
});
</script>
<style lang="stylus" module>
.content
height 100%
overflow auto
</style>

View File

@ -1,92 +0,0 @@
<template>
<div class="rknalgpo" v-if="!fetching">
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
<sequential-entrance animation="entranceFromTop" delay="25">
<template v-for="page in pages">
<x-page-preview class="page" :page="page" :key="page.id"/>
</template>
</sequential-entrance>
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import XPagePreview from '../../../common/views/components/page-preview.vue';
export default Vue.extend({
i18n: i18n(),
components: {
XPagePreview
},
data() {
return {
fetching: true,
pages: [],
existMore: false,
moreFetching: false,
faStickyNote, faPlus
};
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
this.$root.api('i/pages', {
limit: 11
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
}
this.pages = pages;
this.fetching = false;
Progress.done();
});
},
fetchMore() {
this.moreFetching = true;
this.$root.api('i/pages', {
limit: 11,
untilId: this.pages[this.pages.length - 1].id
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
} else {
this.existMore = false;
}
this.pages = this.pages.concat(pages);
this.moreFetching = false;
});
},
create() {
this.$router.push(`/i/pages/new`);
}
}
});
</script>
<style lang="stylus" scoped>
.rknalgpo
margin 0 auto
> * > .page
margin-bottom 8px
@media (min-width 500px)
> * > .page
margin-bottom 16px
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="mk-messaging-room-page">
<x-messaging-room v-if="user" :user="user" :is-naked="true"/>
<x-messaging-room v-if="user || group" :user="user" :group="group" :is-naked="true"/>
</div>
</template>
@ -19,7 +19,8 @@ export default Vue.extend({
data() {
return {
fetching: true,
user: null
user: null,
group: null
};
},
watch: {
@ -47,14 +48,25 @@ export default Vue.extend({
Progress.start();
this.fetching = true;
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
if (this.$route.params.user) {
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
document.title = this.$t('@.messaging') + ': ' + getUserName(this.user);
document.title = this.$t('@.messaging') + ': ' + getUserName(this.user);
Progress.done();
});
Progress.done();
});
} else {
this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => {
this.group = group;
this.fetching = false;
document.title = this.$t('@.messaging') + ': ' + this.group.name;
Progress.done();
});
}
}
}
});

View File

@ -4,7 +4,7 @@
<template #header><fa icon="comments"/>{{ $t('@.messaging') }}</template>
<template #func><button @click="add"><fa icon="plus"/></button></template>
<x-messaging ref="index" compact @navigate="navigate"/>
<x-messaging ref="index" compact @navigate="navigate" @navigateGroup="navigateGroup"/>
</ui-container>
</div>
</template>
@ -31,6 +31,11 @@ export default define({
user: user
});
},
navigateGroup(group) {
this.$root.new(MkMessagingRoomWindow, {
group: group
});
},
add() {
this.$root.new(MkMessagingWindow);
},

View File

@ -18,17 +18,16 @@ import MkDrive from './views/pages/drive.vue';
import MkWidgets from './views/pages/widgets.vue';
import MkMessaging from './views/pages/messaging.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkReceivedFollowRequests from './views/pages/received-follow-requests.vue';
import MkNote from './views/pages/note.vue';
import MkSearch from './views/pages/search.vue';
import MkFavorites from './views/pages/favorites.vue';
import MkUserLists from './views/pages/user-lists.vue';
import MkUserList from './views/pages/user-list.vue';
import UI from './views/pages/ui.vue';
import MkReversi from './views/pages/games/reversi.vue';
import MkTag from './views/pages/tag.vue';
import MkShare from '../common/views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
import MkNotFound from '../common/views/pages/not-found.vue';
import DeckColumn from '../common/views/deck/deck.column-template.vue';
import PostForm from './views/components/post-form-dialog.vue';
import FileChooser from './views/components/drive-file-chooser.vue';
@ -125,9 +124,14 @@ init((launch, os) => {
{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) },
{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) },
{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) },
{ path: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) },
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }
{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) },
{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
]}]
: [
{ path: '/', name: 'index', component: MkIndex },
@ -135,12 +139,15 @@ init((launch, os) => {
{ path: '/signup', name: 'signup', component: MkSignup },
{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) },
{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
{ path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) },
{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
{ path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
{ path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
{ path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) },
{ path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
{ path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) },
{ path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
{ path: '/i/messaging/group/:group', component: MkMessagingRoom },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', name: 'drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },
@ -151,8 +158,8 @@ init((launch, os) => {
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
{ path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) },
{ path: '/explore', name: 'explore', component: () => import('./views/pages/explore.vue').then(m => m.default) },
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('./views/pages/explore.vue').then(m => m.default) },
{ path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
{ path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
{ path: '/share', component: MkShare },
{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi },
{ path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [

View File

@ -19,7 +19,7 @@
<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><p @click="showNotifications = true"><i><fa :icon="['far', 'bell']" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li>
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/follow-requests" :data-active="$route.name == 'follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
@ -28,6 +28,7 @@
<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/groups" :data-active="$route.name == 'user-groups'"><i><fa :icon="faUsers" fixed-width/></i>{{ $t('user-groups') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li>
</ul>
@ -66,7 +67,7 @@
import Vue from 'vue';
import i18n from '../../../i18n';
import { lang } from '../../../config';
import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faNewspaper, faHashtag, faHome, faColumns, faUsers } from '@fortawesome/free-solid-svg-icons';
import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
import { search } from '../../../common/scripts/search';
@ -87,7 +88,7 @@ export default Vue.extend({
announcements: [],
searching: false,
showNotifications: false,
faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote
faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote, faUsers
};
},

View File

@ -1,28 +0,0 @@
<template>
<mk-ui>
<template #header><span style="margin-right:4px;"><fa :icon="faHashtag"/></span>{{ $t('@.explore') }}</template>
<main>
<x-explore v-bind="$attrs"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { faHashtag } from '@fortawesome/free-solid-svg-icons';
import XExplore from '../../../common/views/pages/explore.vue';
export default Vue.extend({
i18n: i18n(''),
components: {
XExplore
},
data() {
return {
faHashtag
};
},
});
</script>

View File

@ -2,9 +2,10 @@
<mk-ui>
<template #header>
<template v-if="user"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span><mk-user-name :user="user"/></template>
<template v-else-if="group"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ group.name }}</template>
<template v-else><mk-ellipsis/></template>
</template>
<x-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
<x-messaging-room v-if="!fetching" :user="user" :group="group" :is-naked="true"/>
</mk-ui>
</template>
@ -22,6 +23,7 @@ export default Vue.extend({
return {
fetching: true,
user: null,
group: null,
unwatchDarkmode: null
};
},
@ -48,12 +50,21 @@ export default Vue.extend({
methods: {
fetch() {
this.fetching = true;
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
if (this.$route.params.user) {
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`;
});
document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`;
});
} else {
this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => {
this.group = group;
this.fetching = false;
document.title = this.$t('@.messaging') + ': ' + this.group.name;
});
}
}
}
});

View File

@ -1,7 +1,7 @@
<template>
<mk-ui>
<template #header><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</template>
<x-messaging @navigate="navigate" :header-top="48"/>
<x-messaging @navigate="navigate" @navigateGroup="navigateGroup" :header-top="48"/>
</mk-ui>
</template>
@ -21,6 +21,9 @@ export default Vue.extend({
methods: {
navigate(user) {
(this as any).$router.push(`/i/messaging/${getAcct(user)}`);
},
navigateGroup(group) {
(this as any).$router.push(`/i/messaging/group/${group.id}`);
}
}
});

View File

@ -1,94 +0,0 @@
<template>
<mk-ui>
<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template>
<main>
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
<sequential-entrance animation="entranceFromTop" delay="25">
<template v-for="page in pages">
<x-page-preview class="page" :page="page" :key="page.id"/>
</template>
</sequential-entrance>
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import XPagePreview from '../../../common/views/components/page-preview.vue';
export default Vue.extend({
i18n: i18n(),
components: {
XPagePreview
},
data() {
return {
fetching: true,
pages: [],
existMore: false,
moreFetching: false,
faStickyNote, faPlus
};
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
this.$root.api('i/pages', {
limit: 11
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
}
this.pages = pages;
this.fetching = false;
Progress.done();
});
},
fetchMore() {
this.moreFetching = true;
this.$root.api('i/pages', {
limit: 11,
untilId: this.pages[this.pages.length - 1].id
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
} else {
this.existMore = false;
}
this.pages = this.pages.concat(pages);
this.moreFetching = false;
});
},
create() {
this.$router.push(`/i/pages/new`);
}
}
});
</script>
<style lang="stylus" scoped>
main
> * > .page
margin-bottom 8px
@media (min-width 500px)
> * > .page
margin-bottom 16px
</style>

View File

@ -0,0 +1,38 @@
<template>
<mk-ui>
<template #header><span style="margin-right:4px;" v-if="icon"><fa :icon="icon"/></span>{{ title }}</template>
<main>
<component :is="component" @init="init" v-bind="$attrs"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
component: {
required: true
}
},
data() {
return {
title: null,
icon: null,
};
},
mounted() {
},
methods: {
init(v) {
this.title = v.title;
this.icon = v.icon;
}
}
});
</script>

View File

@ -1,48 +0,0 @@
<template>
<mk-ui>
<template #header v-if="!fetching"><fa icon="list"/>{{ list.name }}</template>
<main v-if="!fetching">
<x-editor :list="list"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
import XEditor from '../../../common/views/components/user-list-editor.vue';
export default Vue.extend({
components: {
XEditor
},
data() {
return {
fetching: true,
list: null
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
this.$root.api('users/lists/show', {
listId: this.$route.params.list
}).then(list => {
this.list = list;
this.fetching = false;
Progress.done();
});
}
}
});
</script>

View File

@ -1,35 +0,0 @@
<template>
<mk-ui>
<template #header><fa icon="list"/>{{ $t('title') }}</template>
<template #func><button @click="$refs.lists.add()"><fa icon="plus"/></button></template>
<x-lists ref="lists" @choosen="choosen"/>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('mobile/views/pages/user-lists.vue'),
data() {
return {
fetching: true,
lists: []
};
},
components: {
XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
},
mounted() {
document.title = this.$t('title');
},
methods: {
choosen(list) {
if (!list) return;
this.$router.push(`/i/lists/${list.id}`);
}
}
});
</script>

View File

@ -27,13 +27,6 @@ export type Source = {
port: number;
pass: string;
};
drive?: {
storage: string;
bucket?: string;
prefix?: string;
baseUrl?: string;
config?: any;
};
autoAdmin?: boolean;

View File

@ -24,6 +24,8 @@ import { SwSubscription } from '../models/entities/sw-subscription';
import { Blocking } from '../models/entities/blocking';
import { UserList } from '../models/entities/user-list';
import { UserListJoining } from '../models/entities/user-list-joining';
import { UserGroup } from '../models/entities/user-group';
import { UserGroupJoining } from '../models/entities/user-group-joining';
import { Hashtag } from '../models/entities/hashtag';
import { NoteFavorite } from '../models/entities/note-favorite';
import { AbuseUserReport } from '../models/entities/abuse-user-report';
@ -41,6 +43,7 @@ import { UserKeypair } from '../models/entities/user-keypair';
import { UserPublickey } from '../models/entities/user-publickey';
import { UserProfile } from '../models/entities/user-profile';
import { Page } from '../models/entities/page';
import { PageLike } from '../models/entities/page-like';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@ -105,6 +108,8 @@ export function initDb(justBorrow = false, sync = false, log = false) {
UserPublickey,
UserList,
UserListJoining,
UserGroup,
UserGroupJoining,
UserNotePining,
Following,
FollowRequest,
@ -116,6 +121,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
NoteWatching,
NoteUnread,
Page,
PageLike,
Log,
DriveFile,
DriveFolder,

View File

@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
import { User } from './user';
import { DriveFile } from './drive-file';
import { id } from '../id';
import { UserGroup } from './user-group';
@Entity()
export class MessagingMessage {
@ -29,10 +30,10 @@ export class MessagingMessage {
@Index()
@Column({
...id(),
...id(), nullable: true,
comment: 'The recipient user ID.'
})
public recipientId: User['id'];
public recipientId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE'
@ -40,6 +41,19 @@ export class MessagingMessage {
@JoinColumn()
public recipient: User | null;
@Index()
@Column({
...id(), nullable: true,
comment: 'The recipient group ID.'
})
public groupId: UserGroup['id'] | null;
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE'
})
@JoinColumn()
public group: UserGroup | null;
@Column('varchar', {
length: 4096, nullable: true
})
@ -50,6 +64,12 @@ export class MessagingMessage {
})
public isRead: boolean;
@Column({
...id(),
array: true, default: '{}'
})
public reads: User['id'][];
@Column({
...id(),
nullable: true,

View File

@ -288,4 +288,61 @@ export class Meta {
nullable: true
})
public feedbackUrl: string | null;
@Column('boolean', {
default: false,
})
public useObjectStorage: boolean;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageBucket: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStoragePrefix: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageBaseUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageEndpoint: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageRegion: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageAccessKey: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public objectStorageSecretKey: string | null;
@Column('integer', {
nullable: true
})
public objectStoragePort: number | null;
@Column('boolean', {
default: true,
})
public objectStorageUseSSL: boolean;
}

View File

@ -0,0 +1,33 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { Page } from './page';
@Entity()
@Index(['userId', 'pageId'], { unique: true })
export class PageLike {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column(id())
public pageId: Page['id'];
@ManyToOne(type => Page, {
onDelete: 'CASCADE'
})
@JoinColumn()
public page: Page | null;
}

View File

@ -95,6 +95,11 @@ export class Page {
})
public visibleUserIds: User['id'][];
@Column('integer', {
default: 0
})
public likedCount: number;
constructor(data: Partial<Page>) {
if (data == null) return;

View File

@ -0,0 +1,41 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user';
import { UserGroup } from './user-group';
import { id } from '../id';
@Entity()
export class UserGroupJoining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroupJoining.'
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The user ID.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
comment: 'The group ID.'
})
public userGroupId: UserGroup['id'];
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE'
})
@JoinColumn()
public userGroup: UserGroup | null;
}

View File

@ -0,0 +1,46 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserGroup {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroup.'
})
public createdAt: Date;
@Column('varchar', {
length: 256,
})
public name: string;
@Index()
@Column({
...id(),
comment: 'The ID of owner.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false,
})
public isPrivate: boolean;
constructor(data: Partial<UserGroup>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -6,7 +6,6 @@ import { PollVote } from './entities/poll-vote';
import { Meta } from './entities/meta';
import { SwSubscription } from './entities/sw-subscription';
import { NoteWatching } from './entities/note-watching';
import { UserListJoining } from './entities/user-list-joining';
import { NoteUnread } from './entities/note-unread';
import { RegistrationTicket } from './entities/registration-tickets';
import { UserRepository } from './repositories/user';
@ -20,6 +19,9 @@ import { SigninRepository } from './repositories/signin';
import { MessagingMessageRepository } from './repositories/messaging-message';
import { ReversiGameRepository } from './repositories/games/reversi/game';
import { UserListRepository } from './repositories/user-list';
import { UserListJoining } from './entities/user-list-joining';
import { UserGroupRepository } from './repositories/user-group';
import { UserGroupJoining } from './entities/user-group-joining';
import { FollowRequestRepository } from './repositories/follow-request';
import { MutingRepository } from './repositories/muting';
import { BlockingRepository } from './repositories/blocking';
@ -36,6 +38,7 @@ import { AuthSessionRepository } from './repositories/auth-session';
import { UserProfile } from './entities/user-profile';
import { HashtagRepository } from './repositories/hashtag';
import { PageRepository } from './repositories/page';
import { PageLikeRepository } from './repositories/page-like';
export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository);
@ -51,6 +54,8 @@ export const UserKeypairs = getRepository(UserKeypair);
export const UserPublickeys = getRepository(UserPublickey);
export const UserLists = getCustomRepository(UserListRepository);
export const UserListJoinings = getRepository(UserListJoining);
export const UserGroups = getCustomRepository(UserGroupRepository);
export const UserGroupJoinings = getRepository(UserGroupJoining);
export const UserNotePinings = getRepository(UserNotePining);
export const Followings = getCustomRepository(FollowingRepository);
export const FollowRequests = getCustomRepository(FollowRequestRepository);
@ -74,3 +79,4 @@ export const ReversiGames = getCustomRepository(ReversiGameRepository);
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
export const Logs = getRepository(Log);
export const Pages = getCustomRepository(PageRepository);
export const PageLikes = getCustomRepository(PageLikeRepository);

View File

@ -1,6 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { MessagingMessage } from '../entities/messaging-message';
import { Users, DriveFiles } from '..';
import { Users, DriveFiles, UserGroups } from '..';
import { ensure } from '../../prelude/ensure';
import { types, bool, SchemaType } from '../../misc/schema';
@ -16,11 +16,13 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
src: MessagingMessage['id'] | MessagingMessage,
me?: any,
options?: {
populateRecipient: boolean
populateRecipient?: boolean,
populateGroup?: boolean,
}
): Promise<PackedMessagingMessage> {
const opts = options || {
populateRecipient: true
populateRecipient: true,
populateGroup: true,
};
const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
@ -32,10 +34,13 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
userId: message.userId,
user: await Users.pack(message.user || message.userId, me),
recipientId: message.recipientId,
recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
groupId: message.recipientId,
group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined,
fileId: message.fileId,
file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
isRead: message.isRead
isRead: message.isRead,
reads: message.reads,
};
}
}
@ -83,17 +88,36 @@ export const packedMessagingMessageSchema = {
},
recipientId: {
type: types.string,
optional: bool.false, nullable: bool.false,
optional: bool.false, nullable: bool.true,
format: 'id',
},
recipient: {
type: types.object,
optional: bool.true, nullable: bool.false,
optional: bool.true, nullable: bool.true,
ref: 'User'
},
groupId: {
type: types.string,
optional: bool.false, nullable: bool.true,
format: 'id',
},
group: {
type: types.object,
optional: bool.true, nullable: bool.true,
ref: 'UserGroup'
},
isRead: {
type: types.boolean,
optional: bool.true, nullable: bool.false,
},
reads: {
type: types.array,
optional: bool.true, nullable: bool.false,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
format: 'id'
}
},
},
};

View File

@ -0,0 +1,26 @@
import { EntityRepository, Repository } from 'typeorm';
import { PageLike } from '../entities/page-like';
import { Pages } from '..';
import { ensure } from '../../prelude/ensure';
@EntityRepository(PageLike)
export class PageLikeRepository extends Repository<PageLike> {
public async pack(
src: PageLike['id'] | PageLike,
me?: any
) {
const like = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return {
id: like.id,
page: await Pages.pack(like.page || like.pageId, me),
};
}
public packMany(
likes: any[],
me: any
) {
return Promise.all(likes.map(x => this.pack(x, me)));
}
}

View File

@ -1,24 +1,30 @@
import { EntityRepository, Repository } from 'typeorm';
import { Page } from '../entities/page';
import { SchemaType, types, bool } from '../../misc/schema';
import { Users, DriveFiles } from '..';
import { Users, DriveFiles, PageLikes } from '..';
import { awaitAll } from '../../prelude/await-all';
import { DriveFile } from '../entities/drive-file';
import { User } from '../entities/user';
import { ensure } from '../../prelude/ensure';
export type PackedPage = SchemaType<typeof packedPageSchema>;
@EntityRepository(Page)
export class PageRepository extends Repository<Page> {
public async pack(
src: Page,
src: Page['id'] | Page,
me?: User['id'] | User | null | undefined,
): Promise<PackedPage> {
const meId = me ? typeof me === 'string' ? me : me.id : null;
const page = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const attachedFiles: Promise<DriveFile | undefined>[] = [];
const collectFile = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'image') {
attachedFiles.push(DriveFiles.findOne({
id: x.fileId,
userId: src.userId
userId: page.userId
}));
}
if (x.children) {
@ -26,7 +32,7 @@ export class PageRepository extends Repository<Page> {
}
}
};
collectFile(src.content);
collectFile(page.content);
// 後方互換性のため
let migrated = false;
@ -47,29 +53,31 @@ export class PageRepository extends Repository<Page> {
}
}
};
migrate(src.content);
migrate(page.content);
if (migrated) {
this.update(src.id, {
content: src.content
this.update(page.id, {
content: page.content
});
}
return await awaitAll({
id: src.id,
createdAt: src.createdAt.toISOString(),
updatedAt: src.updatedAt.toISOString(),
userId: src.userId,
user: Users.pack(src.user || src.userId),
content: src.content,
variables: src.variables,
title: src.title,
name: src.name,
summary: src.summary,
alignCenter: src.alignCenter,
font: src.font,
eyeCatchingImageId: src.eyeCatchingImageId,
eyeCatchingImage: src.eyeCatchingImageId ? await DriveFiles.pack(src.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles))
id: page.id,
createdAt: page.createdAt.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
user: Users.pack(page.user || page.userId),
content: page.content,
variables: page.variables,
title: page.title,
name: page.name,
summary: page.summary,
alignCenter: page.alignCenter,
font: page.font,
eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
likedCount: page.likedCount,
isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
});
}

View File

@ -0,0 +1,61 @@
import { EntityRepository, Repository } from 'typeorm';
import { UserGroup } from '../entities/user-group';
import { ensure } from '../../prelude/ensure';
import { UserGroupJoinings } from '..';
import { bool, types, SchemaType } from '../../misc/schema';
export type PackedUserGroup = SchemaType<typeof packedUserGroupSchema>;
@EntityRepository(UserGroup)
export class UserGroupRepository extends Repository<UserGroup> {
public async pack(
src: UserGroup['id'] | UserGroup,
): Promise<PackedUserGroup> {
const userGroup = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const users = await UserGroupJoinings.find({
userGroupId: userGroup.id
});
return {
id: userGroup.id,
createdAt: userGroup.createdAt.toISOString(),
name: userGroup.name,
userIds: users.map(x => x.userId)
};
}
}
export const packedUserGroupSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
format: 'id',
description: 'The unique identifier for this UserGroup.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
format: 'date-time',
description: 'The date that the UserGroup was created.'
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
description: 'The name of the UserGroup.'
},
userIds: {
type: types.array,
nullable: bool.false, optional: bool.true,
items: {
type: types.string,
nullable: bool.false, optional: bool.false,
format: 'id',
}
},
},
};

View File

@ -1,6 +1,6 @@
import { EntityRepository, Repository, In } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..';
import { ensure } from '../../prelude/ensure';
import config from '../../config';
import { SchemaType, bool, types } from '../../misc/schema';
@ -54,6 +54,31 @@ export class UserRepository extends Repository<User> {
};
}
public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
const joinings = await UserGroupJoinings.find({ userId: userId });
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null)));
const [withUser, withGroups] = await Promise.all([
// TODO: ミュートを考慮
MessagingMessages.count({
where: {
recipientId: userId,
isRead: false
},
take: 1
}).then(count => count > 0),
groupQs
]);
return withUser || withGroups.some(x => x);
}
public async pack(
src: User['id'] | User,
me?: User['id'] | User | null | undefined,
@ -151,13 +176,7 @@ export class UserRepository extends Repository<User> {
autoWatch: profile!.autoWatch,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile!.carefulBot,
hasUnreadMessagingMessage: MessagingMessages.count({
where: {
recipientId: user.id,
isRead: false
},
take: 1
}).then(count => count > 0),
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: Notifications.count({
where: {
notifieeId: user.id,

18
src/ormconfig.ts Normal file
View File

@ -0,0 +1,18 @@
import * as fs from 'fs';
import config from './config';
const json = {
type: 'postgres',
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
entities: ['src/models/entities/*.ts'],
migrations: ['migration/*.ts'],
cli: {
migrationsDir: 'migration'
}
};
fs.writeFileSync('ormconfig.json', JSON.stringify(json));

View File

@ -1,21 +1,33 @@
import { publishMainStream } from '../../../services/stream';
import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream';
import { publishMessagingStream } from '../../../services/stream';
import { publishMessagingIndexStream } from '../../../services/stream';
import { User } from '../../../models/entities/user';
import { MessagingMessage } from '../../../models/entities/messaging-message';
import { MessagingMessages } from '../../../models';
import { MessagingMessages, UserGroupJoinings, Users } from '../../../models';
import { In } from 'typeorm';
import { IdentifiableError } from '../../../misc/identifiable-error';
import { UserGroup } from '../../../models/entities/user-group';
/**
* Mark messages as read
*/
export default async (
export async function readUserMessagingMessage(
userId: User['id'],
otherpartyId: User['id'],
messageIds: MessagingMessage['id'][]
) => {
) {
if (messageIds.length === 0) return;
const messages = await MessagingMessages.find({
id: In(messageIds)
});
for (const message of messages) {
if (message.recipientId !== userId) {
throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).');
}
}
// Update documents
await MessagingMessages.update({
id: In(messageIds),
@ -30,14 +42,62 @@ export default async (
publishMessagingStream(otherpartyId, userId, 'read', messageIds);
publishMessagingIndexStream(userId, 'read', messageIds);
// Calc count of my unread messages
const count = await MessagingMessages.count({
recipientId: userId,
isRead: false
});
if (count == 0) {
if (!Users.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllMessagingMessages');
}
};
}
/**
* Mark messages as read
*/
export async function readGroupMessagingMessage(
userId: User['id'],
groupId: UserGroup['id'],
messageIds: MessagingMessage['id'][]
) {
if (messageIds.length === 0) return;
// check joined
const joining = await UserGroupJoinings.findOne({
userId: userId,
userGroupId: groupId
});
if (joining == null) {
throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).');
}
const messages = await MessagingMessages.find({
id: In(messageIds)
});
const reads = [];
for (const message of messages) {
if (message.userId === userId) continue;
if (message.reads.includes(userId)) continue;
// Update document
await MessagingMessages.createQueryBuilder().update()
.set({
reads: (() => `array_append("reads", '${joining.userId}')`) as any
})
.where('id = :id', { id: message.id })
.execute();
reads.push(message.id);
}
// Publish event
publishGroupMessagingStream(groupId, 'read', {
ids: reads,
userId: userId
});
publishMessagingIndexStream(userId, 'read', reads);
if (!Users.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllMessagingMessages');
}
}

View File

@ -0,0 +1,32 @@
import $ from 'cafy';
import define from '../../define';
import del from '../../../../services/drive/delete-file';
import { DriveFiles } from '../../../../models';
import { ID } from '../../../../misc/cafy-id';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to suspend'
}
},
}
};
export default define(meta, async (ps, me) => {
const files = await DriveFiles.find({
userId: ps.userId
});
for (const file of files) {
del(file);
}
});

View File

@ -53,16 +53,18 @@ export default define(meta, async (ps) => {
if (blackDomains.length > 0) {
query.andWhere(new Brackets(qb => {
for (const blackDomain of blackDomains) {
const subDomains = blackDomain.split('.');
let i = 0;
for (const subDomain of subDomains) {
const p = `blackSubDomain_${subDomain}_${i}`;
// 全体で否定できないのでド・モルガンの法則で
// !(P && Q) を !P || !Q で表す
// SQL is 1 based, so we need '+ 1'
qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain });
i++;
}
qb.andWhere(new Brackets(qb => {
const subDomains = blackDomain.split('.');
let i = 0;
for (const subDomain of subDomains) {
const p = `blackSubDomain_${subDomain}_${i}`;
// 全体で否定できないのでド・モルガンの法則で
// !(P && Q) を !P || !Q で表す
// SQL is 1 based, so we need '+ 1'
qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain });
i++;
}
}));
}
}));
}

View File

@ -357,7 +357,47 @@ export const meta = {
desc: {
'ja-JP': 'フィードバックのURL'
}
}
},
useObjectStorage: {
validator: $.optional.bool
},
objectStorageBaseUrl: {
validator: $.optional.nullable.str
},
objectStorageBucket: {
validator: $.optional.nullable.str
},
objectStoragePrefix: {
validator: $.optional.nullable.str
},
objectStorageEndpoint: {
validator: $.optional.nullable.str
},
objectStorageRegion: {
validator: $.optional.nullable.str
},
objectStoragePort: {
validator: $.optional.nullable.num
},
objectStorageAccessKey: {
validator: $.optional.nullable.str
},
objectStorageSecretKey: {
validator: $.optional.nullable.str
},
objectStorageUseSSL: {
validator: $.optional.bool
},
}
};
@ -560,6 +600,46 @@ export default define(meta, async (ps) => {
set.feedbackUrl = ps.feedbackUrl;
}
if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage;
}
if (ps.objectStorageBaseUrl !== undefined) {
set.objectStorageBaseUrl = ps.objectStorageBaseUrl;
}
if (ps.objectStorageBucket !== undefined) {
set.objectStorageBucket = ps.objectStorageBucket;
}
if (ps.objectStoragePrefix !== undefined) {
set.objectStoragePrefix = ps.objectStoragePrefix;
}
if (ps.objectStorageEndpoint !== undefined) {
set.objectStorageEndpoint = ps.objectStorageEndpoint;
}
if (ps.objectStorageRegion !== undefined) {
set.objectStorageRegion = ps.objectStorageRegion;
}
if (ps.objectStoragePort !== undefined) {
set.objectStoragePort = ps.objectStoragePort;
}
if (ps.objectStorageAccessKey !== undefined) {
set.objectStorageAccessKey = ps.objectStorageAccessKey;
}
if (ps.objectStorageSecretKey !== undefined) {
set.objectStorageSecretKey = ps.objectStorageSecretKey;
}
if (ps.objectStorageUseSSL !== undefined) {
set.objectStorageUseSSL = ps.objectStorageUseSSL;
}
await getConnection().transaction(async transactionalEntityManager => {
const meta = await transactionalEntityManager.findOne(Meta, {
order: {

View File

@ -0,0 +1,45 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { PageLikes } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
'ja-JP': '「いいね」したページ一覧を取得します。',
'en-US': 'Get liked pages'
},
tags: ['account', 'pages'],
requireCredential: true,
kind: 'read:page-likes',
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
},
}
};
export default define(meta, async (ps, user) => {
const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
.andWhere(`like.userId = :meId`, { meId: user.id })
.leftJoinAndSelect('like.page', 'page');
const likes = await query
.take(ps.limit!)
.getMany();
return await PageLikes.packMany(likes, user);
});

View File

@ -1,13 +1,13 @@
import $ from 'cafy';
import define from '../../define';
import { MessagingMessage } from '../../../../models/entities/messaging-message';
import { MessagingMessages, Mutings } from '../../../../models';
import { MessagingMessages, Mutings, UserGroupJoinings } from '../../../../models';
import { Brackets } from 'typeorm';
import { types, bool } from '../../../../misc/schema';
export const meta = {
desc: {
'ja-JP': 'Messagingの履歴を取得します。',
'ja-JP': 'トークの履歴を取得します。',
'en-US': 'Show messaging history.'
},
@ -21,6 +21,11 @@ export const meta = {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
group: {
validator: $.optional.bool,
default: false
}
},
@ -40,26 +45,46 @@ export default define(meta, async (ps, user) => {
muterId: user.id,
});
const groups = ps.group ? await UserGroupJoinings.find({
userId: user.id,
}).then(xs => xs.map(x => x.userGroupId)) : [];
if (ps.group && groups.length === 0) {
return [];
}
const history: MessagingMessage[] = [];
for (let i = 0; i < ps.limit!; i++) {
const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId);
const found = ps.group
? history.map(m => m.groupId!)
: history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!);
const query = MessagingMessages.createQueryBuilder('message')
.where(new Brackets(qb => { qb
.where(`message.userId = :userId`, { userId: user.id })
.orWhere(`message.recipientId = :userId`, { userId: user.id });
}))
.orderBy('message.createdAt', 'DESC');
if (found.length > 0) {
query.andWhere(`message.userId NOT IN (:...found)`, { found: found });
query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found });
}
if (ps.group) {
query.where(`message.groupId IN (:...groups)`, { groups: groups });
if (mute.length > 0) {
query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
if (found.length > 0) {
query.andWhere(`message.groupId NOT IN (:...found)`, { found: found });
}
} else {
query.where(new Brackets(qb => { qb
.where(`message.userId = :userId`, { userId: user.id })
.orWhere(`message.recipientId = :userId`, { userId: user.id });
}));
query.andWhere(`message.groupId IS NULL`);
if (found.length > 0) {
query.andWhere(`message.userId NOT IN (:...found)`, { found: found });
query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found });
}
if (mute.length > 0) {
query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
}
}
const message = await query.getOne();

View File

@ -1,16 +1,17 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import read from '../../common/read-messaging-message';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
import { MessagingMessages } from '../../../../models';
import { MessagingMessages, UserGroups, UserGroupJoinings } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { types, bool } from '../../../../misc/schema';
import { Brackets } from 'typeorm';
import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message';
export const meta = {
desc: {
'ja-JP': '指定したユーザーとのMessagingのメッセージ一覧を取得します。',
'ja-JP': 'トークメッセージ一覧を取得します。',
'en-US': 'Get messages of messaging.'
},
@ -22,13 +23,21 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
validator: $.optional.type(ID),
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
}
},
groupId: {
validator: $.optional.type(ID),
desc: {
'ja-JP': '対象のグループのID',
'en-US': 'Target group ID'
}
},
limit: {
validator: $.optional.num.range(1, 100),
default: 10
@ -64,27 +73,85 @@ export const meta = {
code: 'NO_SUCH_USER',
id: '11795c64-40ea-4198-b06e-3c873ed9039d'
},
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f'
},
groupAccessDenied: {
message: 'You can not read messages of groups that you have not joined.',
code: 'GROUP_ACCESS_DENIED',
id: 'a053a8dd-a491-4718-8f87-50775aad9284'
},
}
};
export default define(meta, async (ps, user) => {
// Fetch recipient
const recipient = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
if (ps.userId != null) {
// Fetch recipient (user)
const recipient = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
.andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id });
const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { qb
.where(new Brackets(qb => { qb
.where('message.userId = :meId')
.andWhere('message.recipientId = :recipientId');
}))
.orWhere(new Brackets(qb => { qb
.where('message.userId = :recipientId')
.andWhere('message.recipientId = :meId');
}));
}))
.setParameter('meId', user.id)
.setParameter('recipientId', recipient.id);
const messages = await query.getMany();
const messages = await query.take(ps.limit!).getMany();
// Mark all as read
if (ps.markAsRead) {
read(user.id, recipient.id, messages.map(x => x.id));
// Mark all as read
if (ps.markAsRead) {
readUserMessagingMessage(user.id, recipient.id, messages.map(x => x.id));
}
return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
populateRecipient: false
})));
} else if (ps.groupId != null) {
// Fetch recipient (group)
const recipientGroup = await UserGroups.findOne(ps.groupId);
if (recipientGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
// check joined
const joining = await UserGroupJoinings.findOne({
userId: user.id,
userGroupId: recipientGroup.id
});
if (joining == null) {
throw new ApiError(meta.errors.groupAccessDenied);
}
const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
.andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id });
const messages = await query.take(ps.limit!).getMany();
// Mark all as read
if (ps.markAsRead) {
readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id));
}
return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
populateGroup: false
})));
} else {
throw new Error();
}
return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
populateRecipient: false
})));
});

View File

@ -1,19 +1,22 @@
import $ from 'cafy';
import { ID } from '../../../../../misc/cafy-id';
import { publishMainStream } from '../../../../../services/stream';
import { publishMainStream, publishGroupMessagingStream } from '../../../../../services/stream';
import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream';
import pushSw from '../../../../../services/push-notification';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
import { MessagingMessages, DriveFiles, Mutings } from '../../../../../models';
import { MessagingMessages, DriveFiles, Mutings, UserGroups, UserGroupJoinings } from '../../../../../models';
import { MessagingMessage } from '../../../../../models/entities/messaging-message';
import { genId } from '../../../../../misc/gen-id';
import { types, bool } from '../../../../../misc/schema';
import { User } from '../../../../../models/entities/user';
import { UserGroup } from '../../../../../models/entities/user-group';
import { Not } from 'typeorm';
export const meta = {
desc: {
'ja-JP': '指定したユーザーへMessagingのメッセージを送信します。',
'ja-JP': 'トークメッセージを送信します。',
'en-US': 'Create a message of messaging.'
},
@ -25,13 +28,21 @@ export const meta = {
params: {
userId: {
validator: $.type(ID),
validator: $.optional.type(ID),
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
}
},
groupId: {
validator: $.optional.type(ID),
desc: {
'ja-JP': '対象のグループのID',
'en-US': 'Target group ID'
}
},
text: {
validator: $.optional.str.pipe(MessagingMessages.isValidText)
},
@ -60,6 +71,18 @@ export const meta = {
id: '11795c64-40ea-4198-b06e-3c873ed9039d'
},
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537'
},
groupAccessDenied: {
message: 'You can not send messages to groups that you have not joined.',
code: 'GROUP_ACCESS_DENIED',
id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd'
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
@ -75,16 +98,38 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
// Myself
if (ps.userId === user.id) {
throw new ApiError(meta.errors.recipientIsYourself);
}
let recipientUser: User | undefined;
let recipientGroup: UserGroup | undefined;
// Fetch recipient
const recipient = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
if (ps.userId != null) {
// Myself
if (ps.userId === user.id) {
throw new ApiError(meta.errors.recipientIsYourself);
}
// Fetch recipient (user)
recipientUser = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
} else if (ps.groupId != null) {
// Fetch recipient (group)
recipientGroup = await UserGroups.findOne(ps.groupId);
if (recipientGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
// check joined
const joining = await UserGroupJoinings.findOne({
userId: user.id,
userGroupId: recipientGroup.id
});
if (joining == null) {
throw new ApiError(meta.errors.groupAccessDenied);
}
}
let file = null;
if (ps.fileId != null) {
@ -107,32 +152,49 @@ export default define(meta, async (ps, user) => {
id: genId(),
createdAt: new Date(),
fileId: file ? file.id : null,
recipientId: recipient.id,
recipientId: recipientUser ? recipientUser.id : null,
groupId: recipientGroup ? recipientGroup.id : null,
text: ps.text ? ps.text.trim() : null,
userId: user.id,
isRead: false
isRead: false,
reads: [] as any[]
} as MessagingMessage);
const messageObj = await MessagingMessages.pack(message);
// 自分のストリーム
publishMessagingStream(message.userId, message.recipientId, 'message', messageObj);
publishMessagingIndexStream(message.userId, 'message', messageObj);
publishMainStream(message.userId, 'messagingMessage', messageObj);
if (recipientUser) {
// 自分のストリーム
publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj);
publishMessagingIndexStream(message.userId, 'message', messageObj);
publishMainStream(message.userId, 'messagingMessage', messageObj);
// 相手のストリーム
publishMessagingStream(message.recipientId, message.userId, 'message', messageObj);
publishMessagingIndexStream(message.recipientId, 'message', messageObj);
publishMainStream(message.recipientId, 'messagingMessage', messageObj);
// 相手のストリーム
publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj);
publishMessagingIndexStream(recipientUser.id, 'message', messageObj);
publishMainStream(recipientUser.id, 'messagingMessage', messageObj);
} else if (recipientGroup) {
// グループのストリーム
publishGroupMessagingStream(recipientGroup.id, 'message', messageObj);
// メンバーのストリーム
const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id });
for (const joining of joinings) {
publishMessagingIndexStream(joining.userId, 'message', messageObj);
publishMainStream(joining.userId, 'messagingMessage', messageObj);
}
}
// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
setTimeout(async () => {
const freshMessage = await MessagingMessages.findOne({ id: message.id });
const freshMessage = await MessagingMessages.findOne(message.id);
if (freshMessage == null) return; // メッセージが削除されている場合もある
if (!freshMessage.isRead) {
if (recipientUser) {
if (freshMessage.isRead) return; // 既読
//#region ただしミュートされているなら発行しない
const mute = await Mutings.find({
muterId: recipient.id,
muterId: recipientUser.id,
});
const mutedUserIds = mute.map(m => m.muteeId.toString());
if (mutedUserIds.indexOf(user.id) != -1) {
@ -140,8 +202,15 @@ export default define(meta, async (ps, user) => {
}
//#endregion
publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj);
pushSw(message.recipientId, 'unreadMessagingMessage', messageObj);
publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj);
pushSw(recipientUser.id, 'unreadMessagingMessage', messageObj);
} else if (recipientGroup) {
const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id, userId: Not(user.id) });
for (const joining of joinings) {
if (freshMessage.reads.includes(joining.userId)) return; // 既読
publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj);
pushSw(joining.userId, 'unreadMessagingMessage', messageObj);
}
}
}, 2000);

View File

@ -1,7 +1,7 @@
import $ from 'cafy';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { publishMessagingStream } from '../../../../../services/stream';
import { publishMessagingStream, publishGroupMessagingStream } from '../../../../../services/stream';
import * as ms from 'ms';
import { ApiError } from '../../../error';
import { MessagingMessages } from '../../../../../models';
@ -10,7 +10,7 @@ export const meta = {
stability: 'stable',
desc: {
'ja-JP': '指定したメッセージを削除します。',
'ja-JP': '指定したトークメッセージを削除します。',
'en-US': 'Delete a message.'
},
@ -57,6 +57,10 @@ export default define(meta, async (ps, user) => {
await MessagingMessages.delete(message.id);
publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
if (message.recipientId) {
publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
} else if (message.groupId) {
publishGroupMessagingStream(message.groupId, 'deleted', message.id);
}
});

View File

@ -1,13 +1,13 @@
import $ from 'cafy';
import { ID } from '../../../../../misc/cafy-id';
import read from '../../../common/read-messaging-message';
import define from '../../../define';
import { ApiError } from '../../../error';
import { MessagingMessages } from '../../../../../models';
import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message';
export const meta = {
desc: {
'ja-JP': '指定した自分宛てのメッセージを既読にします。',
'ja-JP': '指定した自分宛てのトークメッセージを既読にします。',
'en-US': 'Mark as read a message of messaging.'
},
@ -39,12 +39,21 @@ export const meta = {
export default define(meta, async (ps, user) => {
const message = await MessagingMessages.findOne({
id: ps.messageId,
recipientId: user.id
});
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
}
read(user.id, message.userId, [message.id]);
if (message.recipientId) {
await readUserMessagingMessage(user.id, message.recipientId, [message.id]).catch(e => {
if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage);
throw e;
});
} else if (message.groupId) {
await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => {
if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage);
throw e;
});
}
});

View File

@ -153,7 +153,7 @@ export default define(meta, async (ps, me) => {
globalTimeLine: !instance.disableGlobalTimeline,
elasticsearch: config.elasticsearch ? true : false,
recaptcha: instance.enableRecaptcha,
objectStorage: config.drive && config.drive.storage === 'minio',
objectStorage: instance.useObjectStorage,
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
@ -182,6 +182,16 @@ export default define(meta, async (ps, me) => {
response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey;
response.useObjectStorage = instance.useObjectStorage;
response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
response.objectStorageBucket = instance.objectStorageBucket;
response.objectStoragePrefix = instance.objectStoragePrefix;
response.objectStorageEndpoint = instance.objectStorageEndpoint;
response.objectStorageRegion = instance.objectStorageRegion;
response.objectStoragePort = instance.objectStoragePort;
response.objectStorageAccessKey = instance.objectStorageAccessKey;
response.objectStorageSecretKey = instance.objectStorageSecretKey;
response.objectStorageUseSSL = instance.objectStorageUseSSL;
}
return response;

View File

@ -14,7 +14,7 @@ export const meta = {
'en-US': 'Favorite a note.'
},
tags: ['favorites'],
tags: ['notes', 'favorites'],
requireCredential: true,

View File

@ -13,7 +13,7 @@ export const meta = {
'en-US': 'Unfavorite a note.'
},
tags: ['favorites'],
tags: ['notes', 'favorites'],
requireCredential: true,

View File

@ -0,0 +1,79 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { Pages, PageLikes } from '../../../../models';
import { genId } from '../../../../misc/gen-id';
export const meta = {
desc: {
'ja-JP': '指定したページを「いいね」します。',
},
tags: ['pages'],
requireCredential: true,
kind: 'write:page-likes',
params: {
pageId: {
validator: $.type(ID),
desc: {
'ja-JP': '対象のページのID',
'en-US': 'Target page ID.'
}
}
},
errors: {
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
},
yourPage: {
message: 'You cannot like your page.',
code: 'YOUR_PAGE',
id: '28800466-e6db-40f2-8fae-bf9e82aa92b8'
},
alreadyLiked: {
message: 'The page has already been liked.',
code: 'ALREADY_LIKED',
id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
},
}
};
export default define(meta, async (ps, user) => {
const page = await Pages.findOne(ps.pageId);
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
if (page.userId === user.id) {
throw new ApiError(meta.errors.yourPage);
}
// if already liked
const exist = await PageLikes.findOne({
pageId: page.id,
userId: user.id
});
if (exist != null) {
throw new ApiError(meta.errors.alreadyLiked);
}
// Create like
await PageLikes.save({
id: genId(),
createdAt: new Date(),
pageId: page.id,
userId: user.id
});
Pages.increment({ id: page.id }, 'likedCount', 1);
});

View File

@ -70,5 +70,5 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.noSuchPage);
}
return await Pages.pack(page);
return await Pages.pack(page, user);
});

View File

@ -0,0 +1,62 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { Pages, PageLikes } from '../../../../models';
export const meta = {
desc: {
'ja-JP': '指定したページの「いいね」を解除します。',
},
tags: ['pages'],
requireCredential: true,
kind: 'write:page-likes',
params: {
pageId: {
validator: $.type(ID),
desc: {
'ja-JP': '対象のページのID',
'en-US': 'Target page ID.'
}
}
},
errors: {
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
id: 'a0d41e20-1993-40bd-890e-f6e560ae648e'
},
notLiked: {
message: 'You have not liked that page.',
code: 'NOT_LIKED',
id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee'
},
}
};
export default define(meta, async (ps, user) => {
const page = await Pages.findOne(ps.pageId);
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
const exist = await PageLikes.findOne({
pageId: page.id,
userId: user.id
});
if (exist == null) {
throw new ApiError(meta.errors.notLiked);
}
// Delete like
await PageLikes.delete(exist.id);
Pages.decrement({ id: page.id }, 'likedCount', 1);
});

View File

@ -0,0 +1,51 @@
import $ from 'cafy';
import define from '../../../define';
import { UserGroups, UserGroupJoinings } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id';
import { UserGroup } from '../../../../../models/entities/user-group';
import { types, bool } from '../../../../../misc/schema';
import { UserGroupJoining } from '../../../../../models/entities/user-group-joining';
export const meta = {
desc: {
'ja-JP': 'ユーザーグループを作成します。',
'en-US': 'Create a user group.'
},
tags: ['groups'],
requireCredential: true,
kind: 'write:user-groups',
params: {
name: {
validator: $.str.range(1, 100)
}
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
ref: 'UserGroup',
},
};
export default define(meta, async (ps, user) => {
const userGroup = await UserGroups.save({
id: genId(),
createdAt: new Date(),
userId: user.id,
name: ps.name,
} as UserGroup);
// Push the owner
await UserGroupJoinings.save({
id: genId(),
createdAt: new Date(),
userId: user.id,
userGroupId: userGroup.id
} as UserGroupJoining);
return await UserGroups.pack(userGroup);
});

View File

@ -0,0 +1,49 @@
import $ from 'cafy';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { UserGroups } from '../../../../../models';
export const meta = {
desc: {
'ja-JP': '指定したユーザーグループを削除します。',
'en-US': 'Delete a user group'
},
tags: ['groups'],
requireCredential: true,
kind: 'write:user-groups',
params: {
groupId: {
validator: $.type(ID),
desc: {
'ja-JP': '対象となるユーザーグループのID',
'en-US': 'ID of target user group'
}
}
},
errors: {
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: '63dbd64c-cd77-413f-8e08-61781e210b38'
}
}
};
export default define(meta, async (ps, user) => {
const userGroup = await UserGroups.findOne({
id: ps.groupId,
userId: user.id
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
await UserGroups.delete(userGroup.id);
});

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