Compare commits

...

48 Commits

Author SHA1 Message Date
b587fefe44 12.7.0 2020-02-10 23:45:31 +09:00
2511114c28 なんかもうめっちゃ変えた
Resolve #5846
2020-02-10 23:17:42 +09:00
9cd267fee5 🎨 2020-02-10 21:41:35 +09:00
aeac96a4f7 New Crowdin translations (#5901)
* New translations ja-JP.yml (Chinese Simplified)

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

* New translations ja-JP.yml (Chinese Simplified)
2020-02-10 21:40:30 +09:00
88a75e2a99 🎨 2020-02-10 21:37:37 +09:00
e14760775b 🎨 2020-02-10 21:30:59 +09:00
9e7e464ba5 Clean up 2020-02-10 21:28:29 +09:00
060d4fd27f 🎨 2020-02-10 21:27:17 +09:00
940578d062 🎨 2020-02-10 21:16:04 +09:00
9cf42d8b33 Better self link detection 2020-02-10 20:51:17 +09:00
1d62d2924e 🎨 2020-02-10 20:47:02 +09:00
e23bac47ba 🎨 2020-02-10 20:44:59 +09:00
b5d38adfcc 🎨 2020-02-10 20:32:57 +09:00
f30513b20b Fix #5902 2020-02-10 20:29:44 +09:00
ade1e40395 12.6.0 2020-02-10 07:25:49 +09:00
c93b8677e4 New Crowdin translations (#5899)
* 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 (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)
2020-02-10 07:25:32 +09:00
62683d8878 なんかもうめっちゃ変えた
Resolve #5900
2020-02-10 07:23:43 +09:00
3d1239c1b4 Better initial widgets 2020-02-10 05:44:47 +09:00
5268bade66 Better widgets 2020-02-10 05:42:03 +09:00
514eb39a14 ユーザーページからグループに招待できるように 2020-02-10 05:03:01 +09:00
18628b821e Better title adjust logic 2020-02-10 04:04:10 +09:00
344fbe6bcd Note page title 2020-02-10 03:55:33 +09:00
afb8cd2dc1 Update CHANGELOG.md 2020-02-10 03:51:58 +09:00
1a5f385eb5 Improve mfm link 2020-02-10 03:48:45 +09:00
8df7864064 Clean up 2020-02-10 03:16:34 +09:00
9ca60bad7f Update url-preview-popup.vue 2020-02-10 03:13:24 +09:00
bd828bb072 Better resize observe 2020-02-10 03:13:22 +09:00
892cb44d84 Resolve #3644 2020-02-10 02:59:00 +09:00
517ea6a119 Refactor 2020-02-10 02:42:06 +09:00
ba8ffda32a New Crowdin translations (#5884)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* 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 (Japanese, Kansai)

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

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

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

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

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)
2020-02-09 22:47:54 +09:00
90a9cf376e 12.5.0 2020-02-09 22:44:07 +09:00
16d6c55407 🎨 2020-02-09 22:25:36 +09:00
f3508d15a3 Refactor 2020-02-09 22:25:32 +09:00
0add490097 Update ja-JP.yml 2020-02-09 22:05:56 +09:00
2d2d1bd58d Refactor 2020-02-09 22:00:45 +09:00
7813c8a942 Fix #5896 2020-02-09 22:00:38 +09:00
ac5453232f お知らせの固定表示 (#5887)
* お知らせの固定

* ✌️

* Update index.home.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-02-09 21:31:17 +09:00
e78e5274d3 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-09 20:53:08 +09:00
982520bcef fix typo #5890 2020-02-09 20:53:00 +09:00
7abd91f031 いくつかのスタイルを調整 (#5894)
* Chrome(Android)で誕生日欄が崩れていたのを修正

* 入力欄のフォントを親要素から継承するように変更
2020-02-09 19:59:28 +09:00
b93bfb7e5c 🎨 2020-02-09 19:40:15 +09:00
2b20c34c1e Add search shortcut 2020-02-09 19:34:26 +09:00
0f63acea5b Update messaging-room.vue 2020-02-09 19:31:23 +09:00
e600fb7096 Fix #5888 2020-02-09 19:29:49 +09:00
b63fc71865 i18n 2020-02-09 19:18:06 +09:00
23e7650983 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-09 18:53:31 +09:00
01b5ccfdc6 Fix #5891 2020-02-09 18:52:53 +09:00
cc72f91465 Fix cannot update (#5890) 2020-02-09 12:47:50 +09:00
65 changed files with 1323 additions and 491 deletions

View File

@ -1,6 +1,38 @@
ChangeLog
=========
12.7.0 (2020/02/10)
--------------------
### ✨Improvements
* ノートの文字数制限の設定を復活
* デザインの調整
### 🐛Fixes
* 中国語で表示できない問題を修正
12.6.0 (2020/02/10)
--------------------
### ✨Improvements
* リンクにホバーするとURLプレビューを表示するように
* ユーザーページからグループに招待できるように
* ウィジェットはブラウザごとに記憶するように
### 🐛Fixes
* 要素の幅を判定する処理が上手くいかないことがある問題を修正
12.5.0 (2020/02/09)
--------------------
### ✨Improvements
* チュートリアルを実装
* 検索のキーボードショートカットを追加
* タイムラインを遡っている状況でないときに、誰かをフォローまたはフォロー解除したときにタイムラインをリロードするように
### 🐛Fixes
* グループチャットが開始できない問題を修正
* Renoteメニューが開けない問題を修正
* 誕生日設定が崩れていたのを修正
* キャッシュが削除できない問題を修正
12.4.1 (2020/02/09)
--------------------
### 🐛Fixes

View File

@ -214,12 +214,10 @@ remove: "Delete"
removed: "Successfully deleted"
removeAreYouSure: "Are you sure that you want to delete \"{x}\"?"
saved: "Saved"
messaging: "Talk"
messaging: "Messaging"
upload: "Upload"
fromDrive: "From Drive"
fromUrl: "From URL"
editWidgets: "Edit widgets"
exitEdit: "Finish editing"
explore: "Explore"
games: "Misskey Games"
messageRead: "Read"
@ -348,6 +346,7 @@ post: "Notes"
posted: "Posted!"
autoReloadWhenDisconnected: "Auto reload when disconnected with server"
autoNoteWatch: "Watch note automatically"
autoNoteWatchDescription: "Get notified about the notes which you reactioned or replied."
reduceUiAnimation: "Reduce animations of User Interface"
share: "Share"
notFound: "Not found"
@ -356,7 +355,7 @@ uploadFolder: "Default Upload location"
cacheClear: "Clear cache"
markAsReadAllNotifications: "Mark all notifications as read"
markAsReadAllUnreadNotes: "Mark all notes as read"
markAsReadAllTalkMessages: "Mark all conversations as read"
markAsReadAllTalkMessages: "Mark all messages as read"
help: "Help"
inputMessageHere: "Enter message here"
close: "Close"
@ -369,6 +368,36 @@ invites: "Invite"
groupName: "Group name"
members: "Members"
transfer: "Transfer"
messagingWithUser: "Messaging with other user"
messagingWithGroup: "Messaging within group"
enable: "Enable"
next: "Next"
retype: "Enter again"
noteOf: "{user}'s notes"
inviteToGroup: "Invite to group"
_tutorial:
title: "How to use Misskey"
step1_1: "Welcome!"
step1_2: "This page is called \"timeline\". It shows chronologically ordered \"notes\" of people who you \"follow\"."
step1_3: "Your timeline is currently empty, since you have not posed any notes or followed anyone yet."
step2_1: "Let's finish setting up your profile before writing a note or following anyone."
step2_2: "Providing some information about who you are will make it easier for others to follow you back."
step3_1: "Finished setting up your profile?"
step3_2: "The next step is to post a note. You can do this by pressing a pencil icon on the screen."
step3_3: "Fill in the modal and press the button on the right top to post."
step3_4: "Have nothing to say? Try \"I just started Misskey!\""
step4_1: "Finished posting your first note?"
step4_2: "Hurray! Now your first note is displayed on your timeline."
step5_1: "Now, let's try making your timeline more lively by following other people."
step5_2: "{featured} will show you trending notes in this instance. {explore} will let you find trending users. Try following people you like!"
step5_3: "To follow other users, click on their icon and press \"follow\" button on their profile."
step5_4: "If the other user has a lock icon next to their name, that user will have to manually approve your follow request."
step6_1: "Now you will be able to see other users' notes on your timeline."
step6_2: "You can also put \"reactions\" on other people's notes to quickly respond."
step6_3: "To attach a \"reaction\", press \"+\" mark on other user's note and choose an emoji you'd like to react with."
step7_1: "Congratulations! You have now finished Misskey's basic tutorial."
step7_2: "If you would like to learn more about Misskey, try the {help} section."
step7_3: "Good luck and have fun! 🚀"
_2fa:
alreadyRegistered: "You have already registered 2-factor authentication device."
registerDevice: "Register a new device"
@ -389,11 +418,11 @@ _permissions:
"write:favorites": "Edit your favorites list"
"read:following": "View your following information"
"write:following": "Follow or unfollow other accounts"
"read:messaging": "View your talks"
"write:messaging": "Start or delete your talks"
"read:messaging": "View your messages"
"write:messaging": "Compose or Delete messages"
"read:mutes": "View the list of people you muted"
"write:mutes": "Edit the list of people you muted"
"write:notes": "Compose and delete notes"
"write:notes": "Compose or Delete notes"
"read:notifications": "View notifications"
"write:notifications": "Work with notifications"
"read:reactions": "View reactions"
@ -466,6 +495,7 @@ _visibility:
followersDescription: "Post to followers only"
specified: "Direct"
specifiedDescription: "Post to specified users only"
localOnly: "Local only"
_postForm:
replyPlaceholder: "Reply to this note..."
quotePlaceholder: "Quote this note..."

View File

@ -214,18 +214,16 @@ remove: "Borrar"
removed: "Borrado"
removeAreYouSure: "¿Desea borrar \"{x}\"?"
saved: "Guardado"
messaging: "Conversación"
messaging: "Chat"
upload: "Subir"
fromDrive: "Desde el drive"
fromUrl: "Desde la URL"
editWidgets: "Editar widgets"
exitEdit: "Terminar edición"
explore: "Explorar"
games: "Misskey Games"
messageRead: "Ya leído"
recentUsedEmojis: "Emojis usados recientemente"
noMoreHistory: "El historial se ha acabado"
startMessaging: "Iniciar conversación"
startMessaging: "Iniciar chat"
nUsersRead: "Leído por {n} personas"
agreeTo: "De acuerdo con {0}"
tos: "Términos de uso"
@ -348,12 +346,39 @@ post: "Nota"
posted: "Posteado"
autoReloadWhenDisconnected: "Recargar automáticamente cuando el servidor está desconectado"
autoNoteWatch: "Ver nota automáticamente"
autoNoteWatchDescription: "Recibe notificaciones sobre las notas de otros usuarios que a los que respondiste y reaccionaste"
reduceUiAnimation: "Reducir la animación de la UI"
share: "Compartir"
notFound: "No se encuentra"
notFoundDescription: "No se encontró la página correspondiente a la URL elegida"
uploadFolder: "Carpeta de subidas por defecto"
cacheClear: "Borrar caché"
markAsReadAllNotifications: "Marcar todas las notificaciones como leídas"
markAsReadAllUnreadNotes: "Marcar todas las notas como leídas"
markAsReadAllTalkMessages: "Marcar todos los chats como leídos"
help: "Ayuda"
inputMessageHere: "Escribe el mensaje aquí"
close: "Cerrar"
group: "Grupo"
groups: "Grupos"
createGroup: "Crear grupo"
ownedGroups: "Tus"
joinedGroups: "Grupos a los que me uní"
invites: "Invitar"
groupName: "Nombre del grupo"
members: "Miembros"
transfer: "Transferir"
messagingWithUser: "Chatear con usuario"
messagingWithGroup: "Chatear en grupo"
enable: "Activar"
next: "Siguiente"
retype: "Intentar de nuevo"
_tutorial:
title: "Cómo usar Misskey"
step1_1: "Bienvenido"
step1_2: "Esta imagen se llama \"Linea de tiempo\" y muestra en orden cronológico las \"notas\" tuyas y de la gente que \"sigues\""
step1_3: "Si no estás escribiendo ninguna nota y no estás siguiendo a nadie, es esperable que no se muestre nada en la linea de tiempo"
step2_1: "Antes de crear notas y seguir a alguien, primero vamos a crear tu perfil"
_2fa:
registerDevice: "Registrar dispositivo"
step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra."
@ -371,8 +396,6 @@ _permissions:
"write:favorites": "Addministrar favoritos"
"read:following": "Ver información de seguidor"
"write:following": "Seguir o dejar de seguir"
"read:messaging": "Ver conversación"
"write:messaging": "Administrar coversación"
"read:mutes": "Ver usuarios silenciados"
"write:mutes": "Administrar usuarios silenciados"
"write:notes": "Crear/borrar notas"

View File

@ -25,7 +25,7 @@ const languages = [
'ko-KR',
//'nl-NL',
//'pl-PL',
//'zh-CN',
'zh-CN',
//'zh-TW',
];

View File

@ -219,8 +219,6 @@ messaging: "チャット"
upload: "アップロード"
fromDrive: "ドライブから"
fromUrl: "URLから"
editWidgets: "ウィジェットを編集"
exitEdit: "編集を終了"
explore: "みつける"
games: "Misskey Games"
messageRead: "既読"
@ -373,6 +371,36 @@ members: "メンバー"
transfer: "譲渡"
messagingWithUser: "ユーザーとチャット"
messagingWithGroup: "グループでチャット"
enable: "有効にする"
next: "次"
retype: "再入力"
noteOf: "{user}のノート"
inviteToGroup: "グループに招待"
maxNoteTextLength: "ノートの文字数制限"
_tutorial:
title: "Misskeyの使い方"
step1_1: "ようこそ。"
step1_2: "この画面は「タイムライン」と呼ばれ、あなたや、あなたが「フォロー」する人の「ノート」が時系列で表示されます。"
step1_3: "あなたはまだ何もノートを投稿しておらず、誰もフォローしていないので、タイムラインには何も表示されていないはずです。"
step2_1: "ノートを作成したり誰かをフォローしたりする前に、まずあなたのプロフィールを完成させましょう。"
step2_2: "あなたがどんな人かわかると、多くの人にノートを見てもらえたり、フォローしてもらいやすくなります。"
step3_1: "プロフィール設定はうまくできましたか?"
step3_2: "では試しに、何かノートを投稿してみてください。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
step3_3: "内容を書いたら、フォーム右上のボタンを押すと投稿できます。"
step3_4: "内容が思いつかない「Misskey始めました」というのはいかがでしょう。"
step4_1: "投稿できましたか?"
step4_2: "あなたのノートがタイムラインに表示されていれば成功です。"
step5_1: "次は、他の人をフォローしてタイムラインを賑やかにしたいところです。"
step5_2: "{featured}で人気のノートが見れるので、その中から気になった人を選んでフォローしたり、{explore}で人気のユーザーを探すこともできます。"
step5_3: "ユーザーをフォローするには、ユーザーのアイコンをクリックしてユーザーページを表示し、「フォロー」ボタンを押します。"
step5_4: "ユーザーによっては、フォローが承認されるまで時間がかかる場合があります。"
step6_1: "タイムラインに他のユーザーのノートが表示されていれば成功です。"
step6_2: "他の人のノートには、「リアクション」を付けることができ、簡単にあなたの反応を伝えられます。"
step6_3: "リアクションを付けるには、ノートの「+」マークをクリックして、好きなリアクションを選択します。"
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
step7_3: "では、Misskeyをお楽しみください🚀"
_2fa:
alreadyRegistered: "既に設定は完了しています。"

View File

@ -1 +1,169 @@
---
_ago:
unknown: "謎"
future: "未来"
justNow: "たった今"
secondsAgo: "{n}秒前"
minutesAgo: "{n}分前"
hoursAgo: "{n}時間前"
daysAgo: "{n}日前"
weeksAgo: "{n}週間前"
monthsAgo: "{n}ヶ月前"
yearsAgo: "{n}年前"
_time:
second: "秒"
minute: "分"
hour: "時間"
day: "日"
introMisskey: "ようこそMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作成しぃ、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素はよ反応を追加することもできます✌\n新しい世界を探検しよう🚀"
monthAndDay: "{month}月 {day}日"
search: "探す"
notifications: "通知"
username: "ユーザー名"
password: "パスワード"
fetchingAsApObject: "連合に照会中"
ok: "おっけー"
gotIt: "ほい"
cancel: "やめとくわ"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenote"
noNotes: "ノートはあらへん"
noNotifications: "通知はあらへん"
instance: "インスタンス"
settings: "設定"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介はあらへん"
login: "ログイン"
loggingIn: "ログインしとります"
logout: "ログアウト"
signup: "新規登録"
uploading: "アップロードしとります"
save: "保存"
users: "ユーザー"
addUser: "ユーザー増やす"
favorite: "お気に入り"
favorites: "お気に入り"
unfavorite: "お気に入りやめる"
pin: "ピン留め"
unpin: "ピン留めやめる"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
delete: "ほかす"
addToList: "リストに入れたる"
reply: "返す"
loadMore: "もっとあるやろ!"
mentions: "あんた宛て"
directNotes: "ダイレクト投稿"
import: "インポート"
export: "エクスポート"
files: "ファイル"
download: "ダウンロード"
lists: "リスト"
noLists: "リストはあらへん"
followsYou: "フォローされとるで"
error: "問題が発生してん"
enterListName: "リスト名を入れてや"
privacy: "プライバシーってなんや?オカンの年齢か?"
makeFollowManuallyApprove: "他人のフォローは許可してからや!"
defaultNoteVisibility: "もとからの公開範囲"
follow: "フォロー"
followRequest: "フォロー許してくれや!言うてみる"
followRequests: "フォロー許してくれや!"
followRequestPending: "フォロー許してくれるん待っとる"
enterEmoji: "絵文字を入れてや"
you: "あんた"
clickToShow: "押してみ、見せたるわ"
sensitive: "見たらあかんで"
add: "増やす"
reaction: "リアクション"
renameFile: "ファイル名をいらう"
attachCancel: "くっつけるのやめよか"
markAsSensitive: "ちょっと見せられへんわ"
unmarkAsSensitive: "別にええんじゃね?"
enterFileName: "ファイル名を入れてや"
mute: "ミュート"
unmute: "ミュートやめたる"
block: "ブロック"
unblock: "ブロックやめたる"
suspend: "凍結"
unsuspend: "溶かす"
customEmojis: "カスタム絵文字"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定をチャラにすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されへんので通信量が増加します。"
loginFailed: "ログインに失敗してん"
wallpaper: "壁紙"
removeWallpaper: "壁紙ほかす"
youHaveNoLists: "リストはあらへん"
proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
host: "ホスト"
federation: "連合"
instances: "インスタンス"
charts: "チャート"
perHour: "1時間ごと"
perDay: "1日ごと"
operations: "操作"
version: "バージョン"
network: "ネットワーク"
statistics: "統計"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。"
muteAndBlock: "ミュートとブロック"
noUsers: "ユーザーはおらへん"
pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了してん管理者アカウントを作ってや。"
noCustomEmojis: "絵文字はあらへん"
noJobs: "ジョブはあらへん"
all: "みな"
retypedNotMatch: "そやないねん。"
remove: "ほかす"
noMoreHistory: "これより過去の履歴はあらへんで"
nsfw: "見たらあかんで"
userList: "リスト"
about: "情報"
aboutMisskey: "Misskeyってなんや"
notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
close: "さいなら"
joinedGroups: "参加しとるグループ"
_2fa:
alreadyRegistered: "もう設定終わっとるわ"
_auth:
permissionAsk: "このアプリは次の権限を要求しとるで"
_antennaSources:
all: "みなのノート"
homeTimeline: "フォローしとるユーザーのノート"
_widgets:
notifications: "通知"
timeline: "タイムライン"
_cw:
show: "もっとあるやろ!"
_poll:
noMore: "これ以上追加でけへん"
deadlineTime: "時間"
_visibility:
publicDescription: "みなのユーザーに公開"
_profile:
username: "ユーザー名"
_exportOrImport:
allNotes: "全てのノート"
muteList: "ミュート"
blockingList: "ブロック"
userLists: "リスト"
_pages:
script:
categories:
list: "リスト"
blocks:
_join:
arg1: "リスト"
_randomPick:
arg1: "リスト"
_dailyRandomPick:
arg1: "リスト"
_seedRandomPick:
arg2: "リスト"
_pick:
arg1: "リスト"
_listLen:
arg1: "リスト"
types:
array: "リスト"

View File

@ -218,14 +218,12 @@ messaging: "대화"
upload: "업로드"
fromDrive: "드라이브에서"
fromUrl: "URL로부터"
editWidgets: "위젯 편집"
exitEdit: "편집 종료"
explore: "발견하기"
games: "Misskey Games"
messageRead: "읽음"
recentUsedEmojis: "최근에 사용한 이모지"
noMoreHistory: "이것보다 과거의 기록이 없습니다"
startMessaging: "대화 시작"
startMessaging: "대화 시작하기"
nUsersRead: "{n}명이 읽음"
agreeTo: "{0}에 동의"
tos: "이용 약관"
@ -348,6 +346,7 @@ post: "작성"
posted: "게시하였습니다"
autoReloadWhenDisconnected: "서버와의 연결이 끊기면 자동 새로고침"
autoNoteWatch: "노트를 자동으로 지켜보기"
autoNoteWatchDescription: "리액션하거나 답글을 남긴 다른 유저의 노트에 대한 알림을 받습니다."
reduceUiAnimation: "UI의 애니메이션을 줄이기"
share: "공유"
notFound: "찾을 수 없습니다"
@ -369,6 +368,36 @@ invites: "초대"
groupName: "그룹명"
members: "멤버"
transfer: "양도"
messagingWithUser: "유저와 대화하기"
messagingWithGroup: "그룹끼리 대화하기"
enable: "사용"
next: "다음"
retype: "다시 입력"
noteOf: "{user}의 노트"
inviteToGroup: "그룹에 초대하기"
_tutorial:
title: "Misskey의 사용 방법"
step1_1: "환영합니다!"
step1_2: "이 페이지는 \"타임라인\"이라고 불립니다. 당신이 \"팔로우\"하고 있는 사람들의 \"노트\"가 시간순으로 나타납니다."
step1_3: "아직 아무 유저도 팔로우하고 있지 않기에 타임라인은 비어 있을 것입니다."
step2_1: "새 노트를 작성하거나 다른 사람을 팔로우하기 전에, 먼저 프로필을 완성해보도록 합시다."
step2_2: "당신이 어떤 사람인지를 알린다면, 다른 사람들이 당신을 팔로우할 확률이 올라갈 것입니다."
step3_1: "프로필 설정은 잘 끝내셨나요?"
step3_2: "그럼 시험삼아 노트를 작성해 보세요. 화면에 있는 연필 버튼을 누르면 작성 폼이 열립니다."
step3_3: "내용을 작성한 후, 폼 오른쪽 상단의 버튼을 눌러 노트를 올릴 수 있습니다."
step3_4: "쓸 말이 없나요? \"Misskey 시작했어요!\" 같은 건 어떨까요? :>"
step4_1: "노트 작성을 끝내셨나요?"
step4_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다."
step5_1: "이제, 다른 사람을 팔로우하여 타임라인을 활기차게 만들어보도록 합시다."
step5_2: "{featured}에서 이 인스턴스의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요. 마음에 드는 사람을 골라 팔로우해 보세요!"
step5_3: "다른 유저를 팔로우하려면 해당 유저의 아이콘을 클릭하여 프로필 페이지를 띄운 후, 팔로우 버튼을 눌러 주세요."
step5_4: "사용자에 따라 팔로우가 승인될 때까지 시간이 걸릴 수 있습니다."
step6_1: "타임라인에 다른 사용자의 노트가 나타난다면 성공입니다."
step6_2: "다른 유저의 노트에 \"리액션\"을 붙여 간단하게 당신의 반응을 전달할 수도 있습니다."
step6_3: "리액션을 붙이려면, 노트의 \"+\" 버튼을 클릭하고 원하는 이모지를 선택합니다."
step7_1: "이것으로 Misskey의 기본 튜토리얼을 마치겠습니다. 수고하셨습니다!"
step7_2: "Misskey에 대해 더 알고 싶으시다면 {help}를 참고해 주세요."
step7_3: "그럼 Misskey를 즐기세요! 🚀"
_2fa:
alreadyRegistered: "이미 설정이 완료되었습니다."
registerDevice: "디바이스 등록"
@ -379,32 +408,32 @@ _2fa:
step4: "다음 로그인부터는 토큰을 입력해야 합니다."
securityKeyInfo: "FIDO2를 지원하는 하드웨어 시큐리티 키 혹은 휴대전화의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다."
_permissions:
"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": "투표하기"
"read:pages": "페이지 보기"
"write:pages": "페이지 수정"
"read:page-likes": "페이지의 좋아요 보기"
"write:page-likes": "페이지의 좋아요 수정"
"read:user-groups": "유저 그룹 조회"
"write:user-groups": "유저 그룹 변경"
"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": "투표를 합니다"
"read:pages": "페이지를 봅니다"
"write:pages": "페이지 수정합니다"
"read:page-likes": "페이지의 좋아요를 확인합니다"
"write:page-likes": "페이지의 좋아요를 추가하거나 삭제합니다"
"read:user-groups": "유저 그룹 조회합니다"
"write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다"
_auth:
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?"
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
@ -466,6 +495,7 @@ _visibility:
followersDescription: "팔로워에게만 공개"
specified: "다이렉트"
specifiedDescription: "지정한 유저에게만 공개"
localOnly: "로컬에만"
_postForm:
replyPlaceholder: "이 노트에 답글..."
quotePlaceholder: "이 노트를 인용..."

View File

@ -15,7 +15,7 @@ _time:
minute: "分"
hour: "小时"
day: "日"
introMisskey: "欢迎Misskey是一个开源的分散型SNS服务。\n通过「Note」来分享现在发生的事情吧!📡\n「反应」工能也可以让你快速的对大家的「Note」来表达感情👍\n一起来探索新的世界吧🚀"
introMisskey: "欢迎Misskey是一个开源的分散型SNS服务。\n通过「帖子」来分享现在发生的事情吧!📡\n「反应」功能,可以让你快速的对大家的「帖子」来表达感情👍\n一起来探索新的世界吧🚀"
monthAndDay: "{month}月 {day}日"
search: "搜索"
notifications: "通知"
@ -27,6 +27,7 @@ gotIt: "我明白了"
cancel: "取消"
enterUsername: "输入用户名"
renotedBy: "由 {user} 转推"
noNotes: "没有投稿"
noNotifications: "无通知"
instance: "实例"
settings: "设置"
@ -54,13 +55,24 @@ sendMessage: "发送"
copyUsername: "复制用户名"
reply: "回复"
loadMore: "查看更多"
youGotNewFollower: "你有新的关注者"
receiveFollowRequest: "收到关注请求"
followRequestAccepted: "同意关注请求"
mentions: "提及"
directNotes: "指定用户可见"
importAndExport: "导入和导出"
import: "导入"
export: "导出"
files: "文件"
download: "下载"
driveFileDeleteConfirm: "要删除「{name}」文件吗?附加此文件的帖子也会消失。"
unfollowConfirm: "要取消对{name}的关注吗?"
exportRequested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
importRequested: "导入请求已提交。这可能需要花一点时间。"
lists: "列表"
noLists: "列表为空"
note: "帖子"
notes: "帖子"
following: "关注中"
followers: "关注者"
followsYou: "关注了你"
@ -81,15 +93,18 @@ enterEmoji: "输入Emoji"
renote: "转发"
unrenote: "取消转发"
quote: "引用"
pinnedNote: "已置顶的帖子"
you: "您"
clickToShow: "点击以显示"
sensitive: "阅读注意"
add: "添加"
reaction: "反应"
reactionSettingDescription: "快速选择回应中的自定义表情符号,以换行符分隔。"
rememberNoteVisibility: "记录公开范围"
renameFile: "重命名文件"
attachCancel: "删除附件"
markAsSensitive: "阅读注意"
unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "请输入文件名"
mute: "屏蔽"
unmute: "解除屏蔽"
@ -107,14 +122,20 @@ emojiName: "Emoji 名称"
emojiUrl: "emoji 地址"
addEmoji: "添加Emoji"
cacheRemoteFiles: "远程文件缓存"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程实例载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这个账户是Bot"
flagAsCat: "这个账户是Cat"
autoAcceptFollowed: "自动允许关注"
addAcount: "添加账户"
loginFailed: "登录失败"
showOnRemote: "转到所在实例显示"
general: "常规设置"
wallpaper: "壁纸"
removeWallpaper: "移除壁纸"
searchWith: "搜索:{q}"
youHaveNoLists: "列表为空"
followConfirm: "你确定要关注{name}吗?"
proxyAccount: "代理账户"
host: "主机名"
selectUser: "选择用户"
recipient: "收件人"
@ -124,36 +145,51 @@ instances: "实例"
latestRequestSentAt: "上次发送的请求"
latestRequestReceivedAt: "上次收到的请求"
storageUsage: "已用存储"
charts: "图表"
perHour: "每小时"
perDay: "每天"
operations: "操作"
software: "软件"
version: "版本"
metadata: "元数据"
withNFiles: "{n}个文件"
monitor: "监视器"
jobQueue: "作业队列"
cpuAndMemory: "CPU使用量"
network: "网络"
disk: "存储"
instanceInfo: "实例情报"
statistics: "统计"
clearQueue: "清除队列"
clearQueueConfirmTitle: "确定清除队列?"
clearCachedFiles: "清除缓存"
clearCachedFilesConfirm: "确定要清除缓存文件?"
blockedInstances: "被阻拦的实例"
blockedInstancesDescription: "设定要阻拦的实例,以换行来进行分割。被阻拦的实例将无法与本实例进行交换通讯。"
muteAndBlock: "屏蔽/拉黑"
mutedUsers: "禁言用户"
blockedUsers: "已屏蔽用户"
noUsers: "无用户"
editProfile: "编辑资料"
noteDeleteConfirm: "要删除该帖子吗?"
pinLimitExceeded: "无法置顶更多了"
intro: "Misskey的部署结束啦填写管理员账号吧"
done: "完成"
processing: "处理中"
preview: "预览"
noCustomEmojis: "无自定义Emoji"
customEmojisOfRemote: "远程Emoji"
noJobs: "没有任务"
federating: "联合中"
blocked: "已拦截"
suspended: "停止推流"
all: "全部"
subscribing: "已订阅"
publishing: "直播中"
notResponding: "没有响应"
instanceFollowing: "关注实例"
instanceFollowers: "关注实例"
instanceUsers: "实例用户"
changePassword: "修改密码"
security: "安全性"
retypedNotMatch: "两次输入不一致!"
@ -163,7 +199,10 @@ newPasswordRetype: "重新输入密码:"
attachFile: "插入附件"
more: "更多!"
featured: "高亮"
usernameOrUserId: "用户名或用户ID"
noSuchUser: "用户不存在"
lookup: "查询"
announcements: "公告"
imageUrl: "图片URL"
remove: "删除"
removed: "已删除"
@ -171,17 +210,20 @@ removeAreYouSure: "要删掉「{x}」吗?"
saved: "已保存"
messaging: "聊天"
upload: "上传"
fromDrive: "从网盘中"
fromUrl: "从 URL"
editWidgets: "编辑部件"
exitEdit: "停止编辑"
explore: "发现"
games: "Misskey游戏"
messageRead: "已读"
recentUsedEmojis: "最近使用的Emoji表情"
noMoreHistory: "没有更多的历史记录"
startMessaging: "开始聊天"
nUsersRead: "{n}人已读"
agreeTo: "{0}人同意"
tos: "服务条款"
start: "开始"
home: "首页"
remoteUserCaution: "由于是远程用户,信息不完整。"
activity: "活动"
images: "图片"
birthday: "生日"
@ -226,6 +268,7 @@ connectSerice: "已连接"
disconnectSerice: "断开连接"
enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于便利性的原因,管理员和数据图表也可以继续使用。"
registration: "注册"
enableRegistration: "允许新用户注册"
invite: "邀请"
@ -238,11 +281,13 @@ iconUrl: "图标URL"
bannerUrl: "Banner URL"
basicInfo: "基本信息"
pinnedUsers: "置顶用户"
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥"
recaptchaSecretKey: "reCAPTCHA 密钥"
name: "名称"
antennaKeywordsDescription: "使用空格分隔会产生AND规范并且使用换行符分隔会产生OR规范"
serviceworker: "ServiceWorker"
enableServiceworker: "启用ServiceWorker"
caseSensitive: "区分大小写"
@ -287,7 +332,6 @@ uploadFolder: "默认上传文件夹"
cacheClear: "清空缓存"
markAsReadAllNotifications: "将所有通知标为已读"
markAsReadAllUnreadNotes: "将所有帖子标记为已读"
markAsReadAllTalkMessages: "将所有对话标为已读"
help: "帮助"
inputMessageHere: "在此键入信息"
close: "关闭"
@ -300,6 +344,14 @@ invites: "邀请"
groupName: "群组名"
members: "成员"
transfer: "转让"
enable: "启用"
next: "下一个"
retype: "重新输入"
noteOf: "{user}的帖子"
inviteToGroup: "群组邀请"
_tutorial:
title: "Misskey的使用方法"
step1_1: "欢迎!"
_2fa:
alreadyRegistered: "此设备已被注册"
registerDevice: "注册设备"
@ -315,8 +367,6 @@ _permissions:
"write:favorites": "编辑收藏夹"
"read:following": "查看关注信息"
"write:following": "关注/取消关注"
"read:messaging": "查看对话"
"write:messaging": "对话操作"
"read:mutes": "查看屏蔽列表"
"write:mutes": "编辑屏蔽列表"
"read:notifications": "查看通知"
@ -384,7 +434,12 @@ _visibility:
_profile:
name: "名称"
username: "用户名"
description: "个人简介"
metadata: "额外信息"
metadataLabel: "标签"
metadataContent: "内容"
_exportOrImport:
allNotes: "所有帖子"
followingList: "关注中"
muteList: "屏蔽"
blockingList: "屏蔽"
@ -447,9 +502,13 @@ _pages:
blocks:
text: "文本"
image: "图片"
if: "如果"
_if:
variable: "变量"
post: "投稿窗口"
_post:
text: "内容"
textInput: "文本输入"
_textInput:
name: "变量名"
text: "标题"
@ -482,12 +541,30 @@ _pages:
dialog: "显示对话框"
_dialog:
content: "内容"
resetRandom: "重置随机值"
pushEvent: "发送事件"
_pushEvent:
event: "事件名称"
message: "按下时显示的消息"
variable: "发送的变量"
no-variable: "空"
radioButton: "选择项"
_radioButton:
name: "变量名"
title: "标题"
values: "使用换行区分的选择项"
default: "默认值"
script:
categories:
flow: "控制"
logical: "逻辑运算"
operation: "计算"
comparison: "比较"
random: "随机"
value: "值"
fn: "函数"
text: "文本操作"
convert: "转换"
list: "列表"
blocks:
text: "文本"
@ -638,12 +715,14 @@ _pages:
_for:
arg1: "次数"
arg2: "处理"
thereIsEmptySlot: "槽函数{slot}为空!"
types:
string: "文字"
number: "数值"
boolean: "Flag"
array: "列表"
stringArray: "文本列表"
emptySlot: "空白槽函数"
enviromentVariables: "环境变量"
pageVariables: "页面元素"
argVariables: "输入变量"

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.4.1",
"version": "12.7.0",
"codename": "indigo",
"repository": {
"type": "git",
@ -36,6 +36,7 @@
"@fortawesome/free-regular-svg-icons": "5.12.0",
"@fortawesome/free-solid-svg-icons": "5.12.0",
"@fortawesome/vue-fontawesome": "0.1.9",
"@juggle/resize-observer": "3.0.2",
"@koa/cors": "3.0.0",
"@koa/multer": "2.0.2",
"@koa/router": "8.0.6",

View File

@ -18,8 +18,12 @@
</transition>
</div>
<div class="sub">
<fa :icon="faSearch"/>
<input type="search" class="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
<button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button>
<button v-else class="_button edit" @click="widgetsEditMode = true"><fa :icon="faGripVertical"/></button>
<div class="search">
<fa :icon="faSearch"/>
<input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
</div>
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div>
</header>
@ -86,7 +90,7 @@
</nav>
</transition>
<div class="contents">
<div class="contents" ref="contents">
<main ref="main">
<div class="content">
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
@ -126,8 +130,6 @@
<template v-else>
<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/>
</template>
<button ref="widgetsEditButton" v-if="widgetsEditMode" class="_button edit" @click="widgetsEditMode = false">{{ $t('exitEdit') }}</button>
<button ref="widgetsEditButton" v-else class="_button edit" @click="widgetsEditMode = true">{{ $t('editWidgets') }}</button>
</template>
</div>
</div>
@ -150,8 +152,9 @@
<script lang="ts">
import Vue from 'vue';
import { faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
import { ResizeObserver } from '@juggle/resize-observer';
import { v4 as uuid } from 'uuid';
import i18n from './i18n';
import { host } from './config';
@ -184,7 +187,7 @@ export default Vue.extend({
enableWidgets: window.innerWidth >= 1100,
canBack: false,
disconnectedDialog: null as Promise<void> | null,
faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer
};
},
@ -193,12 +196,13 @@ export default Vue.extend({
return {
'p': this.post,
'n': this.post,
's': this.search,
'h|/': this.help
};
},
widgets(): any[] {
return this.$store.state.settings.widgets;
return this.$store.state.deviceUser.widgets;
}
},
@ -229,9 +233,15 @@ export default Vue.extend({
this.connection.on('notification', this.onNotification);
if (this.widgets.length === 0) {
this.$store.dispatch('settings/setWidgets', [{
name: 'notifications',
this.$store.commit('deviceUser/setWidgets', [{
name: 'calendar',
id: 'a', data: {}
}, {
name: 'notifications',
id: 'b', data: {}
}, {
name: 'trends',
id: 'c', data: {}
}]);
}
}
@ -255,21 +265,34 @@ export default Vue.extend({
});
}
});
},
setInterval(() => {
if (this.showNav) return; // TODO: トランジション中も false になるので、これだけでは不十分
this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px';
}, 1000);
mounted() {
// https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width
if (this.enableWidgets) {
setInterval(() => {
if (!this.$refs.widgetsEditButton) return;
const adjustWidgetsWidth = () => {
const lastChild = this.$refs.widgets.children[this.$refs.widgets.children.length - 1];
if (lastChild == null) return;
const width = this.$refs.widgetsEditButton.offsetLeft + 300;
const width = lastChild.offsetLeft + 300;
this.$refs.widgets.style.width = width + 'px';
}, 1000);
};
setInterval(adjustWidgetsWidth, 1000);
}
const adjustTitlePosition = () => {
this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px';
};
adjustTitlePosition();
const ro = new ResizeObserver((entries, observer) => {
adjustTitlePosition();
});
ro.observe(this.$refs.contents);
window.addEventListener('resize', adjustTitlePosition);
},
methods: {
@ -481,8 +504,9 @@ export default Vue.extend({
this.$store.dispatch('switchAccount', {
...i,
token: token
}).then(() => {
location.reload();
});
location.reload();
});
},
@ -529,7 +553,7 @@ export default Vue.extend({
items: widgets.map(widget => ({
text: this.$t('_widgets.' + widget),
action: () => {
this.$store.dispatch('settings/addWidget', {
this.$store.commit('deviceUser/addWidget', {
name: widget,
id: uuid(),
data: {}
@ -541,11 +565,11 @@ export default Vue.extend({
},
removeWidget(widget) {
this.$store.dispatch('settings/removeWidget', widget);
this.$store.commit('deviceUser/removeWidget', widget);
},
saveHome() {
this.$store.dispatch('settings/setWidgets', this.widgets);
this.$store.commit('deviceUser/setWidgets', this.widgets);
}
}
});
@ -708,30 +732,42 @@ export default Vue.extend({
display: none;
}
> [data-icon] {
position: absolute;
top: 0;
left: 16px;
height: $header-height;
pointer-events: none;
font-size: 16px;
> .edit {
padding: 16px;
&.active {
color: var(--accent);
}
}
> .search {
$margin: 8px;
width: calc(100% - #{$post-button-size + $post-button-margin + $margin});
box-sizing: border-box;
margin-right: $margin;
padding: 0 12px 0 42px;
font-size: 1rem;
line-height: 38px;
border: none;
border-radius: 38px;
color: var(--fg);
background: var(--bg);
position: relative;
&:focus {
outline: none;
> input {
$margin: 8px;
width: 200px;
box-sizing: border-box;
margin-right: $margin;
padding: 0 12px 0 42px;
font-size: 1rem;
line-height: 38px;
border: none;
border-radius: 38px;
color: var(--fg);
background: var(--bg);
&:focus {
outline: none;
}
}
> [data-icon] {
position: absolute;
top: 0;
left: 16px;
height: 100%;
pointer-events: none;
font-size: 16px;
}
}
@ -802,7 +838,8 @@ export default Vue.extend({
padding: 8px 0;
> .divider {
margin: 8px 0;
margin: 8px auto;
width: calc(100% - 32px);
}
}
@ -970,12 +1007,6 @@ export default Vue.extend({
margin: 0 auto;
}
> .edit {
display: block;
font-size: 0.9em;
margin: 0 auto;
}
.customize-container {
margin: 8px 0;
background: #fff;

View File

@ -143,7 +143,7 @@ export default Vue.extend({
this.setPosition();
//#region Construct Emoji DB
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
const customEmojis = this.$store.state.instance.meta.emojis;
const emojiDefinitions: EmojiDef[] = [];
for (const x of customEmojis) {

View File

@ -140,7 +140,7 @@ export default Vue.extend({
},
created() {
let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
let local = this.$store.state.instance.meta.emojis;
local = groupByX(local, (x: any) => x.category || '');
this.customEmojis = local;
},

View File

@ -55,38 +55,35 @@ export default Vue.extend({
useOsDefaultEmojis(): boolean {
return this.$store.state.device.useOsDefaultEmojis && !this.isReaction;
},
ce() {
let ce = [];
if (this.customEmojis) ce = ce.concat(this.customEmojis);
if (this.$store.state.instance.meta && this.$store.state.instance.meta.emojis) ce = ce.concat(this.$store.state.instance.meta.emojis);
return ce;
}
},
watch: {
customEmojis() {
if (this.name) {
const customEmoji = this.customEmojis.find(x => x.name == this.name);
if (customEmoji) {
this.customEmoji = customEmoji;
this.url = this.$store.state.device.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.url)
: customEmoji.url;
ce: {
handler() {
if (this.name) {
const customEmoji = this.ce.find(x => x.name == this.name);
if (customEmoji) {
this.customEmoji = customEmoji;
this.url = this.$store.state.device.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.url)
: customEmoji.url;
}
}
}
},
immediate: true
},
},
created() {
if (this.name) {
const customEmoji = this.customEmojis.find(x => x.name == this.name);
if (customEmoji) {
this.customEmoji = customEmoji;
this.url = this.$store.state.device.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.url)
: customEmoji.url;
} else {
//const emoji = lib[this.name];
//if (emoji) {
// this.char = emoji.char;
//}
}
} else {
if (!this.name) {
this.char = this.emoji;
}

View File

@ -45,6 +45,8 @@ export default Vue.extend({
height: 150px;
margin-bottom: 16px;
border-radius: 16px;
pointer-events: none;
user-select: none;
}
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<component :is="self ? 'router-link' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
:title="url"
>
<slot></slot>
<fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/>
</component>
</template>
<script lang="ts">
import Vue from 'vue';
import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { url as local } from '../config';
import XUrlPreview from './url-preview-popup.vue';
export default Vue.extend({
props: {
url: {
type: String,
required: true,
},
rel: {
type: String,
required: false,
}
},
data() {
const self = this.url.startsWith(local);
return {
local,
self: self,
attr: self ? 'to' : 'href',
target: self ? null : '_blank',
showTimer: null,
hideTimer: null,
preview: null,
faExternalLinkSquareAlt
};
},
methods: {
showPreview() {
if (!document.body.contains(this.$el)) return;
if (this.preview) return;
this.preview = new XUrlPreview({
parent: this,
propsData: {
url: this.url,
source: this.$el
}
}).$mount();
document.body.appendChild(this.preview.$el);
},
closePreview() {
if (this.preview) {
this.preview.destroyDom();
this.preview = null;
}
},
onMouseover() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500);
},
onMouseleave() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500);
}
}
});
</script>
<style lang="scss" scoped>
.xlcxczvw {
word-break: break-all;
> .icon {
padding-left: 2px;
font-size: .9em;
font-weight: 400;
font-style: normal;
}
}
</style>

View File

@ -1,30 +1,47 @@
<template>
<div class="yxspomdl">
<fa :icon="faSpinner" pulse fixed-width class="icon"/>
<div class="ring"></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
data() {
return {
faSpinner
};
},
});
</script>
<style lang="scss" scoped>
@keyframes ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.yxspomdl {
padding: 32px;
text-align: center;
> .icon {
font-size: 32px;
opacity: 0.5;
> .ring {
display: inline-block;
width: 80px;
height: 80px;
opacity: 0.7;
}
> .ring:after {
content: " ";
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: solid 6px;
border-color: var(--fg) transparent transparent transparent;
animation: ring 0.5s linear infinite;
}
}
</style>

View File

@ -2,6 +2,7 @@ import Vue, { VNode } from 'vue';
import { MfmForest } from '../../mfm/types';
import { parse, parsePlain } from '../../mfm/parse';
import MkUrl from './url.vue';
import MkLink from './link.vue';
import MkMention from './mention.vue';
import { concat } from '../../prelude/array';
import MkFormula from './formula.vue';
@ -158,14 +159,12 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'link': {
return [createElement('a', {
attrs: {
class: 'link _link',
href: token.node.props.url,
return [createElement(MkLink, {
key: Math.random(),
props: {
url: token.node.props.url,
rel: 'nofollow noopener',
target: '_blank',
title: token.node.props.url,
}
},
}, genEl(token.children))];
}
@ -235,7 +234,6 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'emoji': {
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
return [createElement('mk-emoji', {
key: Math.random(),
attrs: {
@ -243,7 +241,7 @@ export default Vue.component('misskey-flavored-markdown', {
name: token.node.props.name
},
props: {
customEmojis: this.customEmojis || customEmojis,
customEmojis: this.customEmojis,
normal: this.plain
}
})];

View File

@ -1,5 +1,5 @@
<template>
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/>
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
</template>
<script lang="ts">

View File

@ -19,7 +19,7 @@
</router-link>
</i18n>
<div class="info">
<button class="_button time" @click="showRenoteMenu"><mk-time :time="note.createdAt"/></button>
<button class="_button time" @click="showRenoteMenu()" ref="renoteTime"><mk-time :time="note.createdAt"/></button>
<span class="visibility" v-if="note.visibility != 'public'">
<fa v-if="note.visibility == 'home'" :icon="faHome"/>
<fa v-if="note.visibility == 'followers'" :icon="faUnlock"/>
@ -558,7 +558,7 @@ export default Vue.extend({
}).then(this.focus);
},
showRenoteMenu(ev) {
showRenoteMenu(viaKeyboard = false) {
if (!this.$store.getters.isSignedIn || (this.$store.state.i.id !== this.note.userId)) return;
this.$root.menu({
items: [{
@ -571,7 +571,7 @@ export default Vue.extend({
Vue.set(this.note, 'deletedAt', new Date());
}
}],
source: ev.currentTarget || ev.target,
source: this.$refs.renoteTime,
viaKeyboard: viaKeyboard
});
},

View File

@ -1,13 +1,13 @@
<template>
<div class="mk-notes" v-size="[{ max: 500 }]">
<div class="empty _panel" v-if="empty">
<div class="empty" v-if="empty">
<img src="https://xn--931a.moe/assets/info.jpg" alt=""/>
<div>{{ $t('noNotes') }}</div>
</div>
<mk-error v-if="error" @retry="init()"/>
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note, i }">
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }">
<x-note :note="note" :detail="detail" :key="note.id"/>
</x-list>
@ -94,6 +94,8 @@ export default Vue.extend({
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
pointer-events: none;
user-select: none;
}
}

View File

@ -8,7 +8,7 @@
<header>
<button class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button>
<div>
<span class="text-count" :class="{ over: trimmedLength(text) > 500 }">{{ 500 - trimmedLength(text) }}</span>
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
<button class="_button visibility" @click="setVisibility" ref="visibilityButton">
<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span>
<span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
@ -153,7 +153,7 @@ export default Vue.extend({
this.$t('_postForm._placeholders.f')
];
const x = xs[Math.floor(Math.random() * xs.length)];
return this.renote
? this.$t('_postForm.quotePlaceholder')
: this.reply
@ -172,14 +172,18 @@ export default Vue.extend({
canPost(): boolean {
return !this.posting &&
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
(length(this.text.trim()) <= 500) &&
(length(this.text.trim()) <= this.max) &&
(!this.poll || this.pollChoices.length >= 2);
},
max(): number {
return this.$store.state.instance.meta ? this.$store.state.instance.meta.maxNoteTextLength : 1000;
}
},
watch: {
localOnly() {
this.$store.commit('device/setLocalOnly', this.localOnly);
this.$store.commit('deviceUser/setLocalOnly', this.localOnly);
}
},
@ -215,9 +219,9 @@ export default Vue.extend({
}
// デフォルト公開範囲
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.visibility : this.$store.state.settings.defaultNoteVisibility);
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : this.$store.state.settings.defaultNoteVisibility);
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.localOnly : this.$store.state.settings.defaultNoteLocalOnly;
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.localOnly : this.$store.state.settings.defaultNoteLocalOnly;
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
@ -713,7 +717,7 @@ export default Vue.extend({
border-radius: 0;
background: transparent;
color: var(--fg);
font-family: initial;
font-family: inherit;
@media (max-width: 500px) {
padding: 0 16px;

View File

@ -1,5 +1,5 @@
<template>
<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true" :no-style="noStyle"/>
<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :normal="true" :no-style="noStyle"/>
</template>
<script lang="ts">
@ -18,15 +18,5 @@ export default Vue.extend({
default: false
},
},
data() {
return {
customEmojis: []
};
},
created() {
this.$root.getMeta().then(meta => {
if (meta && meta.emojis) this.customEmojis = meta.emojis;
});
},
});
</script>

View File

@ -82,7 +82,6 @@ export default Vue.extend({
token: '',
apiUrl,
host: toUnicode(host),
meta: null,
totpLogin: false,
credential: null,
challengeData: null,
@ -91,11 +90,13 @@ export default Vue.extend({
};
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
});
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
if (this.autoSet) {
this.$once('login', res => {
localStorage.setItem('i', res.i);

View File

@ -79,7 +79,6 @@ export default Vue.extend({
usernameState: null,
passwordStrength: '',
passwordRetypeState: null,
meta: {},
submitting: false,
ToSAgreement: false,
faLock, faExclamationTriangle, faSpinner, faCheck
@ -87,6 +86,10 @@ export default Vue.extend({
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
shouldShowProfileUrl(): boolean {
return (this.username != '' &&
this.usernameState != 'invalid-format' &&
@ -95,12 +98,6 @@ export default Vue.extend({
}
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
});
},
mounted() {
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');

View File

@ -27,6 +27,7 @@ export default Vue.extend({
data() {
return {
connection: null,
connection2: null,
pagination: null,
baseQuery: {
includeMyRenotes: this.$store.state.settings.showMyRenotes,
@ -40,6 +41,7 @@ export default Vue.extend({
created() {
this.$once('hook:beforeDestroy', () => {
this.connection.dispose();
if (this.connection2) this.connection2.dispose();
});
const prepend = note => {
@ -54,6 +56,12 @@ export default Vue.extend({
(this.$refs.tl as any).reload();
};
const onChangeFollowing = () => {
if (!this.$refs.tl.backed) {
this.$refs.tl.reload();
}
};
let endpoint;
if (this.src == 'antenna') {
@ -67,13 +75,12 @@ export default Vue.extend({
this.connection.on('note', prepend);
} else if (this.src == 'home') {
endpoint = 'notes/timeline';
const onChangeFollowing = () => {
this.fetch();
};
this.connection = this.$root.stream.useSharedConnection('homeTimeline');
this.connection.on('note', prepend);
this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', onChangeFollowing);
this.connection2 = this.$root.stream.useSharedConnection('main');
this.connection2.on('follow', onChangeFollowing);
this.connection2.on('unfollow', onChangeFollowing);
} else if (this.src == 'local') {
endpoint = 'notes/local-timeline';
this.connection = this.$root.stream.useSharedConnection('localTimeline');

View File

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

View File

@ -254,7 +254,7 @@ export default Vue.extend({
> .input {
position: relative;
&:before {
content: '';
display: block;
@ -327,14 +327,16 @@ export default Vue.extend({
}
> input {
$height: 32px;
display: block;
height: $height;
width: 100%;
margin: 0;
padding: 0;
font: inherit;
font-weight: normal;
font-size: 16px;
line-height: 32px;
line-height: $height;
color: var(--inputText);
background: transparent;
border: none;

View File

@ -0,0 +1,56 @@
<template>
<div class="fgmtyycl _panel" :style="{ top: top + 'px', left: left + 'px' }">
<x-url-preview :url="url"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../i18n';
import XUrlPreview from './url-preview.vue';
export default Vue.extend({
i18n,
components: {
XUrlPreview
},
props: {
url: {
type: String,
required: true
},
source: {
required: true
}
},
data() {
return {
u: null,
top: 0,
left: 0,
};
},
mounted() {
const rect = this.source.getBoundingClientRect();
const x = ((rect.left + (this.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
const y = rect.top + this.source.offsetHeight + window.pageYOffset;
this.top = y;
this.left = x;
},
});
</script>
<style lang="scss" scoped>
.fgmtyycl {
position: absolute;
z-index: 11000;
width: 500px;
overflow: hidden;
pointer-events: none;
}
</style>

View File

@ -10,7 +10,7 @@
</div>
<div v-else class="mk-url-preview" v-size="[{ max: 400 }, { max: 350 }]">
<transition name="zoom" mode="out-in">
<component :is="hasRoute ? 'router-link' : 'a'" :class="{ compact }" :[attr]="hasRoute ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
<component :is="self ? 'router-link' : 'a'" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
<button class="_button" v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="faPlayCircle"/></button>
</div>
@ -58,12 +58,7 @@ export default Vue.extend({
},
data() {
const isSelf = this.url.startsWith(local);
const hasRoute =
(this.url.substr(local.length) === '/') ||
this.url.substr(local.length).startsWith('/@') ||
this.url.substr(local.length).startsWith('/notes/') ||
this.url.substr(local.length).startsWith('/tags/');
const self = this.url.startsWith(local);
return {
local,
fetching: true,
@ -79,10 +74,9 @@ export default Vue.extend({
},
tweetUrl: null,
playerEnabled: false,
self: isSelf,
hasRoute: hasRoute,
attr: hasRoute ? 'to' : 'href',
target: hasRoute ? null : '_blank',
self: self,
attr: self ? 'to' : 'href',
target: self ? null : '_blank',
faPlayCircle
};
},

View File

@ -1,5 +1,8 @@
<template>
<component :is="hasRoute ? 'router-link' : 'a'" class="ieqqeuvs _link" :[attr]="hasRoute ? url.substr(local.length) : url" :rel="rel" :target="target">
<component :is="self ? 'router-link' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<template v-if="!self">
<span class="schema">{{ schema }}//</span>
<span class="hostname">{{ hostname }}</span>
@ -20,28 +23,35 @@ import Vue from 'vue';
import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { toUnicode as decodePunycode } from 'punycode';
import { url as local } from '../config';
import XUrlPreview from './url-preview-popup.vue';
export default Vue.extend({
props: ['url', 'rel'],
props: {
url: {
type: String,
required: true,
},
rel: {
type: String,
required: false,
}
},
data() {
const isSelf = this.url.startsWith(local);
const hasRoute = isSelf && (
(this.url.substr(local.length) === '/') ||
this.url.substr(local.length).startsWith('/@') ||
this.url.substr(local.length).startsWith('/notes/') ||
this.url.substr(local.length).startsWith('/tags/'));
const self = this.url.startsWith(local);
return {
local,
schema: null,
hostname: null,
port: null,
pathname: null,
query: null,
hash: null,
self: isSelf,
hasRoute: hasRoute,
attr: hasRoute ? 'to' : 'href',
target: hasRoute ? null : '_blank',
schema: null as string | null,
hostname: null as string | null,
port: null as string | null,
pathname: null as string | null,
query: null as string | null,
hash: null as string | null,
self: self,
attr: self ? 'to' : 'href',
target: self ? null : '_blank',
showTimer: null,
hideTimer: null,
preview: null,
faExternalLinkSquareAlt
};
},
@ -53,6 +63,38 @@ export default Vue.extend({
this.pathname = decodeURIComponent(url.pathname);
this.query = decodeURIComponent(url.search);
this.hash = decodeURIComponent(url.hash);
},
methods: {
showPreview() {
if (!document.body.contains(this.$el)) return;
if (this.preview) return;
this.preview = new XUrlPreview({
parent: this,
propsData: {
url: this.url,
source: this.$el
}
}).$mount();
document.body.appendChild(this.preview.$el);
},
closePreview() {
if (this.preview) {
this.preview.destroyDom();
this.preview = null;
}
},
onMouseover() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500);
},
onMouseleave() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500);
}
}
});
</script>

View File

@ -4,7 +4,7 @@
<script lang="ts">
import Vue from 'vue';
import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments } from '@fortawesome/free-solid-svg-icons';
import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import i18n from '../i18n';
import XMenu from './menu.vue';
@ -43,7 +43,11 @@ export default Vue.extend({
icon: faListUl,
text: this.$t('addToList'),
action: this.pushList
}] as any;
}, this.$store.state.i.id != this.user.id ? {
icon: faUsers,
text: this.$t('inviteToGroup'),
action: this.inviteGroup
} : undefined] as any;
if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) {
menu = menu.concat([null, {
@ -118,6 +122,42 @@ export default Vue.extend({
});
},
async inviteGroup() {
const groups = await this.$root.api('users/groups/owned');
if (groups.length === 0) {
this.$root.dialog({
type: 'error',
text: this.$t('youHaveNoGroups')
});
return;
}
const { canceled, result: groupId } = await this.$root.dialog({
type: null,
title: this.$t('group'),
select: {
items: groups.map(group => ({
value: group.id, text: group.name
}))
},
showCancelButton: true
});
if (canceled) return;
this.$root.api('users/groups/invite', {
groupId: groupId,
userId: this.user.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
async toggleMute() {
this.$root.api(this.user.isMuted ? 'mute/delete' : 'mute/create', {
userId: this.user.id

View File

@ -56,14 +56,14 @@ export default Vue.extend({
},
data() {
return {
v: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.visibility : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility),
v: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility),
faGlobe, faUnlock, faEnvelope, faHome
}
},
methods: {
choose(visibility) {
if (this.$store.state.settings.rememberNoteVisibility) {
this.$store.commit('device/setVisibility', visibility);
this.$store.commit('deviceUser/setVisibility', visibility);
}
this.$emit('chosen', visibility);
this.destroyDom();

View File

@ -1,3 +1,5 @@
import { ResizeObserver } from '@juggle/resize-observer';
export default {
inserted(el, binding, vn) {
const query = binding.value;
@ -52,13 +54,16 @@ export default {
calc();
el._sizeResizeCb_ = calc;
const ro = new ResizeObserver((entries, observer) => {
calc();
});
ro.observe(el);
window.addEventListener('resize', calc);
vn.context.$on('hook:activated', calc);
el._ro_ = ro;
},
unbind(el, binding, vn) {
window.removeEventListener('resize', el._sizeResizeCb_);
el._ro_.unobserve(el);
}
};

View File

@ -157,8 +157,6 @@ os.init(async () => {
},
methods: {
api: os.api,
getMeta: os.getMeta,
getMetaSync: os.getMetaSync,
signout: os.signout,
new(vm, props) {
const x = new vm({

View File

@ -17,16 +17,6 @@ let pending = 0;
* Misskey Operating System
*/
export default class MiOS extends EventEmitter {
/**
* Misskeyの /meta で取得できるメタ情報
*/
private meta: {
data: { [x: string]: any };
chachedAt: Date;
};
private isMetaFetching = false;
public app: Vue;
public store: ReturnType<typeof initStore>;
@ -88,7 +78,7 @@ export default class MiOS extends EventEmitter {
// When failure
.catch(() => {
// Render the error screen
document.body.innerHTML = '<div id="err">Error</div>';
document.body.innerHTML = '<div id="err">Oops!</div>';
Progress.done();
});
@ -107,9 +97,9 @@ export default class MiOS extends EventEmitter {
// Finish init
callback();
// Init service worker
this.getMeta().then(data => {
if (data.swPublickey) this.registerSw(data.swPublickey);
this.store.dispatch('instance/fetch').then(() => {
// Init service worker
if (this.store.state.instance.meta.swPublickey) this.registerSw(this.store.state.instance.meta.swPublickey);
});
};
@ -350,49 +340,6 @@ export default class MiOS extends EventEmitter {
return promise;
}
/**
* Misskeyのメタ情報を取得します
*/
@autobind
public getMetaSync() {
return this.meta ? this.meta.data : null;
}
/**
* Misskeyのメタ情報を取得します
* @param force キャッシュを無視するか否か
*/
@autobind
public getMeta(force = false) {
return new Promise<{ [x: string]: any }>(async (res, rej) => {
if (this.isMetaFetching) {
this.once('_meta_fetched_', () => {
res(this.meta.data);
});
return;
}
const expire = 1000 * 60; // 1min
// forceが有効, meta情報を保持していない or 期限切れ
if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) {
this.isMetaFetching = true;
const meta = await this.api('meta', {
detail: false
});
this.meta = {
data: meta,
chachedAt: new Date()
};
this.isMetaFetching = false;
this.emit('_meta_fetched_');
res(meta);
} else {
res(this.meta.data);
}
});
}
}
/**

View File

@ -84,18 +84,19 @@ export default Vue.extend({
data() {
return {
version,
meta: null,
stats: null,
serverInfo: null,
faInfoCircle
}
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
});
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
this.$root.api('stats').then(res => {
this.stats = res;
});

View File

@ -115,13 +115,15 @@ export default Vue.extend({
tagsLocal: [],
tagsRemote: [],
stats: null,
meta: null,
num: Vue.filter('number'),
faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket
};
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
tagUsers(): any {
return {
endpoint: 'hashtags/users',
@ -159,9 +161,6 @@ export default Vue.extend({
this.$root.api('stats').then(stats => {
this.stats = stats;
});
this.$root.getMeta().then(meta => {
this.meta = meta;
});
},
});
</script>

View File

@ -0,0 +1,127 @@
<template>
<div class="_card tbkwesmv">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('_tutorial.title') }}</div>
<div class="_content" v-if="tutorial === 0">
<div>{{ $t('_tutorial.step1_1') }}</div>
<div>{{ $t('_tutorial.step1_2') }}</div>
<div>{{ $t('_tutorial.step1_3') }}</div>
</div>
<div class="_content" v-else-if="tutorial === 1">
<div>{{ $t('_tutorial.step2_1') }}</div>
<div>{{ $t('_tutorial.step2_2') }}</div>
<router-link class="_link" to="/my/settings">{{ $t('editProfile') }}</router-link>
</div>
<div class="_content" v-else-if="tutorial === 2">
<div>{{ $t('_tutorial.step3_1') }}</div>
<div>{{ $t('_tutorial.step3_2') }}</div>
<div>{{ $t('_tutorial.step3_3') }}</div>
<small>{{ $t('_tutorial.step3_4') }}</small>
</div>
<div class="_content" v-else-if="tutorial === 3">
<div>{{ $t('_tutorial.step4_1') }}</div>
<div>{{ $t('_tutorial.step4_2') }}</div>
</div>
<div class="_content" v-else-if="tutorial === 4">
<div>{{ $t('_tutorial.step5_1') }}</div>
<i18n path="_tutorial.step5_2" tag="div">
<router-link class="_link" place="featured" to="/featured">{{ $t('featured') }}</router-link>
<router-link class="_link" place="explore" to="/explore">{{ $t('explore') }}</router-link>
</i18n>
<div>{{ $t('_tutorial.step5_3') }}</div>
<small>{{ $t('_tutorial.step5_4') }}</small>
</div>
<div class="_content" v-else-if="tutorial === 5">
<div>{{ $t('_tutorial.step6_1') }}</div>
<div>{{ $t('_tutorial.step6_2') }}</div>
<div>{{ $t('_tutorial.step6_3') }}</div>
</div>
<div class="_content" v-else-if="tutorial === 6">
<div>{{ $t('_tutorial.step7_1') }}</div>
<i18n path="_tutorial.step7_2" tag="div">
<router-link class="_link" place="help" to="/docs">{{ $t('help') }}</router-link>
</i18n>
<div>{{ $t('_tutorial.step7_3') }}</div>
</div>
<div class="_footer navigation">
<div class="step">
<button class="arrow _button" @click="tutorial--" :disabled="tutorial === 0">
<fa :icon="faChevronLeft"/>
</button>
<span>{{ tutorial + 1 }} / 7</span>
<button class="arrow _button" @click="tutorial++" :disabled="tutorial === 6">
<fa :icon="faChevronRight"/>
</button>
</div>
<mk-button class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
<mk-button class="ok" @click="tutorial++" primary v-else><fa :icon="faCheck"/> {{ $t('next') }}</mk-button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faInfoCircle, faChevronLeft, faChevronRight, faCheck } from '@fortawesome/free-solid-svg-icons'
import MkButton from '../components/ui/button.vue';
export default Vue.extend({
components: {
MkButton,
},
data() {
return {
faInfoCircle, faChevronLeft, faChevronRight, faCheck
}
},
computed: {
tutorial: {
get() { return this.$store.state.settings.tutorial || 0; },
set(value) { this.$store.dispatch('settings/set', { key: 'tutorial', value }); }
},
},
});
</script>
<style lang="scss" scoped>
.tbkwesmv {
> ._content {
> small {
opacity: 0.7;
}
}
> .navigation {
display: flex;
flex-direction: row;
align-items: baseline;
> .step {
> .arrow {
padding: 4px;
&:disabled {
opacity: 0.5;
}
&:first-child {
padding-right: 8px;
}
&:last-child {
padding-left: 8px;
}
}
> span {
margin: 0 4px;
}
}
> .ok {
margin-left: auto;
}
}
}
</style>

View File

@ -13,6 +13,9 @@
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
</button>
</portal>
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" @before="before()" @after="after()"/>
</div>
</template>
@ -23,6 +26,7 @@ import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatell
import { faComments } from '@fortawesome/free-regular-svg-icons';
import Progress from '../scripts/loading';
import XTimeline from '../components/timeline.vue';
import XTutorial from './index.home.tutorial.vue';
export default Vue.extend({
metaInfo() {
@ -32,7 +36,8 @@ export default Vue.extend({
},
components: {
XTimeline
XTimeline,
XTutorial,
},
props: {
@ -57,7 +62,7 @@ export default Vue.extend({
return {
't': this.focus
};
}
},
},
watch: {
@ -78,21 +83,11 @@ export default Vue.extend({
},
created() {
this.$root.getMeta().then((meta: Record<string, any>) => {
if (!(
this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
) && this.src === 'global') this.src = 'local';
if (!(
this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
) && ['local', 'social'].includes(this.src)) this.src = 'home';
});
if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src;
if (this.src === 'list') {
this.list = this.$store.state.device.tl.arg;
} else if (this.src === 'antenna') {
this.antenna = this.$store.state.device.tl.arg;
}
this.src = this.$store.state.deviceUser.tl.src;
if (this.src === 'list') {
this.list = this.$store.state.deviceUser.tl.arg;
} else if (this.src === 'antenna') {
this.antenna = this.$store.state.deviceUser.tl.arg;
}
},
@ -159,7 +154,7 @@ export default Vue.extend({
},
saveSrc() {
this.$store.commit('device/setTl', {
this.$store.commit('deviceUser/setTl', {
src: this.src,
arg: this.src == 'list' ? this.list : this.antenna
});
@ -172,7 +167,13 @@ export default Vue.extend({
});
</script>
<style lang="scss">
<style lang="scss" scoped>
.mk-home {
> .tutorial {
margin-bottom: var(--margin);
}
}
._kjvfvyph_ {
position: relative;
height: 100%;

View File

@ -1,10 +1,10 @@
<template>
<div class="rsqzvsbo">
<div class="_panel about">
<div class="banner" :style="{ backgroundImage: `url(${ banner })` }"></div>
<div class="_panel about" v-if="meta">
<div class="banner" :style="{ backgroundImage: `url(${ meta.bannerUrl })` }"></div>
<div class="body">
<h1 class="name" v-html="name || host"></h1>
<div class="desc" v-html="description || $t('introMisskey')"></div>
<h1 class="name" v-html="meta.name || host"></h1>
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
<mk-button @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</mk-button>
<mk-button @click="signin()" style="display: inline-block;">{{ $t('login') }}</mk-button>
</div>
@ -39,23 +39,16 @@ export default Vue.extend({
noPaging: true,
},
host: toUnicode(host),
meta: null,
name: null,
description: null,
banner: null,
announcements: [],
};
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
this.name = meta.name;
this.description = meta.description;
this.announcements = meta.announcements;
this.banner = meta.bannerUrl;
});
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
this.$root.api('stats').then(stats => {
this.stats = stats;
});

View File

@ -20,15 +20,14 @@ export default Vue.extend({
data() {
return {
meta: null,
instanceName: getInstanceName(),
}
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
});
}
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
});
</script>

View File

@ -98,7 +98,7 @@
<div class="operations">
<span class="label">{{ $t('operations') }}</span>
<mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch>
<mk-switch v-model="isBlocked" class="switch">{{ $t('blockThisInstance') }}</mk-switch>
<mk-switch :value="isBlocked" class="switch" @change="changeBlock">{{ $t('blockThisInstance') }}</mk-switch>
</div>
<details class="metadata">
<summary class="label">{{ $t('metadata') }}</summary>
@ -147,9 +147,7 @@ export default Vue.extend({
data() {
return {
meta: null,
isSuspended: false,
isBlocked: false,
isSuspended: this.instance.isSuspended,
now: null,
chart: null,
chartInstance: null,
@ -184,6 +182,14 @@ export default Vue.extend({
null;
return stats;
},
meta() {
return this.$store.state.instance.meta;
},
isBlocked() {
return this.meta && this.meta.blockedHosts.includes(this.instance.host);
}
},
@ -195,12 +201,6 @@ export default Vue.extend({
});
},
isBlocked() {
this.$root.api('admin/update-meta', {
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
});
},
chartSrc() {
this.renderChart();
},
@ -210,13 +210,7 @@ export default Vue.extend({
}
},
async created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
this.isSuspended = this.instance.isSuspended;
this.isBlocked = this.meta.blockedHosts.includes(this.instance.host);
});
async created() {
this.now = new Date();
const [perHour, perDay] = await Promise.all([
@ -235,6 +229,12 @@ export default Vue.extend({
},
methods: {
changeBlock(e) {
this.$root.api('admin/update-meta', {
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
});
},
setSrc(src) {
this.chartSrc = src;
},

View File

@ -20,6 +20,9 @@
</section>
<section class="_card info">
<div class="_content">
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
</div>
<div class="_content">
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
@ -120,30 +123,30 @@
<section class="_card">
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><fa :icon="faTwitter"/> {{ $t('twitter-integration-config') }}</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</mk-switch>
<header><fa :icon="faTwitter"/> Twitter</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableTwitterIntegration">
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-key') }}</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-secret') }}</mk-input>
<mk-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</mk-info>
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faGithub"/> {{ $t('github-integration-config') }}</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</mk-switch>
<header><fa :icon="faGithub"/> GitHub</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableGithubIntegration">
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-id') }}</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-secret') }}</mk-input>
<mk-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</mk-info>
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faDiscord"/> {{ $t('discord-integration-config') }}</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</mk-switch>
<header><fa :icon="faDiscord"/> Discord</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableDiscordIntegration">
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-id') }}</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-secret') }}</mk-input>
<mk-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</mk-info>
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_footer">
@ -171,7 +174,7 @@
<script lang="ts">
import Vue from 'vue';
import { faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import MkButton from '../../components/ui/button.vue';
@ -180,7 +183,7 @@ import MkTextarea from '../../components/ui/textarea.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import MkUserSelect from '../../components/user-select.vue';
import { version } from '../../config';
import { version, url } from '../../config';
import i18n from '../../i18n';
import getAcct from '../../../misc/acct/render';
@ -204,7 +207,7 @@ export default Vue.extend({
data() {
return {
version,
meta: null,
url,
stats: null,
serverInfo: null,
proxyAccount: null,
@ -222,6 +225,7 @@ export default Vue.extend({
tosUrl: null,
bannerUrl: null,
iconUrl: null,
maxNoteTextLength: 0,
enableRegistration: false,
enableLocalTimeline: false,
enableGlobalTimeline: false,
@ -240,52 +244,56 @@ export default Vue.extend({
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
}
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
this.maintainerName = this.meta.maintainerName;
this.maintainerEmail = this.meta.maintainerEmail;
this.enableRegistration = !this.meta.disableRegistration;
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
this.enableRecaptcha = this.meta.enableRecaptcha;
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
this.proxyAccountId = this.meta.proxyAccountId;
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
this.githubClientId = this.meta.githubClientId;
this.githubClientSecret = this.meta.githubClientSecret;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret;
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
this.proxyAccount = proxyAccount;
});
}
});
created() {
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
this.maintainerName = this.meta.maintainerName;
this.maintainerEmail = this.meta.maintainerEmail;
this.maxNoteTextLength = this.meta.maxNoteTextLength;
this.enableRegistration = !this.meta.disableRegistration;
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
this.enableRecaptcha = this.meta.enableRecaptcha;
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
this.proxyAccountId = this.meta.proxyAccountId;
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
this.githubClientId = this.meta.githubClientId;
this.githubClientSecret = this.meta.githubClientSecret;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret;
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
this.proxyAccount = proxyAccount;
});
}
this.$root.api('admin/server-info').then(res => {
this.serverInfo = res;
@ -346,6 +354,7 @@ export default Vue.extend({
iconUrl: this.iconUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
disableRegistration: !this.enableRegistration,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
@ -372,6 +381,7 @@ export default Vue.extend({
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
this.$store.dispatch('instance/fetch');
if (withDialog) {
this.$root.dialog({
type: 'success',

View File

@ -247,6 +247,7 @@ export default Vue.extend({
padding: 16px 16px 0 16px;
resize: none;
font-size: 1em;
font-family: inherit;
outline: none;
border: none;
border-radius: 0;
@ -270,12 +271,8 @@ export default Vue.extend({
margin: 0;
padding: 16px;
font-size: 1em;
color: #aaa;
transition: color 0.1s ease;
&:hover {
color: var(--accent);
}
color: var(--accent);
&:active {
color: var(--accentDarken);
@ -337,7 +334,6 @@ export default Vue.extend({
font-size: 1em;
font-weight: normal;
text-decoration: none;
color: #aaa;
transition: color 0.1s ease;
&:hover {

View File

@ -231,7 +231,6 @@ export default Vue.extend({
display: block;
margin: 2px 0 0 0;
font-size: 10px;
color: var(--messagingRoomMessageInfo);
> .read {
margin: 0 8px;
@ -288,6 +287,7 @@ export default Vue.extend({
> .balloon {
background: $me-balloon-color;
box-shadow: 0 6px 16px var(--accentShadow);
text-align: left;
&[data-no-text] {

View File

@ -8,6 +8,7 @@
<portal to="avatar"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
</template>
<template v-if="!fetching && group">
<portal to="icon"><fa :icon="faUsers"/></portal>
<portal to="title">{{ group.name }}</portal>
</template>
@ -35,7 +36,7 @@
<script lang="ts">
import Vue from 'vue';
import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons';
import { faArrowCircleDown, faFlag, faUsers } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
import XList from '../components/date-separated-list.vue';
import XMessage from './messaging-room.message.vue';
@ -63,7 +64,7 @@ export default Vue.extend({
connection: null,
showIndicator: false,
timer: null,
faArrowCircleDown, faFlag
faArrowCircleDown, faFlag, faUsers
};
},

View File

@ -150,7 +150,7 @@ export default Vue.extend({
showCancelButton: true
});
if (canceled) return;
this.navigateGroup(group);
this.$router.push(`/my/messaging/group/${group.id}`);
}
}
});

View File

@ -45,6 +45,8 @@ export default Vue.extend({
height: 150px;
margin-bottom: 16px;
border-radius: 16px;
pointer-events: none;
user-select: none;
}
}
}

View File

@ -1,5 +1,8 @@
<template>
<div class="mk-note-page">
<portal to="avatar" v-if="note"><mk-avatar class="avatar" :user="note.user" :disable-preview="true"/></portal>
<portal to="title" v-if="note">{{ $t('noteOf', { user: note.user.name }) }}</portal>
<transition name="zoom" mode="out-in">
<x-note v-if="note" :note="note" :key="note.id" :detail="true"/>
<div v-else-if="error">

View File

@ -56,15 +56,17 @@ export default Vue.extend({
computed: {
integrations() {
return this.$store.state.i.integrations;
}
},
meta() {
return this.$store.state.instance.meta;
},
},
created() {
this.$root.getMeta().then(meta => {
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.enableGithubIntegration = meta.enableGithubIntegration;
});
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
},
mounted() {

View File

@ -31,6 +31,7 @@ export const router = new VueRouter({
{ path: '/my/mentions', component: page('mentions') },
{ path: '/my/messaging', name: 'messaging', component: page('messaging') },
{ path: '/my/messaging/:user', component: page('messaging-room') },
{ path: '/my/messaging/group/:group', component: page('messaging-room') },
{ path: '/my/drive', name: 'drive', component: page('drive') },
{ path: '/my/drive/folder/:folder', component: page('drive') },
{ path: '/my/pages', name: 'pages', component: page('pages') },

View File

@ -8,7 +8,8 @@ export default (opts) => ({
fetching: true,
moreFetching: false,
inited: false,
more: false
more: false,
backed: false,
};
},
@ -78,6 +79,7 @@ export default (opts) => ({
async fetchMore() {
if (!this.more || this.moreFetching || this.items.length === 0) return;
this.moreFetching = true;
this.backed = true;
let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
if (params && params.then) params = await params;
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;

View File

@ -5,6 +5,7 @@ import * as nestedProperty from 'nested-property';
import MiOS from './mios';
const defaultSettings = {
tutorial: 0,
keepCw: false,
showFullAcct: false,
rememberNoteVisibility: false,
@ -15,7 +16,15 @@ const defaultSettings = {
wallpaper: null,
memo: null,
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
widgets: []
};
const defaultDeviceUserSettings = {
visibility: 'public',
localOnly: false,
widgets: [],
tl: {
src: 'home'
},
};
const defaultDeviceSettings = {
@ -26,16 +35,19 @@ const defaultDeviceSettings = {
autoReload: false,
accounts: [],
recentEmojis: [],
visibility: 'public',
localOnly: false,
themes: [],
theme: 'light',
animation: true,
userData: {},
};
function copy<T>(data: T): T {
return JSON.parse(JSON.stringify(data));
}
export default (os: MiOS) => new Vuex.Store({
plugins: [createPersistedState({
paths: ['i', 'device', 'settings']
paths: ['i', 'device', 'deviceUser', 'settings', 'instance']
})],
state: {
@ -57,10 +69,11 @@ export default (os: MiOS) => new Vuex.Store({
},
actions: {
login(ctx, i) {
async login(ctx, i) {
ctx.commit('updateI', i);
ctx.dispatch('settings/merge', i.clientData);
ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
ctx.commit('settings/init', i.clientData);
ctx.commit('deviceUser/init', ctx.state.device.userData[i.id] || {});
await ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
},
addAcount(ctx, info) {
@ -73,14 +86,17 @@ export default (os: MiOS) => new Vuex.Store({
},
logout(ctx) {
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
ctx.commit('updateI', null);
ctx.commit('settings/init', {});
ctx.commit('deviceUser/init', {});
localStorage.removeItem('i');
},
switchAccount(ctx, i) {
ctx.commit('updateI', i);
ctx.commit('settings/init', i.clientData);
async switchAccount(ctx, i) {
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
localStorage.setItem('i', i.token);
await ctx.dispatch('login', i);
},
mergeMe(ctx, me) {
@ -89,12 +105,36 @@ export default (os: MiOS) => new Vuex.Store({
}
if (me.clientData) {
ctx.dispatch('settings/merge', me.clientData);
ctx.commit('settings/init', me.clientData);
}
},
},
modules: {
instance: {
namespaced: true,
state: {
meta: null
},
mutations: {
set(state, meta) {
state.meta = meta;
},
},
actions: {
async fetch(ctx) {
const meta = await os.api('meta', {
detail: false
});
ctx.commit('set', meta);
}
}
},
device: {
namespaced: true,
@ -105,6 +145,32 @@ export default (os: MiOS) => new Vuex.Store({
state[x.key] = x.value;
},
setUserData(state, x: { userId: string; data: any }) {
state.userData[x.userId] = copy(x.data);
},
}
},
deviceUser: {
namespaced: true,
state: defaultDeviceUserSettings,
mutations: {
init(state, x) {
for (const [key, value] of Object.entries(defaultDeviceUserSettings)) {
if (x[key]) {
state[key] = x[key];
} else {
state[key] = value;
}
}
},
set(state, x: { key: string; value: any }) {
state[x.key] = x.value;
},
setTl(state, x) {
state.tl = {
src: x.src,
@ -119,6 +185,25 @@ export default (os: MiOS) => new Vuex.Store({
setLocalOnly(state, localOnly) {
state.localOnly = localOnly;
},
setWidgets(state, widgets) {
state.widgets = widgets;
},
addWidget(state, widget) {
state.widgets.unshift(widget);
},
removeWidget(state, widget) {
state.widgets = state.widgets.filter(w => w.id != widget.id);
},
updateWidget(state, x) {
const w = state.widgets.find(w => w.id == x.id);
if (w) {
w.data = x.data;
}
},
}
},
@ -144,13 +229,6 @@ export default (os: MiOS) => new Vuex.Store({
},
actions: {
merge(ctx, settings) {
if (settings == null) return;
for (const [key, value] of Object.entries(settings)) {
ctx.commit('set', { key, value });
}
},
set(ctx, x) {
ctx.commit('set', x);
@ -161,41 +239,6 @@ export default (os: MiOS) => new Vuex.Store({
});
}
},
setWidgets(ctx, widgets) {
ctx.state.widgets = widgets;
ctx.dispatch('updateWidgets');
},
addWidget(ctx, widget) {
ctx.state.widgets.unshift(widget);
ctx.dispatch('updateWidgets');
},
removeWidget(ctx, widget) {
ctx.state.widgets = ctx.state.widgets.filter(w => w.id != widget.id);
ctx.dispatch('updateWidgets');
},
updateWidget(ctx, x) {
const w = ctx.state.widgets.find(w => w.id == x.id);
if (w) {
w.data = x.data;
ctx.dispatch('updateWidgets');
}
},
updateWidgets(ctx) {
const widgets = ctx.state.widgets;
ctx.commit('set', {
key: 'widgets',
value: widgets
});
os.api('i/update-client-setting', {
name: 'widgets',
value: widgets
});
},
}
}
}

View File

@ -89,6 +89,8 @@ body {
width: 64px;
height: 64px;
animation: ini 0.6s infinite linear;
color: var(--accent);
fill: currentColor;
}
}
@ -196,6 +198,7 @@ a {
@extend ._button;
color: #fff;
background: var(--accent);
box-shadow: 0 6px 16px var(--accentShadow);
&:not(:disabled):hover {
background: var(--jkhztclx);
@ -385,7 +388,7 @@ a {
}
@keyframes blink {
0% { opacity: 1; }
30% { opacity: 1; }
90% { opacity: 0; }
0% { opacity: 1; transform: scale(1); }
30% { opacity: 1; transform: scale(1); }
90% { opacity: 0; transform: scale(0.5); }
}

View File

@ -18,7 +18,7 @@ self.addEventListener('install', ev => {
caches.open(cacheName)
.then(cache => {
return cache.addAll([
'/'
`/?v=${version}`
]);
})
.then(() => self.skipWaiting())
@ -45,7 +45,7 @@ self.addEventListener('fetch', ev => {
return response || fetch(ev.request);
})
.catch(() => {
return caches.match('/');
return caches.match(`/?v=${version}`);
})
);
});

View File

@ -10,6 +10,7 @@
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
accentShadow: ':alpha<0.3<@accent',
focus: ':alpha<0.3<@accent',
bg: '#000',
fg: '#c7d1d8',

View File

@ -10,6 +10,7 @@
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
accentShadow: ':alpha<0.4<@accent',
focus: ':alpha<0.3<@accent',
bg: '#fafafa',
fg: '#5c6a73',

View File

@ -51,7 +51,7 @@ export default function <T extends object>(data: {
},
save() {
this.$store.dispatch('settings/updateWidget', this.widget);
this.$store.commit('deviceUser/updateWidget', this.widget);
}
}
});

View File

@ -10,6 +10,7 @@
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>検索</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
</tbody>
</table>

View File

@ -13,6 +13,11 @@ export const meta = {
default: 10
},
withUnreads: {
validator: $.optional.boolean,
default: false
},
sinceId: {
validator: $.optional.type(ID),
},
@ -38,5 +43,5 @@ export default define(meta, async (ps, user) => {
}
}
return announcements;
return ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements;
});

View File

@ -327,6 +327,10 @@ const override = (source: string, target: string, depth: number = 0) =>
router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
router.get('/flush', async ctx => {
await ctx.render('flush');
});
// Render base html for all requests
router.get('*', async ctx => {
const meta = await fetchMeta();

View File

@ -55,6 +55,6 @@ html
| Please turn on your JavaScript
div#ini.
<svg viewBox="0 0 50 50">
<path fill=#fb4e4e d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
<path d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
</svg>
block content

View File

@ -0,0 +1,20 @@
doctype html
html
script.
localStorage.removeItem('locale');
try {
navigator.serviceWorker.controller.postMessage('clear');
navigator.serviceWorker.getRegistrations().then(registrations => {
return Promise.all(registrations.map(registration => registration.unregister()));
}).then(() => {
location = '/';
});
} catch (e) {
console.error(e);
setTimeout(() => {
location = '/';
}, 10000)
}

View File

@ -3,6 +3,8 @@ import { Note } from '../../models/entities/note';
import { User } from '../../models/entities/user';
import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models';
// TODO: 状態が変化していない場合は各種イベントを送信しない
/**
* Mark a note as read
*/

View File

@ -102,6 +102,11 @@
normalize-path "^2.0.1"
through2 "^2.0.3"
"@juggle/resize-observer@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.0.2.tgz#f4f326806826c0da5221411374e4c54902c327bb"
integrity sha512-9fEfZIxZ1qTahMSX50pigfSJ/1N6X2oABhmgd4Dt2SsPkZi0lTlvoHu5V2yrGZ6m+oALfS4tJrpx9nbVIxwOYQ==
"@koa/cors@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.0.0.tgz#df021b4df2dadf1e2b04d7c8ddf93ba2d42519cb"