Compare commits

...

44 Commits

Author SHA1 Message Date
7b44727b23 Merge branch 'develop' 2019-04-18 22:01:45 +09:00
debe648a98 11.2.0 2019-04-18 22:00:53 +09:00
fda8cf77ed Improve warp 2019-04-18 22:00:11 +09:00
8aaab195c6 Fix bug 2019-04-18 21:40:36 +09:00
4096ddcbd0 Use - 2019-04-18 21:36:44 +09:00
ae6cc11ad2 Fix icon 2019-04-18 21:34:56 +09:00
dab7e527de Improve user lists index (#4605)
* wip

* Revert "wip"

This reverts commit 6212831ce3bdae5ce17f8ace9945710ba7696185.

* improve list index

* Update user-lists.vue
2019-04-18 21:33:24 +09:00
8b92feac71 Resolve #4732 2019-04-18 21:29:19 +09:00
4beb3e5755 Fix #4734 (#4745) 2019-04-18 20:46:59 +09:00
55f63229cd Update timemachine.vue 2019-04-18 19:42:40 +09:00
7827aeb695 Resolve #4735 2019-04-18 19:40:23 +09:00
cce768aaac Fix API definition 2019-04-18 14:58:43 +09:00
80b83c0624 間違えた 2019-04-18 14:41:51 +09:00
73b683bb4d Add test 2019-04-18 14:39:49 +09:00
d78a5c0863 Fix #4703 2019-04-18 14:34:47 +09:00
683e5b6abe Add type annotations 2019-04-18 14:29:17 +09:00
653b8f6352 スプラッシュがクリックに反応するように (#4561)
* confirm silence

* Resolve #4554

* Revert "confirm silence"

This reverts commit e1dbdc2bfc0f41c2b308b142c70e9e4573c98cf9.
2019-04-18 03:37:49 +09:00
9ec6afa375 confirm on user menu (#4553) 2019-04-18 03:36:06 +09:00
adff5382ca confirm silence (#4560) 2019-04-18 03:33:51 +09:00
704aabd703 Use menu instead of prompt Fix #4540, Fix #342 (#4575)
* Use menu instead prompt

* fix

* https://bit.ly/2U0JuVt

* fix
2019-04-18 03:32:45 +09:00
f7b1ef0690 アンケートウィジットでもMFMを使用するように v11 (#4741)
* MFM in poll

* use mfm
2019-04-18 03:14:04 +09:00
929982117f Merge branch 'develop' 2019-04-18 01:12:21 +09:00
56a530d769 11.1.6 2019-04-18 01:12:03 +09:00
7ef75fb06b New Crowdin translations (#4692)
* 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 (Dutch)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* 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 (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 (Czech)

* New translations ja-JP.yml (Czech)
2019-04-18 01:11:06 +09:00
112a72abdf Docker: Back to npm from yarn (#4730)
This commit reverts "Fix Dockerfile #4214" which uses yarn instead of npm.
The cause of the build error is that binding.gyp and src/crypto_key.cc
are missing when installing dependencies.
In other words, yarn did not fix build error.

There is no reason to use yarn, so go back to npm.
2019-04-18 01:09:31 +09:00
71813e03ee Fix bug 2019-04-18 01:05:40 +09:00
9b05b6ef28 Improve readability 2019-04-18 00:57:06 +09:00
54a87b25b3 Remove unused imports 2019-04-18 00:56:10 +09:00
55e97864bd Fix: v11で未認知ユーザーからActivityが飛んできた場合に処理できない (#4733)
* Fix: inboxに未知のユーザーが来ると処理できない

* こうかな
2019-04-18 00:53:00 +09:00
95733c9490 [MFM] Better hashtag parsing 2019-04-18 00:40:56 +09:00
e19ae644f1 Fix indent 2019-04-18 00:13:31 +09:00
ac4ea25267 Better error handling 2019-04-18 00:09:08 +09:00
611e4f34dc Merge branch 'develop' 2019-04-17 19:37:32 +09:00
faf017f333 11.1.5 2019-04-17 19:36:58 +09:00
5eec896615 Better avgColor 2019-04-17 17:13:49 +09:00
17f35174ea 🎨 2019-04-17 17:01:57 +09:00
bf71b31123 Update CONTRIBUTING.md 2019-04-17 16:59:39 +09:00
9399a44c82 Fix error 2019-04-17 16:50:50 +09:00
c96418806f Disable sql log 2019-04-17 16:45:31 +09:00
7945eddef6 Clean up 2019-04-17 14:39:45 +09:00
0ede390fef Refactor 2019-04-17 14:32:59 +09:00
85959a3b9b Fix #4721 Fix #4722 2019-04-17 14:30:31 +09:00
946c3a25b9 Clean up 2019-04-17 07:25:34 +09:00
f9d697128a Update schemas.ts 2019-04-17 04:32:04 +09:00
86 changed files with 856 additions and 484 deletions

View File

@ -56,22 +56,10 @@ jobs:
executor:
type: string
default: "default"
without_redis:
type: boolean
default: false
executor: <<parameters.executor>>
steps:
- attach_workspace:
at: /tmp/workspace
- when:
condition: <<parameters.without_redis>>
steps:
- run:
name: Configure
command: |
mv .config/test.yml .config/test_redis.yml
touch .config/test.yml
cat .config/test_redis.yml | while IFS= read line; do if [[ "$line" = '# __REDIS__' ]]; then break; else echo "$line" >> .config/test.yml; fi; done
- run:
name: Test
command: |
@ -134,32 +122,14 @@ workflows:
branches:
only: master
- test:
name: manual-test-with-redis
executor: with-redis
name: manual-test
requires:
- manual-build
filters:
branches:
ignore: master
- test:
name: auto-test-without-redis
executor: with-redis
requires:
- auto-build
filters:
branches:
only: master
- test:
name: manual-test-with-redis
without_redis: true
requires:
- manual-build
filters:
branches:
ignore: master
- test:
name: auto-test-without-redis
without_redis: true
name: auto-test
requires:
- auto-build
filters:

View File

@ -5,6 +5,37 @@ If you encounter any problems with updating, please try the following:
1. `npm run clean` or `npm run cleanall`
2. Retry update (Don't forget `npm i`)
11.2.0 (2019/04/18)
-------------------
### Improvements
* 検索で日付(日時)を入力するとタイムラインをその時点まで遡るように
* APIコンソールでエンドポイントをサジェストするように
* モバイル版でドライブのメニューを使いやすく
* サイレンス時に確認を表示するように
* ユーザーメニューでブロックなどの操作を行う時に確認するように
### Fixes
* アプリケーション連携画面でパーミッションが表示されない問題を修正
* アンケートウィジットでもMFMを使用するように
* フォローしてないユーザーのホーム投稿がSTLに流れてくる問題を修正
* モバイル版でウィジェットを設定できない問題を修正
* スプラッシュがクリックに反応するように
11.1.6 (2019/04/18)
-------------------
### Fixes
* 未認知ユーザーからActivityが飛んできた場合に処理できない問題を修正
* その投稿を見たのにも関わらずメンションインジケーターが点灯し続ける問題を修正
* ハッシュタグの判定を改善
* サーバーのエラーハンドリングを改善
11.1.5 (2019/04/17)
-------------------
### Fixes
* ユーザー名に含まれているカスタム絵文字が表示されないことがある問題を修正
* 壁紙の設定ができない問題を修正
* デザインの調整
11.1.4 (2019/04/17)
-------------------
### Fixes

View File

@ -46,10 +46,40 @@ Convert な(na) to にゃ(nya)
Revert Nyaize
## Code style
### Use semicolon
To avoid ASI Hazard
### セミコロンを省略しない
ASI Hazardを避けるためでもある
### 中括弧を省略しない
Bad:
``` ts
if (foo)
bar;
else
baz;
```
Good:
``` ts
if (foo) {
bar;
} else {
baz;
}
```
ただし**`if`が一行**の時だけは省略しても良い
Good:
``` ts
if (foo) bar;
```
### `export default`を使わない
インテリセンスと相性が悪かったりするため
参考:
* https://gfx.hatenablog.com/entry/2017/11/24/135343
* https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html
### Don't use `export default`
Bad:
``` ts
export default function(foo: string): string {

View File

@ -21,12 +21,11 @@ RUN apk add --no-cache \
pkgconfig \
python \
zlib-dev
RUN npm i -g yarn
COPY package.json ./
RUN yarn install
RUN npm i
COPY . ./
RUN yarn build
RUN npm run build
FROM base AS runner

View File

@ -70,8 +70,14 @@ common:
followers: "Sledující"
favorites: "Oblíbené"
permissions:
'read:drive': "Prohlížet Disk"
'write:drive': "Pracovat s Diskem"
"read:account": "Zobrazit informace o účtu"
"read:drive": "Prohlížet Disk"
"write:drive": "Pracovat s Diskem"
"read:favorites": "Prohlížet oblíbené"
"read:messaging": "Prohlížet konverzaci"
"write:messaging": "Pracovat s konverzaci"
"read:mutes": "Prohlížet ztlumené"
"write:votes": "Hlasovat"
empty-timeline-info:
follow-users-to-make-your-timeline: "Poznámky sledujících se zobrazí ve vaší časové ose"
explore: "Najít uživatele"
@ -1114,7 +1120,7 @@ mobile/views/components/post-form.vue:
reply: "Odpovědět"
renote: "Renotovat"
reply-placeholder: "Odpovědět na tento příspěvěk"
location-alert: "Vaše zařízení nepodporuje lokační službu"
geolocation-alert: "Vaše zařízení nepodporuje lokační službu"
error: "Chyba"
username-prompt: "Zadejte uživatelské jméno"
mobile/views/components/sub-note-content.vue:
@ -1185,4 +1191,3 @@ deck/deck.user-column.vue:
activity: "Aktivita"
dev/views/new-app.vue:
app-name-desc: "Jméno vaší aplikace"
app-desc: "Stručný popis nebo představení vaší aplikace."

View File

@ -62,10 +62,14 @@ common:
followers: "Folgende"
favorites: "Diesen Beitrag favorisieren"
permissions:
'read:account': "Accountinformationen anzeigen."
'write:account': "Accountinformationen bearbeiten."
'read:drive': "Dateien anzeigen"
'write:drive': "Dateien bearbeiten"
"read:account": "Accountinformationen anzeigen."
"write:account": "Accountinformationen bearbeiten."
"read:drive": "Dateien anzeigen"
"write:drive": "Dateien bearbeiten"
"read:favorites": "Favoriten anzeigen"
"read:messaging": "Unterhaltung anzeigen"
"write:messaging": "Unterhaltung bearbeiten"
"write:votes": "Abstimmen"
empty-timeline-info:
follow-users-to-make-your-timeline: "Beiträge von Benutzern, denen du folgst, werden in der Zeitleiste angezeigt."
explore: "Benutzer finden"
@ -739,10 +743,7 @@ dev/views/new-app.vue:
create-app: "Erstelle Anwendung"
app-name: "Name der Anwendung"
app-name-desc: "Der Name der Anwendung"
app-name-ex: "z.B. Misskey für iOS"
app-overview: "Beschreibung der Anwendung"
app-desc: "Eine kurze Beschreibung oder Einführung der Anwendung."
app-desc-ex: "z.B. Ein iOS-Client für Misskey."
callback-url: "Callback-URL (optional)"
callback-url-desc: "Die URL, auf die nach erfolgreicher Authentifizierung umgeleitet werden soll."
authority: "Berechtigungen"

View File

@ -70,10 +70,21 @@ common:
followers: "Followers"
favorites: "Favorites"
permissions:
'read:account': "View account information"
'write:account': "Update your account information"
'read:drive': "Browse the Drive"
'write:drive': "Work with the Drive"
"read:account": "View account information"
"write:account": "Update your account information"
"read:blocks": "View Blocks"
"write:blocks": "Work with Blocks"
"read:drive": "Browse the Drive"
"write:drive": "Work with the Drive"
"read:favorites": "View Favorites"
"write:favorites": "Work with Favorites"
"read:following": "View Follower info"
"write:following": "Work with Follow info"
"read:messaging": "View Messaging"
"write:messaging": "Work with Messaging"
"read:mutes": "View Muted"
"write:mutes": "Work with Muted"
"write:votes": "Vote"
empty-timeline-info:
follow-users-to-make-your-timeline: "Following users will show their posts in your timeline."
explore: "Find users"
@ -281,6 +292,7 @@ common:
nav: "Navigation"
tips: "Tips"
hashtags: "Hashtags"
queue: "Queue"
dev: "Failed to create the application. Please try again."
ai-chan-kawaii: "Ai-chan kawaii!"
you: "You"
@ -1462,7 +1474,7 @@ mobile/views/components/post-form.vue:
quote-placeholder: "Quote this post... (optional)"
reply-placeholder: "Reply to this note..."
cw-placeholder: "Comments for the post (optional)"
location-alert: "Your device does not provide location services"
geolocation-alert: "Your device does not provide location services."
error: "Error"
username-prompt: "Enter user name"
mobile/views/components/sub-note-content.vue:
@ -1615,10 +1627,7 @@ dev/views/new-app.vue:
create-app: "Creating application"
app-name: "Application name"
app-name-desc: "The name of your app"
app-name-ex: "ex) Misskey for iOS"
app-overview: "Application summary"
app-desc: "A brief description or introduction of your app."
app-desc-ex: "ex) Misskey iOS client."
callback-url: "The callback URL (optional)"
callback-url-desc: "The URL to redirect to after the user is authenticated via the authentication form."
authority: "Permissions"

View File

@ -20,8 +20,15 @@ common:
application-authorization: "Autorizaciones de la aplicación."
close: "Cerrar"
do-not-copy-paste: "Por favor no copies código aquí. Tu cuenta puede resultar comprometida."
load-more: "Leer más"
enter-password: "Escribe una contraseña"
2fa: "Autenticación de dos factores"
customize-home: "Personaliza la página principal"
featured-notes: "Destacados"
dark-mode: "Modo oscuro"
signin: "Iniciar sesión"
signup: "¡Regístrate!"
signout: "Cerrar sesión"
got-it: "¡Listo!"
customization-tips:
title: "Consejos de personalización"
@ -50,8 +57,22 @@ common:
drive: "Drive"
messaging: "Conversación"
home: "Inicio"
deck: "Deck"
timeline: "Timeline"
explore: "Explorar"
following: "Siguiendo"
followers: "Seguidores"
favorites: "Me gusta esta nota"
permissions:
"read:account": "Ver información de la cuenta"
"write:account": "Editar información de la cuenta"
"read:blocks": "Ver bloques"
"write:blocks": "Editar bloques"
"read:favorites": "Ver favoritos"
"write:favorites": "Editar favoritos"
"read:messaging": "Ver conversación"
"read:notifications": "Ver notificaciones"
"write:votes": "Vota"
weekday-short:
sunday: "domingo"
monday: "lunes"
@ -97,12 +118,24 @@ common:
d: "¿Quieres decir algo?"
e: "¡Escribe aquí!"
f: "Esperando a que escribas algo..."
settings: "Configuración"
_settings:
profile: "Tu perfil"
notification: "Notificaciones"
apps: "Aplicaciones"
tags: "Etiquetas"
mute-and-block: "Silenciar/Bloquear"
blocking: "Bloquear"
security: "Seguridad"
password: "Contraseña"
other: "Otros"
appearance: "Diseño"
behavior: "Comportamiento"
fetch-on-scroll-desc: "Cuando te deslizas al final de la página nuevo contenido se carga automáticamente."
note-visibility: "Visibilidad de la publicación"
default-note-visibility: "Rango de publicación predeterminado"
web-search-engine: "Buscador web"
web-search-engine-desc: "Ejemplo: https://www.google.com/?#q={{query}}"
use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo"
line-width: "Grosor de línea"
line-width-thick: "Grosor"
@ -128,6 +161,19 @@ common:
contrasted-acct: "Añadir contraste al nombre de usuario"
wallpaper: "Fondo de pantalla"
choose-wallpaper: "Escoge un fondo de pantalla"
delete-wallpaper: "Quitar fondo de pantalla"
show-clock-on-header: "Muestra el reloj en la parte superior derecha"
timeline: "Timeline"
sound: "Sonido"
enable-sounds: "Habilitar sonido"
volume: "Volúmen"
test: "Prueba"
version: "Versión"
no-updates: "No hay actualizaciones disponibles"
no-updates-desc: "Tu Misskey está actualizado"
update-available: "¡Una nueva versión está disponible!"
update-available-desc: "Las actualizaciones se aplicarán cuando la página se vuelva a cargar."
advanced-settings: "Configuraciones avanzadas"
navbar-position-left: "Izquierda"
search: "Buscar"
delete: "eliminar"
@ -869,6 +915,7 @@ mobile/views/components/post-form.vue:
reply: "Responder"
renote: "Republicar"
reply-placeholder: "Responder a esta nota..."
geolocation-alert: "Tu dispositivo no tiene soporte de geolocalización."
mobile/views/components/sub-note-content.vue:
private: "Esta publicación es privada"
deleted: "Esta publicación ha sido removida"

View File

@ -68,10 +68,11 @@ common:
followers: "Abonné·e·s"
favorites: "Mettre cette note en favoris"
permissions:
'read:account': "Afficher les informations du compte"
'write:account': "Mettre à jour les informations de votre compte"
'read:drive': "Parcourir le Drive"
'write:drive': "Écrire sur le Drive"
"read:account": "Afficher les informations du compte"
"write:account": "Mettre à jour les informations de votre compte"
"read:drive": "Parcourir le Drive"
"write:drive": "Écrire sur le Drive"
"write:votes": "Vote"
empty-timeline-info:
follow-users-to-make-your-timeline: "Les utilisateurs suivants afficheront leurs publications sur votre fil."
explore: "Trouver des utilisateurs"
@ -274,6 +275,7 @@ common:
nav: "Navigation"
tips: "Conseils"
hashtags: "Hashtags"
queue: "File d'attente"
dev: "Échec lors de la création de lapplication. Veuillez réessayer."
ai-chan-kawaii: "Ai-Chan est mignonne !"
you: "Vous"
@ -1434,7 +1436,7 @@ mobile/views/components/post-form.vue:
quote-placeholder: "Citer ce billet ... (Facultatif)"
reply-placeholder: "Répondre à cette note"
cw-placeholder: "Commenter le contenu (optionnel)"
location-alert: "Votre appareil ne prend pas en charge les services de localisation"
geolocation-alert: "Votre appareil ne prend pas en charge les services de localisation"
error: "Erreur"
username-prompt: "Saisir un nom d'utilisateur"
mobile/views/components/sub-note-content.vue:
@ -1586,10 +1588,7 @@ dev/views/new-app.vue:
create-app: "Création dune application"
app-name: "Nom de lapplication"
app-name-desc: "Le nom de votre application"
app-name-ex: "p. ex. Misskey pour iOS"
app-overview: "Description courte de lapplication"
app-desc: "Brève description introductive à votre application."
app-desc-ex: "p. ex) Misskey pour iOS"
callback-url: "LUrl de callback (facultatif)"
callback-url-desc: "Vous pouvez définir lURL de redirection lorsque lutilisateur sest authentifié via formulaire dauthentification."
authority: "Autorisations "

View File

@ -35,6 +35,7 @@ common:
signup: "新規登録"
signout: "ログアウト"
reload-to-apply-the-setting: "この設定を反映するにはページをリロードする必要があります。今すぐリロードしますか?"
fetching-as-ap-object: "連合に照会中"
got-it: "わかった"
customization-tips:
@ -527,8 +528,12 @@ common/views/components/user-menu.vue:
mention: "メンション"
mute: "ミュート"
unmute: "ミュート解除"
mute-confirm: "このユーザーをミュートしますか?"
unmute-confirm: "このユーザーをミュート解除しますか?"
block: "ブロック"
unblock: "ブロック解除"
block-confirm: "このユーザーをブロックしますか?"
unblock-confirm: "このユーザーをブロック解除しますか?"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
report-abuse: "スパムを報告"
@ -536,8 +541,12 @@ common/views/components/user-menu.vue:
report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
silence: "サイレンス"
unsilence: "サイレンス解除"
silence-confirm: "このユーザーをサイレンスしますか?"
unsilence-confirm: "このユーザーをサイレンス解除しますか?"
suspend: "凍結"
unsuspend: "凍結解除"
suspend-confirm: "このユーザーを凍結しますか?"
unsuspend-confirm: "このユーザーを凍結解除しますか?"
common/views/components/poll.vue:
vote-to: "「{}」に投票する"
@ -739,6 +748,10 @@ common/views/components/user-list-editor.vue:
delete-are-you-sure: "リスト「$1」を削除しますか"
deleted: "削除しました"
common/views/components/user-lists.vue:
create-list: "リストを作成"
list-name: "リスト名"
common/views/widgets/broadcast.vue:
fetching: "確認中"
no-broadcasts: "お知らせはありません"
@ -1145,8 +1158,6 @@ desktop/views/components/received-follow-requests-window.vue:
desktop/views/components/user-lists-window.vue:
title: "リスト"
create-list: "リストを作成"
list-name: "リスト名"
desktop/views/components/user-preview.vue:
notes: "投稿"
@ -1336,7 +1347,9 @@ admin/views/users.vue:
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました"
make-silence: "サイレンス"
silence-confirm: "サイレンスしますか?"
unmake-silence: "サイレンスの解除"
unsilence-confirm: "サイレンスを解除しますか?"
verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました"
@ -1573,12 +1586,11 @@ mobile/views/components/drive.vue:
file-count: "ファイル"
nothing-in-drive: "ドライブには何もありません"
folder-is-empty: "このフォルダは空です"
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
folder-name-cannot-empty: "フォルダ名を空白にすることはできません。"
mobile/views/components/drive-file-chooser.vue:
select-file: "ファイルを選択"
@ -1668,9 +1680,17 @@ mobile/views/components/ui.nav.vue:
admin: "管理"
about: "Misskeyについて"
mobile/views/pages/drive.vue:
contextmenu:
upload: "ファイルをアップロード"
url-upload: "ファイルをURLでアップロード"
create-folder: "フォルダーを作成"
rename-folder: "フォルダー名を変更"
move-folder: "このフォルダを移動"
delete-folder: "このフォルダを削除"
mobile/views/pages/user-lists.vue:
title: "リスト"
enter-list-name: "リスト名を入力してください"
mobile/views/pages/signup.vue:
lets-start: "📦 始めましょう"

View File

@ -60,6 +60,8 @@ common:
following: "フォローしとる"
followers: "フォロワー"
favorites: "お気に入り"
permissions:
"write:votes": "投票するで"
weekday-short:
sunday: "日"
monday: "月"
@ -1126,7 +1128,7 @@ mobile/views/components/post-form.vue:
quote-placeholder: "この投稿を持ってくる(オプション)"
reply-placeholder: "この投稿への返信..."
cw-placeholder: "内容への注釈 (オプション)"
location-alert: "あんさんのつことる端末は位置情報に対応しとらんみたいやわ、知らんけど。"
geolocation-alert: "あんさんのつことる端末は位置情報に対応しとらんみたいやわ、知らんけど。"
error: "エラー"
username-prompt: "ユーザー名を入力してや"
mobile/views/components/sub-note-content.vue:
@ -1272,10 +1274,7 @@ dev/views/new-app.vue:
create-app: "アプリケーション作る"
app-name: "アプリケーションの名前"
app-name-desc: "あんたのアプリの名前。"
app-name-ex: "ex) 関西ミスキー保安協会"
app-overview: "このアプリどんなん?"
app-desc: "あんたのアプリどんなんか教えて"
app-desc-ex: "ex) 関西人なら誰でも口ずさめるこのCMがついにMisskeyへ。"
callback-url: "コールバックURL (無くてもええで)"
callback-url-desc: "ユーザーが認証フォームで認証した後どこに連れてくかを設定できるで"
authority: "権限"

View File

@ -70,10 +70,11 @@ common:
followers: "팔로워"
favorites: "즐겨찾기"
permissions:
'read:account': "계정 정보 보기"
'write:account': "계정 정보 변경"
'read:drive': "드라이브 보기"
'write:drive': "드라이브 수정"
"read:account": "계정 정보 보기"
"write:account": "계정 정보 변경"
"read:drive": "드라이브 보기"
"write:drive": "드라이브 수정"
"write:votes": "투표하기"
empty-timeline-info:
follow-users-to-make-your-timeline: "사용자를 팔로우하면 글이 타임라인에 표시됩니다."
explore: "사용자 탐색"
@ -281,6 +282,7 @@ common:
nav: "내비게이션"
tips: "팁"
hashtags: "해시태그"
queue: "큐"
dev: "앱을 만드는 데 실패했습니다. 다시 시도하시기 바랍니다."
ai-chan-kawaii: "아이쨩 귀여워"
you: "당신"
@ -1462,7 +1464,7 @@ mobile/views/components/post-form.vue:
quote-placeholder: "이 글을 인용... (선택적)"
reply-placeholder: "이 글에 답글..."
cw-placeholder: "내용 주석 (선택적)"
location-alert: "사용하시는 장치가 위치정보 기능에 대응하지 않습니다"
geolocation-alert: "사용하시는 장치가 위치정보 기능에 대응하지 않습니다"
error: "오류"
username-prompt: "사용자명을 입력하여 주십시오"
mobile/views/components/sub-note-content.vue:
@ -1615,10 +1617,7 @@ dev/views/new-app.vue:
create-app: "어플리케이션 생성"
app-name: "어플리케이션 이름"
app-name-desc: "앱의 이름."
app-name-ex: "ex) Misskey for iOS"
app-overview: "앱 개요"
app-desc: "어플리케이션에 대한 간단한 설명."
app-desc-ex: "ex) Misskey iOS 클라이언트."
callback-url: "콜백 URL (옵션)"
callback-url-desc: "사용자가 인증 폼에서 인증한 뒤 리다이렉트할 URL을 설정합니다."
authority: "권한"

View File

@ -23,6 +23,8 @@ common:
timeline: "Tijdlijn"
followers: "Volgers"
favorites: "Deze notitie toevoegen aan favorieten"
permissions:
"write:votes": "Stemmen"
weekday-short:
sunday: "Z"
monday: "M"

View File

@ -37,6 +37,8 @@ common:
home: "Hjem"
followers: "Følgere"
favorites: "Merket som favoritt"
permissions:
"write:votes": "Stem"
weekday-short:
sunday: "S"
monday: "M"

View File

@ -63,7 +63,8 @@ common:
followers: "Śledzący"
favorites: "Moje ulubione"
permissions:
'read:drive': "Wyświetl dysk"
"read:drive": "Wyświetl dysk"
"write:votes": "Zagłosuj"
empty-timeline-info:
explore: "Poznaj"
weekday-short:
@ -1082,7 +1083,7 @@ mobile/views/components/post-form.vue:
quote-placeholder: "Zacytuj ten wpis… (nieobowiązkowe)"
reply-placeholder: "Odpowiedź na ten wpis…"
cw-placeholder: "Treść ostrzeżenia (opcjonalnie)"
location-alert: "Twoje urządzenie nie pozwala na przekazywanie informacji o lokalizacji"
geolocation-alert: "Twoje urządzenie nie obsługuje geolokalizacji."
error: "Błąd"
username-prompt: "Wprowadź nazwę użytkownika"
mobile/views/components/sub-note-content.vue:

View File

@ -70,10 +70,26 @@ common:
followers: "关注者"
favorites: "最爱"
permissions:
'read:account': "查看账户信息"
'write:account': "更改我的帐户信息"
'read:drive': "查看网盘"
'write:drive': "管理网盘文件"
"read:account": "查看账户信息"
"write:account": "更改我的帐户信息"
"read:blocks": "查看黑名单"
"write:blocks": "编辑黑名单"
"read:drive": "查看网盘"
"write:drive": "管理网盘文件"
"read:favorites": "查看收藏夹"
"write:favorites": "编辑收藏夹"
"read:following": "查看关注信息"
"write:following": "关注/取消关注"
"read:messaging": "查看对话"
"write:messaging": "对话操作"
"read:mutes": "查看屏蔽列表"
"write:mutes": "编辑屏蔽列表"
"write:notes": "创建或删除帖子"
"read:notifications": "查看通知"
"write:notifications": "管理通知"
"read:reactions": "查看回应"
"write:reactions": "回应操作"
"write:votes": "投票"
empty-timeline-info:
follow-users-to-make-your-timeline: "关注其他用户时,帖子将显示在时间线中。"
explore: "查找用户"
@ -129,7 +145,7 @@ common:
apps: "应用程序"
tags: "标签"
mute-and-block: "屏蔽/拉黑"
blocking: "屏蔽"
blocking: "拉黑"
security: "安全性"
signin: "登录历史"
password: "密码"
@ -281,6 +297,7 @@ common:
nav: "导航"
tips: "提示"
hashtags: "标签"
queue: "队列"
dev: "构建应用程序失败,请再试一次。"
ai-chan-kawaii: "小蓝真可爱"
you: "您"
@ -966,6 +983,7 @@ common/views/components/password-settings.vue:
changed: "密码已更改"
failed: "更改密码失败"
common/views/components/post-form-attaches.vue:
attach-cancel: "删除附件"
mark-as-sensitive: "标记为“敏感”"
unmark-as-sensitive: "取消标记为“敏感”"
desktop/views/components/sub-note-content.vue:
@ -1385,6 +1403,7 @@ desktop/views/widgets/polls.vue:
desktop/views/widgets/post-form.vue:
title: "帖子"
note: "帖子"
something-happened: "由于某种原因无法发帖。"
desktop/views/widgets/profile.vue:
update-banner: "点击来剪辑背景"
update-avatar: "点击来剪辑头像"
@ -1461,7 +1480,7 @@ mobile/views/components/post-form.vue:
quote-placeholder: "引用这个帖子t... (可选)"
reply-placeholder: "回复这个帖子"
cw-placeholder: "评论帖子(可选)"
location-alert: "您的设备不提供位服务"
geolocation-alert: "您的设备不提供位服务"
error: "错误"
username-prompt: "请输入用户名"
mobile/views/components/sub-note-content.vue:
@ -1611,14 +1630,17 @@ dev/views/apps.vue:
create-app: "创建应用"
app-missing: "没有应用"
dev/views/new-app.vue:
new-app: "新应用"
new-app-info: "可以从 API 中创建应用。 (app/create)"
create-app: "正在创建应用"
app-name: "应用名称"
app-name-placeholder: "ex) iOS版Misskey"
app-name-desc: "您应用的名称"
app-name-ex: "ex) iOS版本的Misskey"
app-overview: "应用摘要"
app-desc: "您的应用的简要说明或介绍。"
app-desc-ex: "ex) iOS版Misskey客户端."
app-overview-placeholder: " ex) iOS版Misskey客户端."
app-overview-desc: "您的应用的简要说明或介绍。"
callback-url: "回应URL (optional)"
callback-url-placeholder: "ex) https://your.app.example.com/callback.php"
callback-url-desc: "通过身份验证表单对用户进行身份验证后重定向到的URL。"
authority: "权限"
authority-desc: "只能通过API访问此处请求的功能。"

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "11.1.4",
"version": "11.2.0",
"codename": "daybreak",
"repository": {
"type": "git",

View File

@ -232,6 +232,8 @@ export default Vue.extend({
},
async silenceUser() {
if (!await this.getConfirmed(this.$t('silence-confirm'))) return;
const process = async () => {
await this.$root.api('admin/silence-user', { userId: this.user.id });
this.$root.dialog({
@ -251,6 +253,8 @@ export default Vue.extend({
},
async unsilenceUser() {
if (!await this.getConfirmed(this.$t('unsilence-confirm'))) return;
const process = async () => {
await this.$root.api('admin/unsilence-user', { userId: this.user.id });
this.$root.dialog({

View File

@ -137,7 +137,6 @@ export default prop => ({
Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt);
Vue.set(this.$_ns_target, 'renote', null);
this.$_ns_target.text = null;
this.$_ns_target.tags = [];
this.$_ns_target.fileIds = [];
this.$_ns_target.poll = null;
this.$_ns_target.geo = null;

View File

@ -0,0 +1,64 @@
import { faHistory } from '@fortawesome/free-solid-svg-icons';
export async function search(v: any, q: string) {
q = q.trim();
if (q.startsWith('@')) {
v.$router.push(`/${q}`);
return;
}
if (q.startsWith('#')) {
v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
return;
}
// like 2018/03/12
if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) {
const date = new Date(q.replace(/-/g, '/'));
// 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは
// 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので
// 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の
// 結果になってしまい、2018/03/12 のコンテンツは含まれない)
if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) {
date.setHours(23, 59, 59, 999);
}
v.$root.$emit('warp', date);
v.$root.dialog({
icon: faHistory,
splash: true,
});
return;
}
if (q.startsWith('https://')) {
const dialog = v.$root.dialog({
type: 'waiting',
text: v.$t('@.fetching-as-ap-object'),
showOkButton: false,
showCancelButton: false,
cancelableByBgClick: false
});
try {
const res = await v.$root.api('ap/show', {
uri: q
});
dialog.close();
if (res.type == 'User') {
v.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
v.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {
dialog.close();
// TODO: Show error
}
return;
}
v.$router.push(`/search?q=${encodeURIComponent(q)}`);
}

View File

@ -6,7 +6,17 @@
<mk-signin/>
</template>
<template v-else>
<div class="icon" v-if="!input && !select && !user" :class="type"><fa :icon="icon"/></div>
<div class="icon" v-if="icon">
<fa :icon="icon"/>
</div>
<div class="icon" v-else-if="!input && !select && !user" :class="type">
<fa icon="check" v-if="type === 'success'"/>
<fa :icon="faTimesCircle" v-if="type === 'error'"/>
<fa icon="exclamation-triangle" v-if="type === 'warning'"/>
<fa icon="info-circle" v-if="type === 'info'"/>
<fa :icon="faQuestionCircle" v-if="type === 'question'"/>
<fa icon="spinner" pulse v-if="type === 'waiting'"/>
</div>
<header v-if="title" v-html="title"></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>
@ -14,8 +24,8 @@
<ui-select v-if="select" v-model="selectedValue" autofocus>
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</ui-select>
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
<ui-button @click="ok" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
<ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button>
</ui-horizon-group>
</template>
@ -55,10 +65,21 @@ export default Vue.extend({
user: {
required: false
},
icon: {
required: false
},
showOkButton: {
type: Boolean,
default: true
},
showCancelButton: {
type: Boolean,
default: false
},
cancelableByBgClick: {
type: Boolean,
default: true
},
splash: {
type: Boolean,
default: false
@ -69,22 +90,11 @@ export default Vue.extend({
return {
inputValue: this.input && this.input.default ? this.input.default : null,
userInputValue: null,
selectedValue: null
selectedValue: null,
faTimesCircle, faQuestionCircle
};
},
computed: {
icon(): any {
switch (this.type) {
case 'success': return 'check';
case 'error': return faTimesCircle;
case 'warning': return 'exclamation-triangle';
case 'info': return 'info-circle';
case 'question': return faQuestionCircle;
}
}
},
mounted() {
this.$nextTick(() => {
(this.$refs.bg as any).style.pointerEvents = 'auto';
@ -113,6 +123,8 @@ export default Vue.extend({
methods: {
async ok() {
if (!this.showOkButton) return;
if (this.user) {
const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
if (user) {
@ -156,7 +168,9 @@ export default Vue.extend({
},
onBgClick() {
this.cancel();
if (this.cancelableByBgClick) {
this.cancel();
}
},
onInputKeydown(e) {
@ -183,9 +197,6 @@ export default Vue.extend({
height 100%
&.splash
&, *
pointer-events none !important
> .main
min-width 0
width initial
@ -243,7 +254,7 @@ export default Vue.extend({
margin-top 8px
> .body
margin 16px 0
margin 16px 0 0 0
> .buttons
margin-top 16px

View File

@ -121,7 +121,7 @@ export default Vue.extend({
if (this.file.properties.avgColor) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: this.file.properties.avgColor.replace('255)', '0)'),
backgroundColor: 'transparent', // TODO fade
duration: 100,
easing: 'linear'
});

View File

@ -6,7 +6,7 @@
<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="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa icon="flag"/>{{ $t('no-history') }}</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>
@ -35,6 +35,7 @@ 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';
export default Vue.extend({
i18n: i18n('common/views/components/messaging-room.vue'),
@ -54,7 +55,7 @@ export default Vue.extend({
connection: null,
showIndicator: false,
timer: null,
faArrowCircleDown
faArrowCircleDown, faFlag
};
},

View File

@ -32,7 +32,7 @@ export default Vue.extend({
props: {
files: {
type: Object,
type: Array,
required: true
},
detachMediaFn: {

View File

@ -14,7 +14,7 @@
<section>
<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
<ui-input v-model="endpoint">
<ui-input v-model="endpoint" :datalist="endpoints">
<span>{{ $t('console.endpoint') }}</span>
</ui-input>
<ui-textarea v-model="body">
@ -39,15 +39,23 @@ import * as JSON5 from 'json5';
export default Vue.extend({
i18n: i18n('common/views/components/api-settings.vue'),
data() {
return {
endpoint: '',
body: '{}',
res: null,
sending: false
sending: false,
endpoints: []
};
},
created() {
this.$root.api('endpoints').then(endpoints => {
this.endpoints = endpoints;
});
},
methods: {
regenerateToken() {
this.$root.dialog({

View File

@ -525,15 +525,11 @@ export default Vue.extend({
this.$chooseDriveFile({
multiple: false
}).then(file => {
this.$root.api('i/update', {
wallpaperId: file.id
});
this.$store.dispatch('settings/set', { key: 'wallpaper', value: file.url });
});
},
deleteWallpaper() {
this.$root.api('i/update', {
wallpaperId: null
});
this.$store.dispatch('settings/set', { key: 'wallpaper', value: null });
},
checkForUpdate() {
this.checkingForUpdate = true;

View File

@ -23,6 +23,7 @@
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
:list="id"
>
<input v-else ref="input"
:type="type"
@ -37,7 +38,11 @@
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
:list="id"
>
<datalist :id="id" v-if="datalist">
<option v-for="data in datalist" :value="data"/>
</datalist>
</template>
<template v-else>
<input ref="input"
@ -130,6 +135,10 @@ export default Vue.extend({
required: false,
default: false
},
datalist: {
type: Array,
required: false,
},
inline: {
type: Boolean,
required: false,
@ -147,7 +156,8 @@ export default Vue.extend({
return {
v: this.value,
focused: false,
passwordStrength: ''
passwordStrength: '',
id: Math.random().toString()
};
},
computed: {

View File

@ -0,0 +1,95 @@
<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: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
title
});
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

@ -89,8 +89,10 @@ export default Vue.extend({
});
},
toggleMute() {
async toggleMute() {
if (this.user.isMuted) {
if (!await this.getConfirmed(this.$t('unmute-confirm'))) return;
this.$root.api('mute/delete', {
userId: this.user.id
}).then(() => {
@ -102,6 +104,8 @@ export default Vue.extend({
});
});
} else {
if (!await this.getConfirmed(this.$t('mute-confirm'))) return;
this.$root.api('mute/create', {
userId: this.user.id
}).then(() => {
@ -115,8 +119,10 @@ export default Vue.extend({
}
},
toggleBlock() {
async toggleBlock() {
if (this.user.isBlocking) {
if (!await this.getConfirmed(this.$t('unblock-confirm'))) return;
this.$root.api('blocking/delete', {
userId: this.user.id
}).then(() => {
@ -128,6 +134,8 @@ export default Vue.extend({
});
});
} else {
if (!await this.getConfirmed(this.$t('block-confirm'))) return;
this.$root.api('blocking/create', {
userId: this.user.id
}).then(() => {
@ -164,7 +172,9 @@ export default Vue.extend({
});
},
toggleSilence() {
async toggleSilence() {
if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilence-confirm' : 'silence-confirm'))) return;
this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
userId: this.user.id
}).then(() => {
@ -181,7 +191,9 @@ export default Vue.extend({
});
},
toggleSuspend() {
async toggleSuspend() {
if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspend-confirm' : 'suspend-confirm'))) return;
this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
userId: this.user.id
}).then(() => {
@ -196,7 +208,18 @@ export default Vue.extend({
text: e
});
});
}
},
async getConfirmed(text: string): Promise<Boolean> {
const confirm = await this.$root.dialog({
type: 'warning',
showCancelButton: true,
title: 'confirm',
text,
});
return !confirm.canceled;
},
}
});
</script>

View File

@ -21,7 +21,7 @@
<fa :icon="['far', 'laugh']"/>
</button>
</div>
<x-post-form-attaches class="files" :files="files" :detachMediaFn="detachMedia"/>
<x-post-form-attaches class="files" :files="files" :detach-media-fn="detachMedia"/>
<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
<mk-uploader ref="uploader" @uploaded="attachMedia"/>
<footer>

View File

@ -142,7 +142,7 @@ export default Vue.extend({
if (this.file.properties.avgColor) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: this.file.properties.avgColor.replace('255)', '0)'),
backgroundColor: 'transparent', // TODO fade
duration: 100,
easing: 'linear'
});

View File

@ -123,7 +123,7 @@ export default Vue.extend({
},
fetchMore() {
if (!this.more || this.moreFetching) return;
if (!this.more || this.moreFetching || this.notes.length === 0) return;
this.moreFetching = true;
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
this.notes = this.notes.concat(x.notes);

View File

@ -87,6 +87,5 @@ export default Vue.extend({
height 100%
flex auto
overflow auto
background var(--bg)
</style>

View File

@ -90,9 +90,8 @@
import Vue from 'vue';
import i18n from '../../../i18n';
import MkUserListsWindow from './user-lists-window.vue';
import MkUserListWindow from './user-list-window.vue';
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
import MkSettingsWindow from './settings-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';
@ -143,12 +142,7 @@ export default Vue.extend({
},
list() {
this.close();
const w = this.$root.new(MkUserListsWindow);
w.$once('choosen', list => {
this.$root.new(MkUserListWindow, {
list
});
});
this.$root.new(MkUserListsWindow);
},
followRequests() {
this.close();

View File

@ -9,6 +9,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { search } from '../../../common/scripts/search';
export default Vue.extend({
i18n: i18n('desktop/views/components/ui.header.search.vue'),
@ -22,29 +23,11 @@ export default Vue.extend({
async onSubmit() {
if (this.wait) return;
const q = this.q.trim();
if (q.startsWith('@')) {
this.$router.push(`/${q}`);
} else if (q.startsWith('#')) {
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
} else if (q.startsWith('https://')) {
this.wait = true;
try {
const res = await this.$root.api('ap/show', {
uri: q
});
if (res.type == 'User') {
this.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
this.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {
// TODO
}
this.wait = true;
search(this, this.q).finally(() => {
this.wait = false;
} else {
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
}
this.q = '';
});
}
}
});

View File

@ -148,10 +148,7 @@ export default Vue.extend({
},
list() {
const w = this.$root.new(MkUserListsWindow);
w.$once('choosen', list => {
this.$router.push(`i/lists/${ list.id }`);
});
this.$root.new(MkUserListsWindow);
},
followRequests() {

View File

@ -1,6 +1,6 @@
<template>
<div class="mk-ui" v-hotkey.global="keymap">
<div class="bg" v-if="$store.getters.isSignedIn && $store.state.i.wallpaperUrl" :style="style"></div>
<div class="bg" v-if="$store.getters.isSignedIn && $store.state.settings.wallpaper" :style="style"></div>
<x-header class="header" v-if="navbar == 'top'" v-show="!zenMode" ref="header"/>
<x-sidebar class="sidebar" v-if="navbar != 'top'" v-show="!zenMode" ref="sidebar"/>
<div class="content" :class="[{ sidebar: navbar != 'top', zen: zenMode }, navbar]">
@ -33,10 +33,9 @@ export default Vue.extend({
},
style(): any {
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
if (!this.$store.getters.isSignedIn || this.$store.state.settings.wallpaper == null) return {};
return {
backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
backgroundImage: `url(${ this.$store.state.settings.wallpaper })`
};
},
@ -96,7 +95,6 @@ export default Vue.extend({
background-size cover
background-position center
background-attachment fixed
opacity 0.3
> .content.sidebar.left
padding-left 68px

View File

@ -18,10 +18,12 @@ export default Vue.extend({
data() {
return {
connection: null,
date: null,
makePromise: cursor => this.$root.api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
untilId: cursor ? cursor : undefined,
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@ -46,6 +48,10 @@ export default Vue.extend({
},
mounted() {
this.init();
this.$root.$on('warp', this.warp);
this.$once('hook:beforeDestroy', () => {
this.$root.$off('warp', this.warp);
});
},
beforeDestroy() {
this.connection.dispose();
@ -68,6 +74,10 @@ export default Vue.extend({
},
onUserRemoved() {
(this.$refs.timeline as any).reload();
},
warp(date) {
this.date = date;
(this.$refs.timeline as any).reload();
}
}
});

View File

@ -1,85 +1,36 @@
<template>
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
<template #header><fa icon="list"/> {{ $t('title') }}</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>
<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'),
data() {
return {
fetching: true,
lists: []
};
},
mounted() {
this.$root.api('users/lists/list').then(lists => {
this.fetching = false;
this.lists = lists;
});
components: {
XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
},
methods: {
add() {
this.$root.dialog({
title: this.$t('list-name'),
input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
title
});
this.$emit('choosen', list);
});
},
choice(list) {
this.$emit('choosen', list);
},
close() {
(this as any).$refs.window.close();
},
choosen(list) {
this.$root.new(MkUserListWindow, {
list
});
}
}
});
</script>
<style lang="stylus" scoped>
.xkxvokkjlptzyewouewmceqcxhpgzprp
padding 16px
> 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
padding 16px
border solid 1px var(--faceDivider)
border-radius 4px
<style lang="stylus" module>
.content
height 100%
overflow auto
</style>

View File

@ -53,6 +53,12 @@ export default Vue.extend({
},
created() {
this.$root.$on('warp', this.warp);
this.$once('hook:beforeDestroy', () => {
this.$root.$off('warp', this.warp);
this.connection.dispose();
});
const prepend = note => {
(this.$refs.timeline as any).prepend(note);
};
@ -124,13 +130,14 @@ export default Vue.extend({
});
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
focus() {
(this.$refs.timeline as any).focus();
},
warp(date) {
this.date = date;
(this.$refs.timeline as any).reload();
}
}
});

View File

@ -36,7 +36,8 @@ export default Vue.extend({
includeReplies: this.mode == 'with-replies',
includeMyRenotes: this.mode != 'my-posts',
withFiles: this.mode == 'with-media',
untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
untilId: cursor ? cursor : undefined
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@ -62,10 +63,11 @@ export default Vue.extend({
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
this.$root.$on('warp', this.warp);
this.$once('hook:beforeDestroy', () => {
this.$root.$off('warp', this.warp);
document.removeEventListener('keydown', this.onDocumentKeydown);
});
},
methods: {

View File

@ -11,7 +11,9 @@
<div class="mkw-polls--body">
<div class="poll" v-if="!fetching && poll != null">
<p v-if="poll.text"><router-link :to="poll | notePage">{{ poll.text }}</router-link></p>
<p v-if="poll.text"><router-link :to="poll | notePage">
<mfm :text="poll.text" :author="poll.user" :custom-emojis="poll.emojis"/>
</router-link></p>
<p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p>
<mk-poll :note="poll"/>
</div>

View File

@ -14,7 +14,7 @@ export default define({
}).extend({
methods: {
chosen(date) {
this.$emit('chosen', date);
this.$root.$emit('warp', date);
},
func() {
if (this.props.design == 5) {

View File

@ -458,10 +458,14 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
},
dialog(opts) {
const vm = this.new(Dialog, opts);
return new Promise((res) => {
const p: any = new Promise((res) => {
vm.$once('ok', result => res({ canceled: false, result }));
vm.$once('cancel', () => res({ canceled: true }));
});
p.close = () => {
vm.close();
};
return p;
}
},
router,

View File

@ -379,43 +379,30 @@ export default Vue.extend({
});
},
openContextMenu() {
const fn = window.prompt(this.$t('prompt'));
if (fn == null || fn == '') return;
switch (fn) {
case '1':
this.selectLocalFile();
break;
case '2':
this.urlUpload();
break;
case '3':
this.createFolder();
break;
case '4':
this.renameFolder();
break;
case '5':
this.moveFolder();
break;
case '6':
this.deleteFolder();
break;
}
},
selectLocalFile() {
(this.$refs.file as any).click();
},
createFolder() {
const name = window.prompt(this.$t('folder-name'));
if (name == null || name == '') return;
this.$root.api('drive/folders/create', {
name: name,
parentId: this.folder ? this.folder.id : undefined
}).then(folder => {
this.addFolder(folder, true);
this.$root.dialog({
title: this.$t('folder-name')
input: {
default: this.folder.name
}
}).then(({ result: name }) => {
if (!name) {
this.$root.dialog({
type: 'error',
text: this.$t('folder-name-cannot-empty')
});
return;
}
this.$root.api('drive/folders/create', {
name: name,
parentId: this.folder ? this.folder.id : undefined
}).then(folder => {
this.addFolder(folder, true);
});
});
},
@ -427,13 +414,25 @@ export default Vue.extend({
});
return;
}
const name = window.prompt(this.$t('folder-name'), this.folder.name);
if (name == null || name == '') return;
this.$root.api('drive/folders/update', {
name: name,
folderId: this.folder.id
}).then(folder => {
this.cd(folder);
this.$root.dialog({
title: this.$t('folder-name')
input: {
default: this.folder.name
}
}).then(({ result: name }) => {
if (!name) {
this.$root.dialog({
type: 'error',
text: this.$t('cannot-empty')
});
return;
}
this.$root.api('drive/folders/update', {
name: name,
folderId: this.folder.id
}).then(folder => {
this.cd(folder);
});
});
},

View File

@ -124,7 +124,7 @@ export default Vue.extend({
},
fetchMore() {
if (!this.more || this.moreFetching) return;
if (!this.more || this.moreFetching || this.notes.length === 0) return;
this.moreFetching = true;
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
this.notes = this.notes.concat(x.notes);

View File

@ -66,6 +66,7 @@ import i18n from '../../../i18n';
import { lang } from '../../../config';
import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
import { search } from '../../../common/scripts/search';
export default Vue.extend({
i18n: i18n('mobile/views/components/ui.nav.vue'),
@ -133,29 +134,10 @@ export default Vue.extend({
}).then(async ({ canceled, result: query }) => {
if (canceled) return;
const q = query.trim();
if (q.startsWith('@')) {
this.$router.push(`/${q}`);
} else if (q.startsWith('#')) {
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
} else if (q.startsWith('https://')) {
this.searching = true;
try {
const res = await this.$root.api('ap/show', {
uri: q
});
if (res.type == 'User') {
this.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
this.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {
// TODO
}
this.searching = true;
search(this, query).finally(() => {
this.searching = false;
} else {
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
}
});
});
},

View File

@ -15,10 +15,12 @@ export default Vue.extend({
data() {
return {
connection: null,
date: null,
makePromise: cursor => this.$root.api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
untilId: cursor ? cursor : undefined,
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@ -45,6 +47,11 @@ export default Vue.extend({
mounted() {
this.init();
this.$root.$on('warp', this.warp);
this.$once('hook:beforeDestroy', () => {
this.$root.$off('warp', this.warp);
});
},
beforeDestroy() {
@ -73,6 +80,11 @@ export default Vue.extend({
onUserRemoved() {
(this.$refs.timeline as any).reload();
},
warp(date) {
this.date = date;
(this.$refs.timeline as any).reload();
}
}
});

View File

@ -17,11 +17,13 @@ export default Vue.extend({
data() {
return {
date: null,
makePromise: cursor => this.$root.api('users/notes', {
userId: this.user.id,
limit: fetchLimit + 1,
withFiles: this.withMedia,
untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
untilId: cursor ? cursor : undefined
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@ -37,6 +39,20 @@ export default Vue.extend({
}
})
};
},
created() {
this.$root.$on('warp', this.warp);
this.$once('hook:beforeDestroy', () => {
this.$root.$off('warp', this.warp);
});
},
methods: {
warp(date) {
this.date = date;
(this.$refs.timeline as any).reload();
}
}
});
</script>

View File

@ -5,7 +5,7 @@
<template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
<template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template>
</template>
<template #func><button @click="fn"><fa icon="ellipsis-h"/></button></template>
<template #func v-if="folder || (!folder && !file)"><button @click="openContextMenu" ref="contextSource"><fa icon="ellipsis-h"/></button></template>
<x-drive
ref="browser"
:init-folder="initFolder"
@ -26,9 +26,12 @@
import Vue from 'vue';
import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading';
import XMenu from '../../../common/views/components/menu.vue';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n(),
i18n: i18n('mobile/views/pages/drive.vue'),
components: {
XDrive: () => import('../components/drive.vue').then(m => m.default),
},
@ -63,9 +66,6 @@ export default Vue.extend({
(this.$refs as any).browser.goRoot(true);
}
},
fn() {
(this.$refs as any).browser.openContextMenu();
},
onMoveRoot(silent) {
const title = `${this.$root.instanceName} Drive`;
@ -104,6 +104,42 @@ export default Vue.extend({
this.file = file;
this.folder = null;
},
openContextMenu() {
this.$root.new(XMenu, {
items: [{
type: 'item',
text: this.$t('contextmenu.upload'),
icon: 'upload',
action: this.$refs.browser.selectLocalFile
}, {
type: 'item',
text: this.$t('contextmenu.url-upload'),
icon: faCloudUploadAlt,
action: this.$refs.browser.urlUpload
}, {
type: 'item',
text: this.$t('contextmenu.create-folder'),
icon: ['far', 'folder'],
action: this.$refs.browser.createFolder
}, ...(this.folder ? [{
type: 'item',
text: this.$t('contextmenu.rename-folder'),
icon: 'i-cursor',
action: this.$refs.browser.renameFolder
}, {
type: 'item',
text: this.$t('contextmenu.move-folder'),
icon: ['far', 'folder-open'],
action: this.$refs.browser.moveFolder
}, {
type: 'item',
text: this.$t('contextmenu.delete-folder'),
icon: faTrashAlt,
action: this.$refs.browser.deleteFolder
}] : [])],
source: this.$refs.contextSource,
});
}
}
});

View File

@ -54,6 +54,12 @@ export default Vue.extend({
},
created() {
this.$root.$on('warp', this.warp);
this.$once('hook:beforeDestroy', () => {
this.$root.$off('warp', this.warp);
this.connection.dispose();
});
const prepend = note => {
(this.$refs.timeline as any).prepend(note);
};
@ -125,10 +131,6 @@ export default Vue.extend({
});
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
focus() {
(this.$refs.timeline as any).focus();

View File

@ -1,20 +1,15 @@
<template>
<mk-ui>
<template #header><fa icon="list"/>{{ $t('title') }}</template>
<template #func><button @click="fn"><fa icon="plus"/></button></template>
<template #func><button @click="$refs.lists.add()"><fa icon="plus"/></button></template>
<main>
<ul>
<li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li>
</ul>
</main>
<x-lists ref="lists" @choosen="choosen"/>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
i18n: i18n('mobile/views/pages/user-lists.vue'),
@ -24,31 +19,16 @@ export default Vue.extend({
lists: []
};
},
components: {
XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
},
mounted() {
document.title = this.$t('title');
Progress.start();
this.$root.api('users/lists/list').then(lists => {
this.fetching = false;
this.lists = lists;
Progress.done();
});
},
methods: {
fn() {
this.$root.dialog({
title: this.$t('enter-list-name'),
input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
title
});
this.$router.push(`/i/lists/${list.id}`);
});
choosen(list) {
if (!list) return;
this.$router.push(`/i/lists/${list.id}`);
}
}
});

View File

@ -72,13 +72,13 @@ export default Vue.extend({
computed: {
widgets(): any[] {
return this.$store.state.settings.mobileHome;
return this.$store.state.device.mobileHome;
}
},
created() {
if (this.widgets.length == 0) {
this.widgets = [{
this.$store.commit('device/setMobileHome', [{
name: 'calendar',
id: 'a', data: {}
}, {
@ -96,8 +96,7 @@ export default Vue.extend({
}, {
name: 'version',
id: 'g', data: {}
}];
this.saveHome();
}]);
}
},
@ -123,7 +122,7 @@ export default Vue.extend({
},
addWidget() {
this.$store.commit('settings/addMobileHomeWidget', {
this.$store.commit('device/addMobileHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
data: {}
@ -131,11 +130,11 @@ export default Vue.extend({
},
removeWidget(widget) {
this.$store.commit('settings/removeMobileHomeWidget', widget);
this.$store.commit('device/removeMobileHomeWidget', widget);
},
saveHome() {
this.$store.commit('settings/setMobileHome', this.widgets);
this.$store.commit('device/setMobileHome', this.widgets);
}
}
});

View File

@ -28,6 +28,7 @@ const defaultSettings = {
iLikeSushi: false,
rememberNoteVisibility: false,
defaultNoteVisibility: 'public',
wallpaper: null,
webSearchEngine: 'https://www.google.com/?#q={{query}}',
mutedWords: [],
games: {
@ -357,7 +358,7 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('set', x);
if (ctx.rootGetters.isSignedIn) {
os.api('i/update_client_setting', {
os.api('i/update-client-setting', {
name: x.key,
value: x.value
});

View File

@ -76,8 +76,6 @@ class MyCustomLogger implements Logger {
}
export function initDb(justBorrow = false, sync = false, log = false) {
const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV || '');
try {
const conn = getConnection();
return Promise.resolve(conn);
@ -92,8 +90,8 @@ export function initDb(justBorrow = false, sync = false, log = false) {
database: config.db.db,
synchronize: process.env.NODE_ENV === 'test' || sync,
dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
logging: enableLogging,
logger: enableLogging ? new MyCustomLogger() : undefined,
logging: log,
logger: log ? new MyCustomLogger() : undefined,
entities: [
Meta,
Instance,

View File

@ -141,7 +141,7 @@ export const mfmLanguage = P.createLanguage({
},
hashtag: () => P((input, i) => {
const text = input.substr(i);
const match = text.match(/^#([^\s\.,!\?'"#:\/\[\]]+)/i);
const match = text.match(/^#([^\s\.,!\?'"#:\/\[\]【】]+)/i);
if (!match) return P.makeFailure(i, 'not a hashtag');
let hashtag = match[1];
hashtag = removeOrphanedBrackets(hashtag);

View File

@ -26,6 +26,7 @@ export class AppRepository extends Repository<App> {
id: app.id,
name: app.name,
callbackUrl: app.callbackUrl,
permission: app.permission,
...(opts.includeSecret ? { secret: app.secret } : {}),
...(me ? {
isAuthorized: await AccessTokens.count({

View File

@ -178,12 +178,12 @@ export class NoteRepository extends Repository<Note> {
name: In(reactionEmojis),
host: host
}) : [],
tags: note.tags,
fileIds: note.fileIds,
files: DriveFiles.packMany(note.fileIds),
replyId: note.replyId,
renoteId: note.renoteId,
uri: note.uri,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri || undefined,
...(opts.detail ? {
reply: note.replyId ? this.pack(note.replyId, meId, {

View File

@ -82,7 +82,7 @@ export class UserRepository extends Repository<User> {
const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
const profile = opts.detail ? await UserProfiles.findOne({ userId: user.id }).then(ensure) : null;
const profile = opts.detail ? await UserProfiles.findOne(user.id).then(ensure) : null;
const falsy = opts.detail ? false : undefined;

View File

@ -1,6 +1,5 @@
import * as Bull from 'bull';
import * as httpSignature from 'http-signature';
import parseAcct from '../../misc/acct/parse';
import { IRemoteUser } from '../../models/entities/user';
import perform from '../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
@ -12,7 +11,7 @@ import { Instances, Users, UserPublickeys } from '../../models';
import { instanceChart } from '../../services/chart';
import { UserPublickey } from '../../models/entities/user-publickey';
import fetchMeta from '../../misc/fetch-meta';
import { toPuny, toPunyNullable } from '../../misc/convert-host';
import { toPuny } from '../../misc/convert-host';
import { validActor } from '../../remote/activitypub/type';
import { ensure } from '../../prelude/ensure';
@ -35,68 +34,49 @@ export default async (job: Bull.Job): Promise<void> => {
let key: UserPublickey;
if (keyIdLower.startsWith('acct:')) {
const acct = parseAcct(keyIdLower.slice('acct:'.length));
const host = toPunyNullable(acct.host);
const username = toPuny(acct.username);
logger.warn(`Old keyId is no longer supported. ${keyIdLower}`);
return;
}
if (host === null) {
logger.warn(`request was made by local user: @${username}`);
return;
// アクティビティ内のホストの検証
const host = toPuny(new URL(signature.keyId).hostname);
try {
ValidateActivity(activity, host);
} catch (e) {
logger.warn(e.message);
return;
}
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
logger.info(`Blocked request: ${host}`);
return;
}
const _key = await UserPublickeys.findOne({
keyId: signature.keyId
});
if (_key) {
// 登録済みユーザー
user = await Users.findOne(_key.userId) as IRemoteUser;
key = _key;
} else {
// 未登録ユーザーの場合はリモート解決
user = await resolvePerson(activity.actor) as IRemoteUser;
if (user == null) {
throw new Error('failed to resolve user');
}
// アクティビティ内のホストの検証
try {
ValidateActivity(activity, host);
} catch (e) {
logger.warn(e.message);
return;
}
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
logger.info(`Blocked request: ${host}`);
return;
}
user = await Users.findOne({
usernameLower: username.toLowerCase(),
host: host
}) as IRemoteUser;
key = await UserPublickeys.findOne(user.id).then(ensure);
} else {
// アクティビティ内のホストの検証
const host = toPuny(new URL(signature.keyId).hostname);
try {
ValidateActivity(activity, host);
} catch (e) {
logger.warn(e.message);
return;
}
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
logger.info(`Blocked request: ${host}`);
return;
}
key = await UserPublickeys.findOne({
keyId: signature.keyId
}).then(ensure);
user = await Users.findOne(key.userId) as IRemoteUser;
}
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') {
if (activity.object && validActor.includes(activity.object.type)) {
if (user == null) {
logger.warn('Update activity received, but user not registed.');
} else if (!httpSignature.verifySignature(signature, key.keyPem)) {
if (!httpSignature.verifySignature(signature, key.keyPem)) {
logger.warn('Update activity received, but signature verification failed.');
} else {
updatePerson(activity.actor, null, activity.object);
@ -105,15 +85,6 @@ export default async (job: Bull.Job): Promise<void> => {
}
}
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
if (user == null) {
user = await resolvePerson(activity.actor) as IRemoteUser;
}
if (user == null) {
throw new Error('failed to resolve user');
}
if (!httpSignature.verifySignature(signature, key.keyPem)) {
logger.error('signature verification failed');
return;

View File

@ -20,21 +20,21 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
const uri = (object as any).id;
switch (object.type) {
case 'Note':
case 'Question':
case 'Article':
deleteNote(actor, uri);
break;
case 'Tombstone':
const note = await Notes.findOne({ uri });
if (note != null) {
case 'Note':
case 'Question':
case 'Article':
deleteNote(actor, uri);
}
break;
break;
default:
apLogger.warn(`Unknown type: ${object.type}`);
break;
case 'Tombstone':
const note = await Notes.findOne({ uri });
if (note != null) {
deleteNote(actor, uri);
}
break;
default:
apLogger.warn(`Unknown type: ${object.type}`);
break;
}
};

View File

@ -2,6 +2,7 @@ import { IRemoteUser } from '../../../models/entities/user';
import { ILike } from '../type';
import create from '../../../services/note/reaction/create';
import { Notes } from '../../../models';
import { apLogger } from '../logger';
export default async (actor: IRemoteUser, activity: ILike) => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@ -14,7 +15,8 @@ export default async (actor: IRemoteUser, activity: ILike) => {
const note = await Notes.findOne(noteId);
if (note == null) {
throw new Error();
apLogger.warn(`Like activity recivied, but no such note: ${id}`, { id });
return;
}
await create(actor, note, activity._misskey_reaction);

View File

@ -17,7 +17,7 @@ export async function renderPerson(user: ILocalUser) {
const [avatar, banner, profile] = await Promise.all([
user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined),
user.bannerId ? DriveFiles.findOne(user.bannerId) : Promise.resolve(undefined),
UserProfiles.findOne({ userId: user.id }).then(ensure)
UserProfiles.findOne(user.id).then(ensure)
]);
const attachment: {

View File

@ -0,0 +1,15 @@
import define from '../define';
import endpoints from '../endpoints';
export const meta = {
requireCredential: false,
tags: ['meta'],
params: {
},
};
export default define(meta, async () => {
return endpoints.map(x => x.name);
});

View File

@ -19,7 +19,7 @@ export const meta = {
export default define(meta, async (ps, user) => {
const token = ps.token.replace(/\s/g, '');
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
if (profile.twoFactorTempSecret == null) {
throw new Error('二段階認証の設定が開始されていません');

View File

@ -20,7 +20,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);

View File

@ -17,7 +17,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);

View File

@ -21,7 +21,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.currentPassword, profile.password!);

View File

@ -17,7 +17,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);

View File

@ -19,7 +19,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);

View File

@ -33,7 +33,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);

View File

@ -13,6 +13,7 @@ import { ApiError } from '../../error';
import { Users, DriveFiles, UserProfiles } from '../../../../models';
import { User } from '../../../../models/entities/user';
import { UserProfile } from '../../../../models/entities/user-profile';
import { ensure } from '../../../../prelude/ensure';
export const meta = {
desc: {
@ -157,22 +158,24 @@ export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null;
const updates = {} as Partial<User>;
const profile = {} as Partial<UserProfile>;
const profileUpdates = {} as Partial<UserProfile>;
const profile = await UserProfiles.findOne(user.id).then(ensure);
if (ps.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) profile.description = ps.description;
if (ps.description !== undefined) profileUpdates.description = ps.description;
//if (ps.lang !== undefined) updates.lang = ps.lang;
if (ps.location !== undefined) profile.location = ps.location;
if (ps.birthday !== undefined) profile.birthday = ps.birthday;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot == 'boolean') profile.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed == 'boolean') profile.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.carefulBot == 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed == 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
if (typeof ps.autoWatch == 'boolean') profile.autoWatch = ps.autoWatch;
if (typeof ps.alwaysMarkNsfw == 'boolean') profile.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (typeof ps.autoWatch == 'boolean') profileUpdates.autoWatch = ps.autoWatch;
if (typeof ps.alwaysMarkNsfw == 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (ps.avatarId) {
const avatar = await DriveFiles.findOne(ps.avatarId);
@ -201,16 +204,20 @@ export default define(meta, async (ps, user, app) => {
}
//#region emojis/tags
let emojis = [] as string[];
let tags = [] as string[];
if (updates.name != null) {
const tokens = parsePlain(updates.name);
const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
if (newName != null) {
const tokens = parsePlain(newName);
emojis = emojis.concat(extractEmojis(tokens!));
}
if (profile.description != null) {
const tokens = parse(profile.description);
if (newDescription != null) {
const tokens = parse(newDescription);
emojis = emojis.concat(extractEmojis(tokens!));
tags = extractHashtags(tokens!).map(tag => tag.toLowerCase());
}
@ -224,7 +231,7 @@ export default define(meta, async (ps, user, app) => {
//#endregion
if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
if (Object.keys(profile).length > 0) await UserProfiles.update({ userId: user.id }, profile);
if (Object.keys(profileUpdates).length > 0) await UserProfiles.update({ userId: user.id }, profileUpdates);
const iObj = await Users.pack(user.id, user, {
detail: true,

View File

@ -17,6 +17,8 @@ export const meta = {
tags: ['notes'],
requireCredential: true,
params: {
limit: {
validator: $.optional.num.range(1, 100),

View File

@ -150,7 +150,7 @@ export default define(meta, async (ps, user) => {
}
});
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// この投稿をWatchする
if (profile.autoWatch !== false) {

View File

@ -142,7 +142,7 @@ export default define(meta, async (ps, me) => {
});
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.userId = :userId', { userId: user.id })
.leftJoinAndSelect('note.user', 'user');

View File

@ -1,4 +1,3 @@
export const schemas = {
Error: {
type: 'object',

View File

@ -46,7 +46,7 @@ export default async (ctx: Koa.BaseContext) => {
return;
}
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(password, profile.password!);

View File

@ -20,11 +20,11 @@ export default class extends Channel {
@autobind
private async onNote(note: any) {
// 自分自身の投稿 または その投稿のユーザーをフォローしている または ローカルの投稿 の場合だけ
// 自分自身の投稿 または その投稿のユーザーをフォローしている または 全体公開のローカルの投稿 の場合だけ
if (!(
this.user!.id === note.userId ||
this.following.includes(note.userId) ||
note.user.host == null
(note.user.host == null && note.visibility === 'public')
)) return;
if (['followers', 'specified'].includes(note.visibility)) {

View File

@ -11,7 +11,7 @@ export default async function(user: User) {
name: user.name || user.username
};
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
const notes = await Notes.find({
where: {

View File

@ -363,16 +363,14 @@ export default async function(
logger.debug(`average color is calculated: ${r}, ${g}, ${b}`);
const value = info.isOpaque ? `rgba(${r},${g},${b},0)` : `rgba(${r},${g},${b},255)`;
properties['avgColor'] = value;
properties['avgColor'] = `rgb(${r},${g},${b})`;
} catch (e) { }
};
propPromises = [calcWh(), calcAvg()];
}
const profile = await UserProfiles.findOne({ userId: user.id });
const profile = await UserProfiles.findOne(user.id);
const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]);

View File

@ -253,7 +253,7 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
}
const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure);
const profile = await UserProfiles.findOne(user.id).then(ensure);
// If has in reply to note
if (data.reply) {

View File

@ -67,7 +67,7 @@ export default async function(user: User, note: Note, choice: number) {
}
});
const profile = await UserProfiles.findOne({ userId: user.id });
const profile = await UserProfiles.findOne(user.id);
// ローカルユーザーが投票した場合この投稿をWatchする
if (Users.isLocalUser(user) && profile!.autoWatch) {

View File

@ -80,7 +80,7 @@ export default async (user: User, note: Note, reaction?: string) => {
}
});
const profile = await UserProfiles.findOne({ userId: user.id });
const profile = await UserProfiles.findOne(user.id);
// ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする
if (Users.isLocalUser(user) && profile!.autoWatch) {

View File

@ -562,6 +562,14 @@ describe('MFM', () => {
]);
});
it('ignore 】', () => {
const tokens = parse('#foo】');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'foo' }),
text('】'),
]);
});
it('allow including number', () => {
const tokens = parse('#foo123');
assert.deepStrictEqual(tokens, [

View File

@ -32,7 +32,7 @@ describe('Streaming', () => {
p.on('message', message => {
if (message === 'ok') {
(p.channel as any).onread = () => {};
initDb(true).then(async connection => {
initDb(true).then(async (connection: any) => {
Followings = connection.getRepository(Following);
done();
});
@ -44,7 +44,7 @@ describe('Streaming', () => {
p.kill();
});
const follow = async (follower, followee) => {
const follow = async (follower: any, followee: any) => {
await Followings.save({
id: 'a',
createdAt: new Date(),
@ -484,6 +484,56 @@ describe('Streaming', () => {
});
}));
it('フォローしているユーザーのホーム投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
assert.deepStrictEqual(body.text, 'foo');
ws.close();
done();
}
});
// ホーム投稿
post(bob, {
text: 'foo',
visibility: 'home'
});
}));
it('フォローしていないローカルユーザーのホーム投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
let fired = false;
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// ホーム投稿
post(bob, {
text: 'foo',
visibility: 'home'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });

View File

@ -76,7 +76,7 @@ export const uploadFile = (user: any, path?: string): Promise<any> => new Promis
});
});
export function connectStream(user: any, channel: string, listener: any, params?: any): Promise<WebSocket> {
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
return new Promise((res, rej) => {
const ws = new WebSocket(`ws://localhost/streaming?i=${user.token}`);