Compare commits

...

256 Commits

Author SHA1 Message Date
f968d05ea0 2.37.7 2018-06-12 09:10:52 +09:00
d6e5dc2167 Fix bugs 2018-06-12 09:10:34 +09:00
460147fea2 2.37.6 2018-06-12 08:59:36 +09:00
cea44834bb Improve usability 2018-06-12 08:58:50 +09:00
1af50fd7b8 冗長なハッシュタグの表示を無くした 2018-06-12 08:43:48 +09:00
b18013025f 🎨 2018-06-12 08:09:27 +09:00
399eb60809 2.37.5 2018-06-12 02:47:17 +09:00
ed67e3506b ✌️ 2018-06-12 02:46:54 +09:00
d8ff37fc45 ✌️ 2018-06-12 02:28:28 +09:00
2fcc3bb1ea 2.37.4 2018-06-12 02:19:00 +09:00
2e680c3d1e Fix bug 2018-06-12 02:18:29 +09:00
af0a0ef41b Merge branch 'master' of https://github.com/syuilo/misskey 2018-06-12 02:03:26 +09:00
bbfccb0bbf 🎨 2018-06-12 02:03:18 +09:00
c89eb5d69f Merge pull request #1705 from syuilo/l10n_master
New Crowdin translations
2018-06-12 02:03:06 +09:00
ebde84214e Improve hashtag trend detection 2018-06-12 02:00:05 +09:00
03fbae7b6d 変数調整 2018-06-12 01:51:51 +09:00
f90e9596d4 Fix bug 2018-06-12 01:48:29 +09:00
944f9524e2 Fix bug 2018-06-12 01:45:58 +09:00
c61050244e 変数調整 2018-06-12 01:43:56 +09:00
90337adbbc Improve hashtag trend detection 2018-06-12 01:41:17 +09:00
7b67e41c5b New translations ja.yml (Polish) 2018-06-11 19:22:16 +09:00
91db24fcfc 2.37.3 2018-06-11 14:16:46 +09:00
bb53db905f ✌️ 2018-06-11 14:16:21 +09:00
0e9a1efe46 Merge pull request #1704 from syuilo/l10n_master
New Crowdin translations
2018-06-11 14:11:33 +09:00
289cd3e200 New translations ja.yml (English) 2018-06-11 14:10:47 +09:00
e0f847e539 ✌️ 2018-06-11 14:06:23 +09:00
c2842b486e New translations ja.yml (Portuguese) 2018-06-11 13:51:15 +09:00
7235ade42f New translations ja.yml (Korean) 2018-06-11 13:51:13 +09:00
850be2df1d New translations ja.yml (Polish) 2018-06-11 13:51:11 +09:00
d504501440 New translations ja.yml (Chinese Simplified) 2018-06-11 13:51:09 +09:00
208392f12c New translations ja.yml (Italian) 2018-06-11 13:51:07 +09:00
0fe036c640 New translations ja.yml (Russian) 2018-06-11 13:51:05 +09:00
a40c41f0b0 New translations ja.yml (English) 2018-06-11 13:51:03 +09:00
4affa5b710 New translations ja.yml (Spanish) 2018-06-11 13:51:01 +09:00
4eb574d991 New translations ja.yml (German) 2018-06-11 13:50:59 +09:00
2c1577ea24 New translations ja.yml (French) 2018-06-11 13:50:56 +09:00
b87e7e50b6 Clean up 2018-06-11 13:49:53 +09:00
36215d50bd 2.37.2 2018-06-11 13:48:07 +09:00
5ff1245d0c Merge pull request #1703 from syuilo/l10n_master
New Crowdin translations
2018-06-11 13:47:42 +09:00
ebd189fb27 🍕 2018-06-11 13:46:23 +09:00
6f724827bd ✌️ 2018-06-11 13:45:32 +09:00
b6a0982012 New translations ja.yml (English) 2018-06-11 13:00:50 +09:00
c3e375e8a5 ✌️ 2018-06-11 12:52:47 +09:00
302409fd83 New translations ja.yml (Portuguese) 2018-06-11 11:51:04 +09:00
a2046461c1 New translations ja.yml (Korean) 2018-06-11 11:51:03 +09:00
6660c34120 New translations ja.yml (Polish) 2018-06-11 11:51:01 +09:00
b88ccf0ddd New translations ja.yml (Chinese Simplified) 2018-06-11 11:50:58 +09:00
b898bbf94c New translations ja.yml (Italian) 2018-06-11 11:50:57 +09:00
787e89eb95 New translations ja.yml (Russian) 2018-06-11 11:50:55 +09:00
1022c2c438 New translations ja.yml (English) 2018-06-11 11:50:53 +09:00
ba21c62ed4 New translations ja.yml (Spanish) 2018-06-11 11:50:51 +09:00
bfe66c919b New translations ja.yml (German) 2018-06-11 11:50:49 +09:00
3dacf7f661 New translations ja.yml (French) 2018-06-11 11:50:47 +09:00
c0a3ae2612 2.37.1 2018-06-11 11:45:27 +09:00
da612ef789 ✌️ 2018-06-11 11:45:10 +09:00
df9cb7cf6e 2.37.0 2018-06-11 11:44:26 +09:00
9c1a26110e Merge pull request #1697 from syuilo/l10n_master
New Crowdin translations
2018-06-11 11:43:58 +09:00
0883d18a6c New translations ja.yml (English) 2018-06-11 11:41:56 +09:00
c7246c61a5 New translations ja.yml (Portuguese) 2018-06-11 11:31:01 +09:00
c5a1431fc0 New translations ja.yml (Korean) 2018-06-11 11:30:59 +09:00
f0118a0dff New translations ja.yml (Polish) 2018-06-11 11:30:57 +09:00
cffe96e46f New translations ja.yml (Chinese Simplified) 2018-06-11 11:30:55 +09:00
a9256578f0 New translations ja.yml (Italian) 2018-06-11 11:30:53 +09:00
05ed202904 New translations ja.yml (Russian) 2018-06-11 11:30:51 +09:00
963b63389a New translations ja.yml (English) 2018-06-11 11:30:50 +09:00
e04706dc74 New translations ja.yml (Spanish) 2018-06-11 11:30:47 +09:00
04d4ce5ce1 New translations ja.yml (German) 2018-06-11 11:30:46 +09:00
24cf3730fa New translations ja.yml (French) 2018-06-11 11:30:44 +09:00
0700be86e2 Merge pull request #1702 from syuilo/hashtags-stats
Hashtags stats
2018-06-11 11:27:55 +09:00
7cca509eb3 ✌️ 2018-06-11 11:27:21 +09:00
7d7193cb63 ✌️ 2018-06-11 11:24:29 +09:00
1cf10d05ff Merge pull request #1698 from syuilo/cleanup-dependencies
🆙 move some packages to devDependencies that non required by server
2018-06-11 09:40:04 +09:00
2ec25a7729 wip 2018-06-11 09:11:32 +09:00
2a9065a61e New translations ja.yml (French) 2018-06-11 08:20:56 +09:00
7518e30dcf 🆙 move some packages to devDependencies that non required by server
presumed by:
- move-to-devdependencies.fish
```fish
set targets (ls src | grep -v client | xargs -I'%' echo "src/%")
alias from_import="git grep 'import ' $targets | grep -v 'from \'\.' | grep -v 'from \"\.' | cut -d: -f2 | cut -d\; -f1 | rev | cut -d' ' -f1 | rev | cut -d\' -f2 | sort | uniq | grep -v '^readline\$' | grep -v '^zlib\$' | grep -v '^os\$' | grep -v '^http\$' | grep -v '^fs\$' | grep -v '^events\$' | grep -v '^crypto\$' | grep -v '^child_process\$' | grep -v '^cluster\$'`"
alias from_require="git grep 'require(' $targets | grep -v '(\'\.' | cut -d= -f2 | grep -v '__dirname' | grep require | cut -d' ' -f2 | cut -d')' -f1 | cut -d'(' -f2 | cut -d'\'' -f2 | sort | uniq | grep -v '^readline\$' | grep -v '^zlib\$' | grep -v '^os\$' | grep -v '^http\$' | grep -v '^fs\$' | grep -v '^events\$' | grep -v '^crypto\$' | grep -v '^child_process\$' | grep -v '^cluster\$'"
from_import | xargs npm uninstall --save-dev
from_require | xargs npm uninstall --save-dev
from_import | xargs npm install --save
from_require | xargs npm install --save
git show HEAD:require | node revert-pinning-dependencies.js
```
- revert-pinning-dependencies.js
```js
const readFromStdin = () => new Promise((resolve, reject) => {
  const chunks = []

  process.stdin.setEncoding('utf8')

  process.stdin.on('readable', () => {
    const chunk = process.stdin.read()
    if (chunk == null) return
    chunks.push(chunk)
  })

  process.stdin.on('end', () => {
    return resolve(chunks.join('\n'))
  })
})

async function main () {
  const fs = require('fs')

  const raw = await readFromStdin()
  const head = JSON.parse(raw)
  const now = JSON.parse(fs.readFileSync('package.json'))

  Object.keys(now.dependencies).forEach(key => {
    now.dependencies[key] = head.dependencies[key]
  })

  fs.writeFileSync('package.json', JSON.stringify(now,null,'\t'))
}

main().catch(console.error)
```
2018-06-11 08:08:52 +09:00
dc3c80e3ce wip 2018-06-11 06:48:25 +09:00
a25f61f6be New translations ja.yml (Polish) 2018-06-11 05:41:06 +09:00
e70fb71a04 Update example.yml 2018-06-11 02:18:00 +09:00
f499630c2b 2.36.1 2018-06-11 01:28:45 +09:00
43319a8588 Merge pull request #1694 from syuilo/l10n_master
New Crowdin translations
2018-06-11 01:21:52 +09:00
d62b943c5d New translations ja.yml (English) 2018-06-11 01:21:03 +09:00
8baddf2ea3 Fix #1695 2018-06-11 01:14:29 +09:00
600482660b タグは最大100文字までにした 2018-06-11 01:12:37 +09:00
72ab5c143e Revert "✌️"
This reverts commit 96ab0e7b4c.
2018-06-11 01:07:34 +09:00
96ab0e7b4c ✌️ 2018-06-11 00:27:16 +09:00
b60903e2b4 🍕 2018-06-11 00:24:03 +09:00
b4f4d3f267 New translations ja.yml (Portuguese) 2018-06-10 12:51:00 +09:00
6e017c86e8 New translations ja.yml (Korean) 2018-06-10 12:50:58 +09:00
afcfc2dca5 New translations ja.yml (Polish) 2018-06-10 12:50:57 +09:00
59e22a12a9 New translations ja.yml (Chinese Simplified) 2018-06-10 12:50:55 +09:00
b740ac3e01 New translations ja.yml (Italian) 2018-06-10 12:50:53 +09:00
9719f0df03 New translations ja.yml (Russian) 2018-06-10 12:50:51 +09:00
d4be599538 New translations ja.yml (English) 2018-06-10 12:50:49 +09:00
f88195c90a New translations ja.yml (Spanish) 2018-06-10 12:50:47 +09:00
3b33f7e752 New translations ja.yml (German) 2018-06-10 12:50:46 +09:00
67a37294f7 New translations ja.yml (French) 2018-06-10 12:50:43 +09:00
fd88955696 ✌️ 2018-06-10 12:36:48 +09:00
9d248dbb5a 🎨 2018-06-10 12:29:19 +09:00
20ec4104c6 2.36.0 2018-06-10 12:22:48 +09:00
6c232d116d Merge pull request #1692 from syuilo/l10n_master
New Crowdin translations
2018-06-10 12:22:28 +09:00
2ef78bcd40 🍕 2018-06-10 12:21:44 +09:00
94ce658ab9 ハッシュタグ検索を実装 2018-06-10 12:19:19 +09:00
d8cf4cd341 New translations ja.yml (Polish) 2018-06-10 10:30:48 +09:00
0360337df9 2.35.3 2018-06-10 08:42:29 +09:00
119d38ea08 Fix bug 2018-06-10 08:41:57 +09:00
bee77afb7f Fix bug 2018-06-10 08:34:46 +09:00
16d4b16872 Revert "✌️"
This reverts commit 951b2346ab.
2018-06-10 08:21:48 +09:00
951b2346ab ✌️ 2018-06-10 08:15:05 +09:00
b29ff0e94b ✌️ 2018-06-10 08:03:02 +09:00
c8dd8341ca 2.35.2 2018-06-10 03:58:12 +09:00
8bcf44bc16 ✌️ 2018-06-10 03:55:51 +09:00
50b37a8420 oops 2018-06-10 03:48:38 +09:00
22df795733 Fix bug 2018-06-10 03:47:09 +09:00
7e3bf06db1 2.35.1 2018-06-10 03:31:42 +09:00
6630ca595c 🎨 2018-06-10 03:31:13 +09:00
5d01e19ce7 Fix bug 2018-06-10 03:27:10 +09:00
56df89f8dd 2.35.0 2018-06-10 01:22:44 +09:00
13de984ce3 Merge pull request #1688 from syuilo/l10n_master
New Crowdin translations
2018-06-10 01:21:55 +09:00
15fc0e30d7 New translations ja.yml (English) 2018-06-10 01:21:19 +09:00
4289c11185 New translations ja.yml (German) 2018-06-10 01:21:17 +09:00
a3f564e702 New translations ja.yml (Portuguese) 2018-06-10 01:11:33 +09:00
f6734a0c98 New translations ja.yml (Korean) 2018-06-10 01:11:32 +09:00
72fb416239 New translations ja.yml (Polish) 2018-06-10 01:11:30 +09:00
833f5b09d2 New translations ja.yml (Chinese Simplified) 2018-06-10 01:11:28 +09:00
b21b21f30a New translations ja.yml (Italian) 2018-06-10 01:11:26 +09:00
2f77a3f6d2 New translations ja.yml (Russian) 2018-06-10 01:11:25 +09:00
0bda655452 New translations ja.yml (English) 2018-06-10 01:11:23 +09:00
4f80bb7031 New translations ja.yml (Spanish) 2018-06-10 01:11:21 +09:00
fbe7b3cc9b New translations ja.yml (German) 2018-06-10 01:11:18 +09:00
8402f0abd7 New translations ja.yml (French) 2018-06-10 01:11:16 +09:00
149b2ee5a7 Update sentences 2018-06-10 01:04:31 +09:00
f9d5af0600 MissketDeck: Implement media view tl 2018-06-10 01:01:28 +09:00
72c4ccaee8 Clean up dependencies 2018-06-10 00:41:57 +09:00
92999dcaf2 Update gitignore 2018-06-10 00:39:30 +09:00
5bbd318518 Update message 2018-06-09 23:52:11 +09:00
8807894890 Fix bug 2018-06-09 22:25:15 +09:00
63b7820717 New translations ja.yml (German) 2018-06-09 20:41:00 +09:00
9e7e2d6977 New translations ja.yml (German) 2018-06-09 20:31:00 +09:00
89e4c280ae New translations ja.yml (German) 2018-06-09 20:20:56 +09:00
b6c9f29be4 New translations ja.yml (German) 2018-06-09 20:11:27 +09:00
74cbbc84ed New translations ja.yml (English) 2018-06-09 20:00:53 +09:00
ead4197670 New translations ja.yml (German) 2018-06-09 20:00:51 +09:00
4fc69ccdc8 New translations ja.yml (German) 2018-06-09 19:51:04 +09:00
f556cb44b9 New translations ja.yml (German) 2018-06-09 19:40:57 +09:00
45b540d375 New translations ja.yml (Polish) 2018-06-09 12:00:47 +09:00
af2d36a3c9 2.34.3 2018-06-09 11:41:22 +09:00
42a4f92cfa Fix #1125 2018-06-09 11:40:42 +09:00
ccb9ed3489 🎨 2018-06-09 11:29:50 +09:00
773b2aa3d1 2.34.2 2018-06-09 10:16:29 +09:00
30d5b8d65b 🎨 2018-06-09 10:15:45 +09:00
763676a18c ✌️ 2018-06-09 10:12:03 +09:00
e166ad6780 Better init screen 2018-06-09 10:06:27 +09:00
034c96d070 Merge branch 'master' of https://github.com/syuilo/misskey 2018-06-09 08:38:33 +09:00
f34f8d304c Improve performance 2018-06-09 08:38:30 +09:00
944000c05c Update README.md 2018-06-09 06:52:33 +09:00
e2503cdb47 New translations ja.yml (English) 2018-06-09 06:31:45 +09:00
52db63bca2 2.34.1 2018-06-09 06:28:22 +09:00
55dfd9e2a1 Merge pull request #1687 from syuilo/l10n_master
New Crowdin translations
2018-06-09 06:27:57 +09:00
d193cbf2b7 ✌️ 2018-06-09 06:27:12 +09:00
bdec56a543 ✌️ 2018-06-09 06:25:41 +09:00
e0a6d9740c ✌️ 2018-06-09 06:21:13 +09:00
0ce9c057e1 Fix chart rendering 2018-06-09 06:04:41 +09:00
12a2fdbc20 New translations ja.yml (Portuguese) 2018-06-09 04:21:28 +09:00
57c294bc89 New translations ja.yml (Korean) 2018-06-09 04:21:26 +09:00
9758757805 New translations ja.yml (Polish) 2018-06-09 04:21:25 +09:00
f9350fa35f New translations ja.yml (Chinese Simplified) 2018-06-09 04:21:22 +09:00
e120da4ecd New translations ja.yml (Italian) 2018-06-09 04:21:20 +09:00
328a10b70c New translations ja.yml (Russian) 2018-06-09 04:21:18 +09:00
1ed97c8deb New translations ja.yml (English) 2018-06-09 04:21:17 +09:00
91b970e2aa New translations ja.yml (Spanish) 2018-06-09 04:21:15 +09:00
99af1bb479 New translations ja.yml (German) 2018-06-09 04:21:13 +09:00
11ddcbdee3 New translations ja.yml (French) 2018-06-09 04:21:11 +09:00
6e8a1086d8 2.34.0 2018-06-09 04:15:18 +09:00
c78945436e #1686 2018-06-09 04:14:26 +09:00
6eff8fde74 サーバーの統計情報をメモリに記憶するようにするなど 2018-06-09 01:45:25 +09:00
726d5a177e Merge pull request #1685 from 2vg/patch-1
fix: when text is null, bug can pass validation.
2018-06-08 22:04:22 +09:00
33495b5cb3 fix: "or" operator. 2018-06-08 22:04:07 +09:00
fe159a13a9 2.33.2 2018-06-08 22:03:24 +09:00
22a1dc0566 Better log 2018-06-08 22:03:14 +09:00
02e6b732e9 fix: when text is null, bug can pass validation.
fixed. (maybe?)
2018-06-08 22:00:18 +09:00
cc6fa135ac 2.33.1 2018-06-08 21:50:41 +09:00
5747732156 Fix 2018-06-08 21:50:31 +09:00
581d1617d8 2.33.0 2018-06-08 21:39:24 +09:00
6152fd20bf Improve deck 2018-06-08 21:37:20 +09:00
19300ca65c Add new cli tool 2018-06-08 21:22:13 +09:00
2f3d744e19 🎨 2018-06-08 21:17:48 +09:00
724e812972 Refactor 2018-06-08 20:57:02 +09:00
9a6246fd4e New: Zen mode 2018-06-08 20:34:44 +09:00
34f44de59c Update README.md 2018-06-08 13:14:30 +09:00
16e446c121 2.32.0 2018-06-08 11:47:33 +09:00
8f232a9da9 Merge branch 'master' of https://github.com/syuilo/misskey 2018-06-08 11:46:48 +09:00
ebeb7f8578 ✌️ 2018-06-08 11:46:45 +09:00
f790673068 Merge pull request #1684 from syuilo/l10n_master
New Crowdin translations
2018-06-08 11:17:44 +09:00
335ab5ab54 Merge branch 'master' of https://github.com/syuilo/misskey 2018-06-08 11:17:30 +09:00
00e0d6ce2c Improve usability 2018-06-08 11:17:22 +09:00
414fb6d303 New translations ja.yml (French) 2018-06-08 10:41:25 +09:00
9c35a12211 New translations ja.yml (French) 2018-06-08 10:12:39 +09:00
bb4fe5174f Update README.md 2018-06-08 09:08:09 +09:00
3ffd6ff5a2 2.31.0 2018-06-08 08:39:04 +09:00
b05feb5bf7 MisskeyDeck: ドラッグでカラムを入れ替えられるように 2018-06-08 08:38:32 +09:00
fa171f237d 2.30.2 2018-06-08 08:06:19 +09:00
f2ccb684eb 🎨 2018-06-08 08:05:25 +09:00
ffea6522ac Merge pull request #1683 from syuilo/l10n_master
New Crowdin translations
2018-06-08 07:44:17 +09:00
3d40a7df00 ✌️ 2018-06-08 07:43:12 +09:00
638c41476b New translations ja.yml (German) 2018-06-08 07:40:48 +09:00
c6d3088374 Merge pull request #1682 from syuilo/l10n_master
New Crowdin translations
2018-06-08 07:26:58 +09:00
0f93be9dd4 New translations ja.yml (German) 2018-06-08 07:01:06 +09:00
f59982c9c5 New translations ja.yml (German) 2018-06-08 06:51:25 +09:00
dff67a5e54 New translations ja.yml (German) 2018-06-08 06:41:05 +09:00
6adcc3b2ed New translations ja.yml (German) 2018-06-08 06:31:09 +09:00
877ed3663c New translations ja.yml (German) 2018-06-08 06:21:24 +09:00
6000a82917 New translations ja.yml (German) 2018-06-08 06:11:02 +09:00
6805f9b3e0 New translations ja.yml (German) 2018-06-08 06:01:10 +09:00
1366c785f9 Update README.md 2018-06-08 05:54:42 +09:00
70540b4500 New translations ja.yml (German) 2018-06-08 05:51:01 +09:00
0967f23b6e 2.30.1 2018-06-08 05:49:19 +09:00
1f7d66169c Merge pull request #1681 from syuilo/l10n_master
New Crowdin translations
2018-06-08 05:48:51 +09:00
af501f5eeb Fix bug 2018-06-08 05:48:27 +09:00
60be60c923 New translations ja.yml (German) 2018-06-08 05:41:56 +09:00
48746101e0 New translations ja.yml (Polish) 2018-06-08 05:20:55 +09:00
af9c5c6ab7 typo 2018-06-08 05:07:19 +09:00
602284d38c 2.30.0 2018-06-08 05:04:56 +09:00
26898142c2 Merge pull request #1680 from syuilo/l10n_master
New Crowdin translations
2018-06-08 05:04:32 +09:00
b0a8d7abe9 ✌️ 2018-06-08 05:04:21 +09:00
dc2b266b75 New translations ja.yml (English) 2018-06-08 04:52:14 +09:00
07bbd9506a 🎨 2018-06-08 04:46:31 +09:00
14bb218287 New translations ja.yml (English) 2018-06-08 04:41:42 +09:00
29f238c929 New translations ja.yml (Portuguese) 2018-06-08 04:39:44 +09:00
a39a1d4fa5 New translations ja.yml (Korean) 2018-06-08 04:39:41 +09:00
15117c63f5 New translations ja.yml (Polish) 2018-06-08 04:39:39 +09:00
507ffb6fc6 New translations ja.yml (Chinese Simplified) 2018-06-08 04:39:38 +09:00
6b2e0164cf New translations ja.yml (Italian) 2018-06-08 04:39:36 +09:00
02e06eb1de New translations ja.yml (Russian) 2018-06-08 04:39:33 +09:00
1b50f78733 New translations ja.yml (English) 2018-06-08 04:39:31 +09:00
ead629407c New translations ja.yml (Spanish) 2018-06-08 04:39:29 +09:00
0abbc9e7dd New translations ja.yml (German) 2018-06-08 04:39:26 +09:00
37681e859e New translations ja.yml (French) 2018-06-08 04:39:24 +09:00
caabdc68f3 MisskeyDeck: スタックしたカラムを上下移動できるように 2018-06-08 04:34:15 +09:00
9e97eaf24d New translations ja.yml (English) 2018-06-08 04:31:03 +09:00
4cd06a789b New translations ja.yml (Portuguese) 2018-06-08 04:22:10 +09:00
a3ffd968de New translations ja.yml (Korean) 2018-06-08 04:22:08 +09:00
0cf40563aa New translations ja.yml (Polish) 2018-06-08 04:22:06 +09:00
3e7e7f864b New translations ja.yml (Chinese Simplified) 2018-06-08 04:22:03 +09:00
6ae415e36a New translations ja.yml (Italian) 2018-06-08 04:22:01 +09:00
6cefa3ae26 New translations ja.yml (Russian) 2018-06-08 04:21:59 +09:00
70de3af3ea New translations ja.yml (English) 2018-06-08 04:21:57 +09:00
66ed814527 New translations ja.yml (Spanish) 2018-06-08 04:21:55 +09:00
e12cc3b7a8 New translations ja.yml (German) 2018-06-08 04:21:53 +09:00
93ea19d7ad New translations ja.yml (French) 2018-06-08 04:21:51 +09:00
79d592b431 MisskeyDeck: カラムをスタックできるように 2018-06-08 04:21:06 +09:00
c9c3a0be82 New translations ja.yml (Polish) 2018-06-08 03:55:30 +09:00
f04be199dd New translations ja.yml (French) 2018-06-07 22:21:47 +09:00
f36cb1cc66 New translations ja.yml (French) 2018-06-07 22:12:09 +09:00
a5597e3df9 New translations ja.yml (French) 2018-06-07 21:26:16 +09:00
7f4c28053e New translations ja.yml (French) 2018-06-07 21:12:53 +09:00
ea24043b22 New translations ja.yml (English) 2018-06-07 08:50:51 +09:00
92 changed files with 2885 additions and 1282 deletions

View File

@ -55,3 +55,7 @@ twitter:
# インテグレーション用アプリのコンシューマーシークレット
consumer_secret:
# true にすると、リモートのファイルをキャッシュしなくなります(直リンクします)。
# ストレージ容量を節約することができますが、「リモートメディアを表示しない」設定をオンにしているユーザーは、リモートの画像などは見えなくなります。
preventCache: false

2
.gitignore vendored
View File

@ -11,4 +11,4 @@ npm-debug.log
run.bat
api-docs.json
package-lock.json
yarn.lock
*.log

View File

@ -12,15 +12,18 @@
> Lead Maintainer: [syuilo][syuilo-link]
**[Misskey](https://misskey.xyz)** is a completely open source,
ultimately sophisticated new type of mini-blog based SNS.
ultimately sophisticated professional microblogging software.
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
![](https://c10.patreonusercontent.com/3/e30%3D/patreon-posts/RsKWEDEKf8D_wYDQWAbex9CSb-1DnXW1nfqfLvuys5ROj2k0VF6_luuzHMTyf95n.png?token-time=1529539200&token-hash=RmcSP0947mw5o2-B6g1L6aU_OoDXANe198kLU6HMO30%3D)
:sparkles: Features
----------------------------------------------------------------
* Reactions
* User lists
* Cusromizable column view (known as MisskeyDeck)
* Customizable column view (known as MisskeyDeck)
* and widgets!
* Private messages
* Mute
* Streaming
@ -46,18 +49,9 @@ If you want to...
[![Backers][backers-image]][support-url]
[![Sponsors][sponsors-image]][support-url]
:mortar_board: Notable contributors
----------------------------------------------------------------
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![tamaina][tamaina-icon] | ![rinsuki][rinsuki-icon] |
|:-:|:-:|:-:|:-:|:-:|:-:|
| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [tamaina][tamaina-link] | [rinsuki][rinsuki-link] |
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
### :earth_americas: Translators
| ![][mirro-san-icon] | ![][Conan-kun-icon] | ![][m4sk1n-icon] |
|:-:|:-:|:-:|
| [Mirro][mirro-san-link]<br>English, French | [Asriel][Conan-kun-link]<br>English, French | [Marcin Mikołajczak][m4sk1n-link]<br>Polish |
| ![][ooo-icon] |
|:-:|
| [ooo][ooo-link] |
:four_leaf_clover: Copyright
----------------------------------------------------------------
@ -85,23 +79,8 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
[sponsors-image]: https://opencollective.com/misskey/sponsors.svg
[support-url]: https://opencollective.com/misskey#support
<!-- Contributors Info -->
[syuilo-link]: https://syuilo.com
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
[ayamorisawa-link]: https://github.com/ayamorisawa
[ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70
[otofune-link]: https://github.com/otofune
[otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70
[akihikodaki-link]: https://github.com/akihikodaki
[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4
[rinsuki-link]: https://github.com/rinsuki
[rinsuki-icon]: https://avatars0.githubusercontent.com/u/6533808?s=70&v=4
[tamaina-link]: https://github.com/tamaina
[tamaina-icon]: https://avatars1.githubusercontent.com/u/7973572?s=70&v=4
[mirro-san-link]: https://github.com/mirro-san
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4
[Conan-kun-link]: https://github.com/Conan-kun
[Conan-kun-icon]: https://avatars3.githubusercontent.com/u/30003708?s=70&v=4
[m4sk1n-link]: https://github.com/m4sk1n
[m4sk1n-icon]: https://avatars3.githubusercontent.com/u/21127288?s=70&v=4
[ooo-link]: https://www.patreon.com/user/creators?u=11601413
[ooo-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D

12
cli/update-remote-user.js Normal file
View File

@ -0,0 +1,12 @@
const updatePerson = require('../built/remote/activitypub/models/person').updatePerson;
const args = process.argv.slice(2);
const user = args[0];
console.log(`Updating ${user}...`);
updatePerson(user).then(() => {
console.log(`Updated ${user}`);
}, e => {
console.error(e);
});

View File

@ -57,30 +57,36 @@ common:
memo: "Notizen"
trends: "Trends"
photo-stream: "Bilder"
posts-monitor: "投稿チャート"
slideshow: "Diashow"
version: "Version"
broadcast: "ブロードキャスト"
notifications: "Benachrichtigungen"
users: "Empfohlene Benutzer"
polls: "Umfragen"
post-form: "投稿フォーム"
polls: "アンケート"
post-form: "Beitragsform"
messaging: "Nachrichten"
server: "Server-Info"
donation: "Spenden"
nav: "Navigation"
tips: "Tipps"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
local: "ローカル"
global: "グローバル"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
widgets: "Widget hinzufügen:"
home: "Startseite"
local: "Lokal"
global: "Global"
notifications: "Mitteilungen"
list: "Listen"
swap-left: "Nach links"
swap-right: "Nach rechts"
swap-up: "Nach oben"
swap-down: "Nach unten"
remove: "Spalte löschen"
add-column: "Eine Spalte hinzufügen"
rename: "Umbenennen"
stack-left: "Nach links schichten"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "Verbindung zum Server ist fehlgeschlagen"
description: "Es gibt entweder ein Problem mit deiner Internetverbindung, der Server ist nicht erreichbar oder wird gerade gewartet. Bitte versuche es später noch einmal."
@ -199,7 +205,7 @@ common/views/components/uploader.vue:
common/views/components/visibility-chooser.vue:
public: "Öffentlich"
home: "Home"
home-desc: "ホームタイムラインにのみ公開"
home-desc: "Nur auf die Startseite posten"
followers: "Folgende"
followers-desc: "Nur für diejenigen sichtbar, die dir folgen"
specified: "Direkt"
@ -216,6 +222,12 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "Fotostream"
no-photos: "Keine Fotos"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "Serverinformationen"
toggle: "Sicht umschalten"
@ -260,7 +272,7 @@ desktop/views/components/drive.file.vue:
rename: "Umbenennen"
copy-url: "URL kopieren"
download: "Download"
else-files: "その他..."
else-files: "Anderes…"
set-as-avatar: "Als Avatar festlegen"
set-as-banner: "Setze als Banner"
open-in-app: "In der App öffnen"
@ -302,10 +314,10 @@ desktop/views/components/drive.vue:
upload: "Eine Datei hochladen"
url-upload: "Von einer URL hochladen"
desktop/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-request: "フォロー申請"
following: "Folge ich"
follow: "Folgen"
request-pending: "Ausstehend"
follow-request: "Follower-Anfragen"
desktop/views/components/followers-window.vue:
followers: "{} のフォロワー"
desktop/views/components/followers.vue:
@ -313,18 +325,18 @@ desktop/views/components/followers.vue:
desktop/views/components/following-window.vue:
following: "{} のフォロー"
desktop/views/components/following.vue:
empty: "フォロー中のユーザーはいないようです。"
empty: "Du folgst niemanden"
desktop/views/components/friends-maker.vue:
title: "気になるユーザーをフォロー:"
empty: "おすすめのユーザーは見つかりませんでした。"
fetching: "読み込んでいます"
refresh: "もっと見る"
close: "閉じる"
title: "Wem folgen?"
empty: "Der ausgewählte Benutzer konnte nicht gefunden werden."
fetching: "Lade…"
refresh: "Mehr"
close: "Schließen"
desktop/views/components/game-window.vue:
game: "オセロ"
game: "Othello"
desktop/views/components/home.vue:
done: "完了"
add-widget: "ウィジェットを追加:"
done: "Verbunden"
add-widget: "Widget hinzufügen:"
add: "Hinzufügen"
desktop/views/input-dialog.vue:
cancel: "Abbrechen"
@ -335,9 +347,9 @@ desktop/views/components/messaging-window.vue:
title: "Nachrichten"
desktop/views/components/note-detail.vue:
more: "Lade weitere Konversationen"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
reposted-by: "{}がRenote"
private: "Dieser Post ist privat"
deleted: "Dieser Beitrag wurde entfernt"
reposted-by: "Repostet von {}"
location: "Ort"
renote: "Anmerkung"
add-reaction: "Reaktion hinzufügen"
@ -347,8 +359,8 @@ desktop/views/components/notes.note.vue:
renote: "Anmerken"
add-reaction: "Eine Reaktion hinzufügen"
detail: "Zeige Details"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
private: "Dieser Beitrag ist eine privat"
deleted: "Dieser Beitrag wurde entfernt"
desktop/views/components/notes.vue:
error: "Laden fehlgeschlagen."
retry: "Erneut versuchen"
@ -358,7 +370,7 @@ desktop/views/components/notifications.vue:
desktop/views/components/post-form.vue:
reply-placeholder: "Antworte auf diese Anmerkung..."
quote-placeholder: "Zitiere diese Anmerkung..."
submit: "投稿"
submit: "Beitragsform"
reply: "Antworten"
renote: "Anmerkung"
posted: "Gepostet!"
@ -389,56 +401,56 @@ desktop/views/components/renote-form.vue:
success: "Weitergesagt!"
failure: "Weitersagen fehlgeschlagen"
desktop/views/components/renote-form-window.vue:
title: "この投稿をRenoteしますか"
title: "Bist du dir sicher, dass du das reposten willst?"
desktop/views/components/settings-window.vue:
settings: "設定"
settings: "Experimentelles"
desktop/views/components/settings.vue:
profile: "プロフィール"
notification: "通知"
apps: "アプリ"
mute: "ミュート"
drive: "ドライブ"
security: "セキュリティ"
profile: "Profil"
notification: "Mitteilungen"
apps: "In App öffnen"
mute: "Stummschalten"
drive: "Dateien vom Drive anfügen"
security: "Sicherheit"
signin: "サインイン履歴"
password: "パスワード"
2fa: "二段階認証"
other: "その他"
license: "ライセンス"
behaviour: "動作"
fetch-on-scroll: "スクロールで自動読み込み"
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
auto-popout: "ウィンドウの自動ポップアウト"
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
advanced: "詳細設定"
api-via-stream: "ストリームを経由したAPIリクエスト"
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
display: "デザインと表示"
customize: "ホームをカスタマイズ"
dark-mode: "ダークモード"
circle-icons: "円形のアイコンを使用"
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
password: "Passwort"
2fa: "Zwei-Faktor-Authentifizierung"
other: "Anderes"
license: "Lizenz"
behaviour: "Verhalten"
fetch-on-scroll: "Aktualisieren beim scrollen"
fetch-on-scroll-desc: "Wenn du runterscrollst empfängt die Seite automatisch zusätzliche Inhalte."
auto-popout: "Automatische Pop-out Fenster"
auto-popout-desc: "Pop-out ein offenes Fenster wenn möglich. Diese Einstellung wird im Browser gespeichert."
advanced: "Erweiterte Einstellungen"
api-via-stream: "API-Anfrage via stream"
api-via-stream-desc: "API-Anfrage über WebSocket statt native Aktualisierungs-API (für bessere Leistung). Diese Einstellung wird im Browser gespeichert."
display: "Erscheinungsbild und Anzeige"
customize: "Startseite anpassen"
dark-mode: "Nacht Modus"
circle-icons: "Kreisförmige Icons"
gradient-window-header: "Übergang in Fensterköpfen"
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
show-reply-target: "リプライ先を表示する"
show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
show-renoted-my-notes: "Renoteされた自分の投稿をタイムラインに表示する"
show-maps: "マップの自動展開"
show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。"
sound: "サウンド"
enable-sounds: "サウンドを有効にする"
enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。"
volume: "ボリューム"
test: "テスト"
mobile: "モバイル"
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
language: "言語"
pick-language: "言語を選択"
recommended: "推奨"
auto: "自動"
specify-language: "言語を指定"
show-reply-target: "Zeige Antworten"
show-my-renotes: "Zeige meine Reposts auf der Zeitleiste"
show-renoted-my-notes: "Zeige meine Reposts, die geteilt wurden, auf der Zeitleiste"
show-maps: "Karte anzeigen"
show-maps-desc: "Zeige den Standort zu diesem Beitrag automatisch an."
sound: "Ton"
enable-sounds: "Ton aktivieren"
enable-sounds-desc: "Spiel einen Ton ab beim Erhalten eines Beitrags bzw. einer Nachricht. Diese Einstellung wird im Browser gespeichert."
volume: "Lautstärke"
test: "Test"
mobile: "Mobil"
disable-via-mobile: "Diesen Beitrag nicht mit 'vom Handy' absenden"
language: "Sprache"
pick-language: "Sprache auswählen"
recommended: "Empfohlen"
auto: "Automatisch"
specify-language: "Sprache auswählen"
language-desc: "変更はページの再度読み込み後に反映されます。"
cache: "キャッシュ"
clean-cache: "クリーンアップ"
cache-warn: "クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。"
cache-warn: "Der Cache deines Benutzerkontos (Info, Beiträge, Antworten, Direktnachrichten, Einstellungen), die lokal im Browser gespeichert sind werden gelöscht.\nDu musst die Seite aktualisieren nachdem du aufgeräumt hast."
cache-cleared: "キャッシュを削除しました"
cache-cleared-desc: "ページを再度読み込みしてください。"
auto-watch: "投稿の自動ウォッチ"
@ -447,9 +459,9 @@ desktop/views/components/settings.vue:
operator: "このサーバーの運営者"
update: "Misskey Update"
version: "バージョン:"
latest-version: "最新のバージョン:"
update-checking: "アップデートを確認中"
do-update: "アップデートを確認"
latest-version: "Neuste Version:"
update-checking: "Suche nach Updates"
do-update: "Suche nach Updates"
update-settings: "詳細設定"
prevent-update: "アップデートを延期する(非推奨)"
prevent-update-desc: "この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。"
@ -461,20 +473,20 @@ desktop/views/components/settings.vue:
debug-mode: "デバッグモードを有効にする"
debug-mode-desc: "この設定はブラウザに記憶されます。"
experimental: "実験的機能を有効にする"
experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。"
tools: "ツール"
task-manager: "タスクマネージャ"
experimental-desc: "Experimentelle Funktionen können die Stabilität von Misskey beeinträchtigen. Diese Einstellung wird im Browser gespeichert."
tools: "Werkzeuge"
task-manager: "Taskmanager"
third-parties: "サードパーティ"
desktop/views/components/settings.2fa.vue:
intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
detail: "詳細..."
url: "https://www.google.co.jp/intl/ja/landing/2step/"
url: "https://www.google.de/intl/de/landing/2step/"
caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。"
register: "デバイスを登録する"
already-registered: "既に設定は完了しています。"
unregister: "設定を解除"
unregistered: "二段階認証が無効になりました。"
enter-password: "パスワードを入力してください"
register: "Ein Gerät registrieren"
already-registered: "Das Gerät wurde bereits registriert"
unregister: "Abschalten"
unregistered: "Zwei-Faktor-Authentifizierung wurde deaktiviert."
enter-password: "Bitte Passwort eingeben"
authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:"
howtoinstall: "インストール方法はこちら"
scan: "次に、表示されているQRコードをスキャンします:"
@ -489,16 +501,16 @@ desktop/views/components/settings.api.vue:
regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
regenerate-token: "トークンを再生成"
token: "Token:"
enter-password: "パスワードを入力してください"
enter-password: "Bitte Passwort eingeben"
desktop/views/components/settings.apps.vue:
no-apps: "連携しているアプリケーションはありません"
desktop/views/components/settings.mute.vue:
no-users: "ミュートしているユーザーはいません"
desktop/views/components/settings.password.vue:
reset: "パスワードを変更する"
enter-current-password: "現在のパスワードを入力してください"
enter-new-password: "新しいパスワードを入力してください"
enter-new-password-again: "もう一度新しいパスワードを入力してください"
enter-current-password: "Derzeitiges Passwort eingeben"
enter-new-password: "Neues Passwort eingeben"
enter-new-password-again: "Neues Passwort erneut eingeben"
not-match: "新しいパスワードが一致しません"
changed: "パスワードを変更しました"
desktop/views/components/settings.profile.vue:
@ -515,9 +527,9 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
title: "Taskmanager"
desktop/views/components/timeline.vue:
home: "Home"
local: "Lokal"
@ -559,12 +571,19 @@ desktop/views/components/users-list.vue:
all: "すべて"
iknow: "知り合い"
load-more: "もっと"
fetching: "読み込んでいます"
fetching: "Lade…"
desktop/views/components/users-list-item.vue:
followed: "フォローされています"
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -628,7 +647,7 @@ desktop/views/widgets/notifications.vue:
title: "通知"
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
desktop/views/widgets/post-form.vue:
@ -681,7 +700,7 @@ mobile/views/components/follow-button.vue:
mobile/views/components/friends-maker.vue:
title: "気になるユーザーをフォロー"
empty: "おすすめのユーザーは見つかりませんでした。"
fetching: "読み込んでいます"
fetching: "Lade…"
refresh: "もっと見る"
close: "閉じる"
mobile/views/components/note.vue:
@ -727,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"
load-more: "もっと"
@ -785,7 +804,7 @@ mobile/views/pages/notifications.vue:
notifications: "通知"
read-all: "すべての通知を既読にしますか?"
mobile/views/pages/settings/settings.profile.vue:
title: "プロフィール"
title: "Profil"
name: "名前"
account: "アカウント"
location: "場所"
@ -795,7 +814,7 @@ mobile/views/pages/settings/settings.profile.vue:
banner: "バナー"
is-cat: "このアカウントはCatです"
save: "保存"
saved: "プロフィールを保存しました"
saved: "Profil wurde aktualisiert"
uploading: "アップロード中"
upload-failed: "アップロードに失敗しました"
mobile/views/pages/search.vue:
@ -809,7 +828,7 @@ mobile/views/pages/settings.vue:
lang-tip: "変更はページの再読み込み後に反映されます。"
recommended: "推奨"
auto: "自動"
specify-language: "言語を指定"
specify-language: "Sprache auswählen"
design: "デザインと表示"
dark-mode: "ダークモード"
i-am-under-limited-internet: "私は通信を制限されている"
@ -832,9 +851,9 @@ mobile/views/pages/settings.vue:
twitter-disconnect: "切断する"
update: "Misskey Update"
version: "バージョン:"
latest-version: "最新のバージョン:"
update-checking: "アップデートを確認中"
check-for-updates: "アップデートを確認"
latest-version: "Neuste Version:"
update-checking: "Suche nach Updates"
check-for-updates: "Suche nach Updates"
no-updates: "利用可能な更新はありません"
no-updates-desc: "お使いのMisskeyは最新です。"
update-available: "新しいバージョンが利用可能です"

View File

@ -54,9 +54,10 @@ common:
timemachine: "Calendar (Time Machine)"
activity: "Activity"
rss: "RSS reader"
memo: "Memo"
memo: "Sticky note"
trends: "Trends"
photo-stream: "Photo stream"
posts-monitor: "Chart of posts"
slideshow: "Slideshow"
version: "Version"
broadcast: "Broadcast"
@ -69,6 +70,7 @@ common:
donation: "Donation"
nav: "Navigation"
tips: "Tips"
hashtags: "Hashtags"
deck:
widgets: "Widgets"
home: "Home"
@ -76,11 +78,15 @@ common:
global: "Global"
notifications: "Notifications"
list: "List"
swap-left: "Move Left"
swap-right: "Move Right"
swap-left: "Move left"
swap-right: "Move right"
swap-up: "Move upward"
swap-down: "Move downward"
remove: "Remove"
add-column: "Add a column"
rename: "名前を変更"
rename: "Rename"
stack-left: "Stack to left"
pop-right: "Pop to right"
common/views/components/connect-failed.vue:
title: "Unable to connect to the server"
description: "There is a problem either with your Internet connection, or the server may be down or under maintenance. Please {try again} later."
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "Photostream"
no-photos: "No photos"
common/views/widgets/posts-monitor.vue:
title: "Chart of posts"
toggle: "Toggle views"
common/views/widgets/hashtags.vue:
title: "Hashtags"
count: "{} users mentioned"
common/views/widgets/server.vue:
title: "Server info"
toggle: "Toggle views"
common/views/widgets/memo.vue:
title: "Memo"
title: "Sticky note"
memo: "Write here!"
save: "Save"
desktop/views/components/activity.chart.vue:
@ -422,7 +434,7 @@ desktop/views/components/settings.vue:
show-my-renotes: "Show my reposts in the timeline"
show-renoted-my-notes: "Show my posts that have been shared in the timeline"
show-maps: "Show the map"
show-maps-desc: "Automatically show the map of the location attached to the post."
show-maps-desc: "Automatically show the location on the map attached to this post."
sound: "Sound"
enable-sounds: "Enable sound"
enable-sounds-desc: "Play a sound when you receive a post/message. This setting is stored in the browser."
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "this post is private"
deleted: "this post has been deleted"
media-count: "{} media attached"
poll: "Polls"
poll: "Poll"
desktop/views/components/taskmanager.vue:
title: "Task Manager"
desktop/views/components/timeline.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "Popout"
close: "Close"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "Only media posts"
is-media-view: "Media view"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "Reposted by {}"
private: "this post is private"
deleted: "this post has been deleted"
desktop/views/pages/welcome.vue:
about: "about"
gotit: "Got it!"

View File

@ -54,21 +54,23 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "メモ"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
@ -78,9 +80,13 @@ common:
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "サーバーに接続できません"
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "結果を見る"
voted: "投票済み"
common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です"
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}"
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "投票を破棄"
destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
common/views/components/signin.vue:
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "メモ"
title: "付箋"
memo: "ここに書いて!"
save: "保存"
desktop/views/components/activity.chart.vue:
@ -372,7 +384,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成"
create-poll: "アンケートを作成"
text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue:
note: "新規投稿"
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
desktop/views/components/timeline.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -628,7 +647,7 @@ desktop/views/widgets/notifications.vue:
title: "通知"
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
desktop/views/widgets/post-form.vue:
@ -727,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"
load-more: "もっと"

View File

@ -5,7 +5,7 @@ meta:
common:
misskey: "Une planète du fédiverse"
about-title: "Une ⭐ du fédiverse."
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
about: "Merci d'avoir découvert Misskey. Misskey est une <b>plateforme de micro-blogging distribuée</b> née sur Terre. Parce qu'il fait partie du Fédiverse (un univers composé de diverses plateformes de réseaux sociaux organisées), il est mutuellement connecté avec d'autres plateformes de réseaux sociaux. Désirez-vous prendre une pause, pendant un instant, loin de l'agitation de la ville et plonger dans un nouvel Internet ?"
time:
unknown: "inconnu"
future: "future"
@ -24,7 +24,7 @@ common:
wednesday: "M"
thursday: "J"
friday: "V"
saturday: ""
saturday: "S"
reactions:
like: "Aime"
love: "Adore"
@ -36,10 +36,10 @@ common:
confused: "Confus"
pudding: "Pudding"
note-placeholders:
a: "今どうしてる?"
b: "何かありましたか?"
a: "Que faîtes vous à cet instant ?"
b: "Quoi de neuf ?"
c: "Qu'avez-vous en tête ?"
d: "言いたいことは?"
d: "Voulez-vous exprimer quelque chose ?"
e: "Écrivez ici"
f: "En attente de vos écrits"
delete: "Supprimer"
@ -54,9 +54,10 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "Activité"
rss: "Lecteur de flux RSS"
memo: "Note"
memo: "Pense-bête"
trends: "Tendances"
photo-stream: "Flux de photos"
posts-monitor: "Graph des publications"
slideshow: "Diaporama"
version: "Version"
broadcast: "Diffusion"
@ -69,6 +70,7 @@ common:
donation: "Dons"
nav: "Navigation"
tips: "Conseils"
hashtags: "ハッシュタグ"
deck:
widgets: "Widgets"
home: "Accueil"
@ -78,9 +80,13 @@ common:
list: "Liste"
swap-left: "Déplacer à gauche"
swap-right: "Déplacer à droite"
swap-up: "Vers le haut"
swap-down: "Vers le bas"
remove: "Supprimer"
add-column: "Ajouter une colonne"
rename: "名前を変更"
rename: "Renommer"
stack-left: "Vers la gauche"
pop-right: "Vers la droite"
common/views/components/connect-failed.vue:
title: "Impossible de se connecter au server."
description: "Il y a soit un problème avec votre connexion internet, soit le serveur est hors-ligne ou en maintenance. Veuillez {ressayer} plus tard."
@ -104,7 +110,7 @@ common/views/components/connect-failed.troubleshooter.vue:
success: "Connexion au serveur de Misskey reussie!"
success-desc: "La connexion au serveur a été reussie. Veuillez recharger la page."
flush: "Vider le cache"
set-version: "バージョン指定"
set-version: "Choisissez une version"
common/views/components/messaging.vue:
search-user: "Trouver un utilisateur"
you: "Vous"
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "Montrer les résultats"
voted: "Voté"
common/views/components/poll-editor.vue:
no-only-one-choice: "Vous devez entrer au moins deux choix"
no-only-one-choice: "Vous devez saisir au moins deux choix."
choice-n: "Choix {}"
remove: "Supprimer ce choix"
add: "+ Ajouter un choix"
destroy: "Supprimer ce sondage"
destroy: "Annuler ce sondage"
common/views/components/reaction-picker.vue:
choose-reaction: "Choisissez votre réaction"
common/views/components/signin.vue:
@ -199,7 +205,7 @@ common/views/components/uploader.vue:
common/views/components/visibility-chooser.vue:
public: "Public"
home: "Accueil"
home-desc: "ホームタイムラインにのみ公開"
home-desc: "Publier sur le fil d'Accueil uniquement"
followers: "Abonnés"
followers-desc: "Publier à vos abonnés uniquement"
specified: "Direct"
@ -216,18 +222,24 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "Flux de photo"
no-photos: "Pas de photos"
common/views/widgets/posts-monitor.vue:
title: "Graph des publications"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "Info sur le serveur"
toggle: "Afficher les vues"
common/views/widgets/memo.vue:
title: "Note"
title: "Pense-bête"
memo: "Écrivez ici !"
save: "Enregistrer"
desktop/views/components/activity.chart.vue:
total: "Black ... Total"
notes: "Blue ... Notes"
replies: "Red ... Replies"
renotes: "Green ... Renotes"
notes: "Bleu ... Notes"
replies: "Rouge ... Réponses"
renotes: "Vert ... Partages"
desktop/views/components/activity.vue:
title: "Activitié"
toggle: "Afficher les vues"
@ -304,7 +316,7 @@ desktop/views/components/drive.vue:
desktop/views/components/follow-button.vue:
following: "Abonnements"
follow: "Suivre"
request-pending: "フォロー許可待ち"
request-pending: "En attente d'approbation"
follow-request: "Demande d'abonnement"
desktop/views/components/followers-window.vue:
followers: "{} abonnés"
@ -316,14 +328,14 @@ desktop/views/components/following.vue:
empty: "Vous ne suivez aucun compte."
desktop/views/components/friends-maker.vue:
title: "Utilisateurs recommandés :"
empty: "おすすめのユーザーは見つかりませんでした。"
empty: "Impossible de trouver des utilisateurs à recommander."
fetching: "Chargement"
refresh: "Plus"
close: "Fermer"
desktop/views/components/game-window.vue:
game: "Othello"
desktop/views/components/home.vue:
done: "完了"
done: "Envoyer"
add-widget: "Ajouter un widget"
add: "Ajouter"
desktop/views/input-dialog.vue:
@ -337,14 +349,14 @@ desktop/views/components/note-detail.vue:
more: "Charger davantage de conversations"
private: "cette publication est privée"
deleted: "cette publication a été supprimée"
reposted-by: "{}がRenote"
reposted-by: "Republié par {}"
location: "Géolocalisation"
renote: "Renote"
renote: "Republier"
add-reaction: "Ajouter votre reaction"
desktop/views/components/notes.note.vue:
reposted-by: "Reposté par {}"
reply: "Répondre"
renote: "Renote"
renote: "Republier"
add-reaction: "Ajouter votre reaction"
detail: "Afficher les détails"
private: "cette publication est privée"
@ -360,7 +372,7 @@ desktop/views/components/post-form.vue:
quote-placeholder: "Citer cette note"
submit: "Poster"
reply: "Répondre"
renote: "Renote"
renote: "Republier"
posted: "Posté!"
replied: "Répondu!"
reposted: "Reposté!"
@ -384,7 +396,7 @@ desktop/views/components/progress-dialog.vue:
desktop/views/components/renote-form.vue:
quote: "Citer..."
cancel: "Annuler"
renote: "Renote"
renote: "Republier"
reposting: "Repost en cours..."
success: "Reposté!"
failure: "La renote a échoué"
@ -405,25 +417,25 @@ desktop/views/components/settings.vue:
other: "Autres"
license: "License"
behaviour: "Comportement"
fetch-on-scroll: "スクロールで自動読み込み"
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
auto-popout: "ウィンドウの自動ポップアウト"
fetch-on-scroll: "Chargement lors du défilement"
fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page."
auto-popout: "Fenêtre contextuelle automatique"
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
advanced: "Paramètres avancés"
api-via-stream: "ストリームを経由したAPIリクエスト"
api-via-stream: "Requête API via le flux"
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
display: "デザインと表示"
display: "Affichage et design"
customize: "Personnaliser l'Accueil"
dark-mode: "Mode nuit"
circle-icons: "Utiliser des icônes circulaires"
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
show-reply-target: "リプライ先を表示する"
show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
show-my-renotes: "Afficher mes republications dans le fil"
show-renoted-my-notes: "Renoteされた自分の投稿をタイムラインに表示する"
show-maps: "Afficher la carte"
show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。"
sound: "サウンド"
sound: "Son"
enable-sounds: "Activer le son"
enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。"
volume: "Volume"
@ -439,17 +451,17 @@ desktop/views/components/settings.vue:
cache: "Cache"
clean-cache: "Nettoyage"
cache-warn: "クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。"
cache-cleared: "キャッシュを削除しました"
cache-cleared-desc: "ページを再度読み込みしてください。"
cache-cleared: "Cache nettoyé"
cache-cleared-desc: "Veuillez recharger la page."
auto-watch: "投稿の自動ウォッチ"
auto-watch-desc: "リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。"
about: "À propose de Misskey"
operator: "このサーバーの運営者"
operator: "L'admin de cette instance"
update: "Mise à jour de Misskey"
version: "Version :"
latest-version: "Dernière version :"
update-checking: "Recherche de mises à jour"
do-update: "アップデートを確認"
do-update: "Rechercher des mises à jour"
update-settings: "Paramètres avancés"
prevent-update: "アップデートを延期する(非推奨)"
prevent-update-desc: "この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。"
@ -458,13 +470,13 @@ desktop/views/components/settings.vue:
update-available: "Nouvelle version disponible !"
update-available-desc: "ページを再度読み込みすると更新が適用されます。"
advanced-settings: "Réglages avancés"
debug-mode: "デバッグモードを有効にする"
debug-mode-desc: "この設定はブラウザに記憶されます。"
experimental: "実験的機能を有効にする"
debug-mode: "Activer le mode debug"
debug-mode-desc: "Ce paramètre est stocké dans le navigateur."
experimental: "Activer les fonctionnalités expérimentales"
experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。"
tools: "Outils"
task-manager: "Gestionnaire de tâches"
third-parties: "サードパーティ"
third-parties: "Services tiers"
desktop/views/components/settings.2fa.vue:
intro: "Si vous configurez la vérication en deux étapes vous aurez non seulement besoin de votre mot de passe mais aussi un appareil déjà pré-enregistré(tel que votre smartphone) ce qui ameliora grandement la sécurité de votre compte."
detail: "Voir les détails..."
@ -491,7 +503,7 @@ desktop/views/components/settings.api.vue:
token: "Jeton :"
enter-password: "Veuillez entrer le mot de passe"
desktop/views/components/settings.apps.vue:
no-apps: "連携しているアプリケーションはありません"
no-apps: "Aucune application autorisée"
desktop/views/components/settings.mute.vue:
no-users: "Aucun utilisateurs mis en sourdine"
desktop/views/components/settings.password.vue:
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "cette publication est privée"
deleted: "cette publication a été supprimée"
media-count: "{} médias attachés"
poll: "Sondages"
poll: "Sondage"
desktop/views/components/taskmanager.vue:
title: "Gestionnaire de tâches"
desktop/views/components/timeline.vue:
@ -553,7 +565,7 @@ desktop/views/components/user-lists-window.vue:
create-list: "Créer une liste"
desktop/views/components/user-preview.vue:
notes: "Publications"
following: "フォロー"
following: "Abonné à"
followers: "Abonnés"
desktop/views/components/users-list.vue:
all: "Tout"
@ -565,13 +577,20 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "Fermer"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "Reposté par {}"
private: "cette publication est privée"
deleted: "cette publication a été supprimée"
desktop/views/pages/welcome.vue:
about: "à propos"
gotit: "J'ai compris !"
signin: "Connexion"
signup: "S'enregistrer"
signin-button: "Se connecter"
signup-button: "やる"
signup-button: "S'inscrire"
timeline: "Fil d'actualité"
desktop/views/pages/drive.vue:
title: "Lecteur de Misskey"
@ -655,7 +674,7 @@ mobile/views/components/drive.vue:
nothing-in-drive: "Rien"
folder-is-empty: "Ce dossier est vide"
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
deletion-alert: "Désolé ! La suppression dun dossier nest pas encore implémentée."
folder-name: "Nom du dossier"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
@ -679,7 +698,7 @@ mobile/views/components/follow-button.vue:
request-pending: "フォロー許可待ち"
follow-request: "Demande d'abonnement"
mobile/views/components/friends-maker.vue:
title: "気になるユーザーをフォロー"
title: "Abonnez-vous aux utilisateurs"
empty: "おすすめのユーザーは見つかりませんでした。"
fetching: "Chargement"
refresh: "Voir plus"
@ -715,34 +734,34 @@ mobile/views/components/notifications.vue:
mobile/views/components/post-form.vue:
add-visible-user: "Ajouter un utilisateur"
submit: "Poster"
reply: "返信"
renote: "Renote"
quote-placeholder: "この投稿を引用... (オプション)"
reply: "Répondre"
renote: "Republier"
quote-placeholder: "Citer ce billet ... (Facultatif)"
reply-placeholder: "Répondre à cette note"
cw-placeholder: "内容への注釈 (オプション)"
location-alert: "お使いの端末は位置情報に対応していません"
error: "エラー"
username-prompt: "ユーザー名を入力してください"
location-alert: "Votre appareil ne prend pas en charge les services de localisation"
error: "Erreur"
username-prompt: "Saisir un nom d'utilisateur"
mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "Sondage"
private: "cette publication est privée"
deleted: "cette publication a été supprimée"
media-count: "{} médias attachés"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "Pas de notes"
load-more: "Afficher plus"
mobile/views/components/ui.nav.vue:
timeline: "タイムライン"
timeline: "Fil d'actualité"
notifications: "Notifications"
messaging: "Messages"
follow-requests: "フォロー申請"
follow-requests: "Demandes d'abonnement"
search: "Rechercher"
drive: "Drive"
favorites: "お気に入り"
user-lists: "リスト"
widgets: "ウィジェット"
game: "ゲーム"
darkmode: "ダークモード"
favorites: "Favoris"
user-lists: "Listes"
widgets: "Modules"
game: "Jeux"
darkmode: "Mode nuit"
settings: "Réglages"
about: "À propose de Misskey"
mobile/views/components/user-timeline.vue:
@ -757,10 +776,10 @@ mobile/views/pages/favorites.vue:
title: "Favoris"
mobile/views/pages/user-lists.vue:
title: "Listes"
enter-list-name: "リスト名を入力してください"
enter-list-name: "Nom de la liste"
mobile/views/pages/drive.vue:
drive: "Drive"
more: "もっと見る"
more: "Afficher plus ..."
mobile/views/pages/followers.vue:
followers-of: "Abonnés de {}"
mobile/views/pages/following.vue:
@ -806,7 +825,7 @@ mobile/views/pages/selectdrive.vue:
mobile/views/pages/settings.vue:
signed-in-as: "Connecté en tant que {}"
lang: "Langue"
lang-tip: "変更はページの再読み込み後に反映されます。"
lang-tip: "Le rechargement de la page est requis afin d'appliquer les modifications."
recommended: "Recommandé"
auto: "Automatique"
specify-language: "Spécifier la langue"
@ -822,7 +841,7 @@ mobile/views/pages/settings.vue:
post-style-standard: "Standard"
post-style-smart: "Intelligent"
behavior: "Comportement"
fetch-on-scroll: "スクロールで自動読み込み"
fetch-on-scroll: "Chargement lors du défilement"
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
load-raw-images: "Afficher les photos jointes en haute qualité"
load-remote-media: "Afficher les médias sur le serveur distant"

View File

@ -54,21 +54,23 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "メモ"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
@ -78,9 +80,13 @@ common:
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "サーバーに接続できません"
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "結果を見る"
voted: "投票済み"
common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です"
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}"
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "投票を破棄"
destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
common/views/components/signin.vue:
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "メモ"
title: "付箋"
memo: "ここに書いて!"
save: "保存"
desktop/views/components/activity.chart.vue:
@ -372,7 +384,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成"
create-poll: "アンケートを作成"
text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue:
note: "新規投稿"
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
desktop/views/components/timeline.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -628,7 +647,7 @@ desktop/views/widgets/notifications.vue:
title: "通知"
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
desktop/views/widgets/post-form.vue:
@ -727,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"
load-more: "もっと"

View File

@ -60,21 +60,23 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "メモ"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
@ -85,9 +87,13 @@ common:
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "サーバーに接続できません"
@ -163,11 +169,11 @@ common/views/components/poll.vue:
voted: "投票済み"
common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です"
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}"
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "投票を破棄"
destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
@ -245,12 +251,21 @@ common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
empty: "トレンドなし"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "メモ"
title: "付箋"
memo: "ここに書いて!"
save: "保存"
@ -429,7 +444,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成"
create-poll: "アンケートを作成"
text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue:
@ -595,7 +610,7 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
@ -659,6 +674,15 @@ desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -738,7 +762,7 @@ desktop/views/widgets/notifications.vue:
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
@ -856,7 +880,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"

View File

@ -54,21 +54,23 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "メモ"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
@ -78,9 +80,13 @@ common:
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "サーバーに接続できません"
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "結果を見る"
voted: "投票済み"
common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です"
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}"
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "投票を破棄"
destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
common/views/components/signin.vue:
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "メモ"
title: "付箋"
memo: "ここに書いて!"
save: "保存"
desktop/views/components/activity.chart.vue:
@ -372,7 +384,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成"
create-poll: "アンケートを作成"
text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue:
note: "新規投稿"
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
desktop/views/components/timeline.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -628,7 +647,7 @@ desktop/views/widgets/notifications.vue:
title: "通知"
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
desktop/views/widgets/post-form.vue:
@ -727,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"
load-more: "もっと"

View File

@ -5,7 +5,7 @@ meta:
common:
misskey: "Planeta Fediwersum"
about-title: "⭐ Fediwersum"
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
about: "Dziękujemy za znalezienie Misskey. Misskey jest <b>zdecentralizowaną platformą mikroblogową</b> powstałą na Ziemi. Ponieważ działa ona w Fediwersum (uniwersum, w którego skład wchodzi wiele sieci społecznościowych), jest ona połączona z innymi platformami społecznościowymi. Spróbujesz odpocząć od zatłoczoneo miasta i zanurzyć się w nowym Internecie?"
time:
unknown: "nieznany"
future: "w przyszłości"
@ -54,9 +54,10 @@ common:
timemachine: "Kalendarz (wehikuł czasu)"
activity: "Aktywność"
rss: "Czytnik RSS"
memo: "Notatki"
memo: "Notatka"
trends: "Na czasie"
photo-stream: "Photostream"
posts-monitor: "Wykres wpisów"
slideshow: "Pokaz slajdów"
version: "Wersja"
broadcast: "Transmisja"
@ -69,6 +70,7 @@ common:
donation: "Dotacje"
nav: "Nawigacja"
tips: "Wskazówki"
hashtags: "Hashtagi"
deck:
widgets: "Widżety"
home: "Strona główna"
@ -78,9 +80,13 @@ common:
list: "Listy"
swap-left: "Przesuń w lewo"
swap-right: "Przesuń w prawo"
swap-up: "Przenieś w górę"
swap-down: "Przenieś w dół"
remove: "Usuń"
add-column: "Dodaj kolumnę"
rename: "名前を変更"
rename: "Zmień nazwę"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "Nie udało się połączyć z serwerem"
description: "Wystąpił problem z Twoim połączeniem z Internetem, lub z serwerem. {Spróbuj ponownie} wkrótce."
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "Pokaż wyniki"
voted: "Zagłosowano"
common/views/components/poll-editor.vue:
no-only-one-choice: "Musisz wprowadzić dwie lub więcej opcji."
no-only-one-choice: "Musisz wprowadzić przynajmniej dwie opcje."
choice-n: "Opcja {}"
remove: "Usuń tą opcję"
add: "+ Dodaj opcję"
destroy: "Usuń ankietę"
destroy: "Usuń ankietę"
common/views/components/reaction-picker.vue:
choose-reaction: "Wybierz reakcję"
common/views/components/signin.vue:
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "Photostream"
no-photos: "Brak zdjęć"
common/views/widgets/posts-monitor.vue:
title: "Wykres wpisów"
toggle: "Przełącz widok"
common/views/widgets/hashtags.vue:
title: "Hashtagi"
count: "Wspomniany przez {} użytkowników"
common/views/widgets/server.vue:
title: "Informacje o serwerze"
toggle: "Przełącz widok"
common/views/widgets/memo.vue:
title: "Notatki"
title: "Notatka"
memo: "Napisz tutaj!"
save: "Zapisz"
desktop/views/components/activity.chart.vue:
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "ten wpis jest prywatny"
deleted: "ten wpis został usunięty"
media-count: "{}zawartości multimedialnej"
poll: "Ankiety"
poll: "Ankieta"
desktop/views/components/taskmanager.vue:
title: "Menedżer zadań"
desktop/views/components/timeline.vue:
@ -535,7 +547,7 @@ desktop/views/components/ui.header.account.vue:
dark: "Sprowadź ciemność"
desktop/views/components/ui.header.nav.vue:
home: "Strona główna"
deck: "デッキ"
deck: "Talia"
messaging: "Wiadomości"
game: "Gra"
desktop/views/components/ui.header.notifications.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "Pop-out"
close: "Zamknij"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "Tylko wpisy z zawartością multimedialną"
is-media-view: "Widok multimediów"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "Udostępniono przez {}"
private: "ten wpis jest prywatny"
deleted: "ten wpis został usunięty"
desktop/views/pages/welcome.vue:
about: "O Misskey"
gotit: "Rozumiem!"

View File

@ -54,21 +54,23 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "メモ"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
@ -78,9 +80,13 @@ common:
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "サーバーに接続できません"
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "結果を見る"
voted: "投票済み"
common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です"
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}"
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "投票を破棄"
destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
common/views/components/signin.vue:
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "メモ"
title: "付箋"
memo: "ここに書いて!"
save: "保存"
desktop/views/components/activity.chart.vue:
@ -372,7 +384,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成"
create-poll: "アンケートを作成"
text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue:
note: "新規投稿"
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
desktop/views/components/timeline.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -628,7 +647,7 @@ desktop/views/widgets/notifications.vue:
title: "通知"
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
desktop/views/widgets/post-form.vue:
@ -727,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"
load-more: "もっと"

View File

@ -54,21 +54,23 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "メモ"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
@ -78,9 +80,13 @@ common:
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "サーバーに接続できません"
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "結果を見る"
voted: "投票済み"
common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です"
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}"
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "投票を破棄"
destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
common/views/components/signin.vue:
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "メモ"
title: "付箋"
memo: "ここに書いて!"
save: "保存"
desktop/views/components/activity.chart.vue:
@ -372,7 +384,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成"
create-poll: "アンケートを作成"
text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue:
note: "新規投稿"
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
desktop/views/components/timeline.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -628,7 +647,7 @@ desktop/views/widgets/notifications.vue:
title: "通知"
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
desktop/views/widgets/post-form.vue:
@ -727,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"
load-more: "もっと"

View File

@ -54,21 +54,23 @@ common:
timemachine: "カレンダー(タイムマシン)"
activity: "アクティビティ"
rss: "RSSリーダー"
memo: "メモ"
memo: "付箋"
trends: "トレンド"
photo-stream: "フォトストリーム"
posts-monitor: "投稿チャート"
slideshow: "スライドショー"
version: "バージョン"
broadcast: "ブロードキャスト"
notifications: "通知"
users: "おすすめユーザー"
polls: "投票"
polls: "アンケート"
post-form: "投稿フォーム"
messaging: "メッセージ"
server: "サーバー情報"
donation: "寄付のお願い"
nav: "ナビゲーション"
tips: "ヒント"
hashtags: "ハッシュタグ"
deck:
widgets: "ウィジェット"
home: "ホーム"
@ -78,9 +80,13 @@ common:
list: "リスト"
swap-left: "左に移動"
swap-right: "右に移動"
swap-up: "上に移動"
swap-down: "下に移動"
remove: "カラムを削除"
add-column: "カラムを追加"
rename: "名前を変更"
stack-left: "左に重ねる"
pop-right: "右に出す"
common/views/components/connect-failed.vue:
title: "サーバーに接続できません"
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
@ -146,11 +152,11 @@ common/views/components/poll.vue:
show-result: "結果を見る"
voted: "投票済み"
common/views/components/poll-editor.vue:
no-only-one-choice: "投票には、選択肢が最低2つ必要です"
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
choice-n: "選択肢{}"
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "投票を破棄"
destroy: "アンケートを破棄"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
common/views/components/signin.vue:
@ -216,11 +222,17 @@ common/views/widgets/donation.vue:
common/views/widgets/photo-stream.vue:
title: "フォトストリーム"
no-photos: "写真はありません"
common/views/widgets/posts-monitor.vue:
title: "投稿チャート"
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"
common/views/widgets/memo.vue:
title: "メモ"
title: "付箋"
memo: "ここに書いて!"
save: "保存"
desktop/views/components/activity.chart.vue:
@ -372,7 +384,7 @@ desktop/views/components/post-form.vue:
attach-media-from-drive: "ドライブからメディアを添付"
attach-cancel: "添付取り消し"
insert-a-kao: "v(‘ω’)v"
create-poll: "投票を作成"
create-poll: "アンケートを作成"
text-remain: "残り{}文字"
desktop/views/components/post-form-window.vue:
note: "新規投稿"
@ -515,7 +527,7 @@ desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
desktop/views/components/timeline.vue:
@ -565,6 +577,13 @@ desktop/views/components/users-list-item.vue:
desktop/views/components/window.vue:
popout: "ポップアウト"
close: "閉じる"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"
desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"
@ -628,7 +647,7 @@ desktop/views/widgets/notifications.vue:
title: "通知"
settings: "通知の設定"
desktop/views/widgets/polls.vue:
title: "投票"
title: "アンケート"
refresh: "他を見る"
nothing: "ありません!"
desktop/views/widgets/post-form.vue:
@ -727,7 +746,7 @@ mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
media-count: "{}つのメディア"
poll: "投票"
poll: "アンケート"
mobile/views/components/timeline.vue:
empty: "投稿がありません"
load-more: "もっと"

View File

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "2.29.1",
"clientVersion": "1.0.6218",
"version": "2.37.7",
"clientVersion": "1.0.6474",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@ -29,12 +29,71 @@
"@fortawesome/fontawesome-free-solid": "5.0.2",
"@koa/cors": "2.2.1",
"@prezzemolo/rap": "0.1.2",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"cafy": "8.0.0",
"chalk": "2.4.1",
"crc-32": "1.2.0",
"debug": "3.1.0",
"deepcopy": "0.6.3",
"diskusage": "0.2.4",
"elasticsearch": "15.0.0",
"emojilib": "2.2.12",
"escape-regexp": "0.0.1",
"file-type": "8.0.0",
"gm": "1.23.1",
"http-signature": "1.2.0",
"is-root": "2.0.0",
"is-url": "1.2.4",
"js-yaml": "3.11.0",
"jsdom": "11.11.0",
"koa": "2.5.1",
"koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0",
"koa-favicon": "2.0.1",
"koa-json-body": "5.3.0",
"koa-logger": "3.2.0",
"koa-mount": "3.0.0",
"koa-multer": "1.0.2",
"koa-router": "7.4.0",
"koa-send": "4.1.3",
"koa-slow": "2.1.0",
"koa-views": "6.1.4",
"kue": "0.11.6",
"mongodb": "3.0.10",
"monk": "6.0.6",
"ms": "2.1.1",
"nopt": "4.0.1",
"os-utils": "0.0.14",
"parse5": "5.0.0",
"prominence": "0.2.0",
"promise-sequential": "1.1.1",
"punycode": "2.1.1",
"qrcode": "1.2.0",
"ratelimiter": "3.0.3",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2",
"redis": "2.8.0",
"request": "2.87.0",
"request-promise-native": "1.0.5",
"rndstr": "1.0.0",
"speakeasy": "2.0.0",
"summaly": "2.0.6",
"tcp-port-used": "0.1.2",
"tmp": "0.0.33",
"uuid": "3.2.1",
"web-push": "3.3.1",
"webfinger.js": "2.6.6",
"websocket": "1.0.26",
"ws": "5.2.0",
"xev": "2.0.1"
},
"devDependencies": {
"@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.1",
"@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1",
"@types/elasticsearch": "5.0.23",
"@types/eventemitter3": "2.0.2",
"@types/gm": "1.18.0",
"@types/gulp": "3.8.36",
"@types/gulp-htmlmin": "1.3.32",
@ -63,7 +122,6 @@
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.0",
"@types/mongodb": "3.0.18",
"@types/monk": "6.0.0",
"@types/ms": "0.7.30",
"@types/node": "10.1.2",
"@types/nopt": "3.0.29",
@ -86,30 +144,17 @@
"@types/ws": "5.1.1",
"animejs": "2.2.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"bootstrap-vue": "2.0.0-rc.6",
"cafy": "8.0.0",
"chalk": "2.4.1",
"crc-32": "1.2.0",
"css-loader": "0.28.11",
"debug": "3.1.0",
"deep-equal": "1.0.1",
"deepcopy": "0.6.3",
"diskusage": "0.2.4",
"dompurify": "1.0.4",
"elasticsearch": "15.0.0",
"element-ui": "2.3.9",
"emojilib": "2.2.12",
"escape-regexp": "0.0.1",
"eslint": "4.19.1",
"eslint-plugin-vue": "4.5.0",
"eventemitter3": "3.1.0",
"exif-js": "2.3.0",
"file-loader": "1.1.11",
"file-type": "8.0.0",
"fuckadblock": "3.2.1",
"gm": "1.23.1",
"gulp": "3.9.1",
"gulp-cssnano": "2.1.3",
"gulp-htmlmin": "4.0.0",
@ -127,71 +172,32 @@
"hard-source-webpack-plugin": "0.6.10",
"highlight.js": "9.12.0",
"html-minifier": "3.5.16",
"http-signature": "1.2.0",
"inquirer": "5.2.0",
"is-root": "2.0.0",
"is-url": "1.2.4",
"js-yaml": "3.11.0",
"jsdom": "11.11.0",
"koa": "2.5.1",
"koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0",
"koa-favicon": "2.0.1",
"koa-json-body": "5.3.0",
"koa-logger": "3.2.0",
"koa-mount": "3.0.0",
"koa-multer": "1.0.2",
"koa-router": "7.4.0",
"koa-send": "4.1.3",
"koa-slow": "2.1.0",
"koa-views": "6.1.4",
"kue": "0.11.6",
"license-checker": "20.0.0",
"loader-utils": "1.1.0",
"mecab-async": "0.1.2",
"mkdirp": "0.5.1",
"mocha": "5.2.0",
"moji": "0.5.1",
"mongodb": "3.0.8",
"monk": "6.0.6",
"ms": "2.1.1",
"nan": "2.10.0",
"node-sass": "4.9.0",
"node-sass-json-importer": "3.2.0",
"nopt": "4.0.1",
"nprogress": "0.2.0",
"object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0",
"os-utils": "0.0.14",
"parse5": "5.0.0",
"progress-bar-webpack-plugin": "1.11.0",
"prominence": "0.2.0",
"promise-sequential": "1.1.1",
"pug": "2.0.3",
"punycode": "2.1.1",
"qrcode": "1.2.0",
"ratelimiter": "3.0.3",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "3.2.2",
"redis": "2.8.0",
"request": "2.87.0",
"request-promise-native": "1.0.5",
"rimraf": "2.6.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass-loader": "7.0.1",
"seedrandom": "2.4.3",
"single-line-log": "1.1.2",
"speakeasy": "2.0.0",
"style-loader": "0.21.0",
"stylus": "0.54.5",
"stylus-loader": "3.0.2",
"summaly": "2.0.6",
"swagger-jsdoc": "1.9.7",
"syuilo-password-strength": "0.0.1",
"tcp-port-used": "0.1.2",
"textarea-caret": "3.1.0",
"tmp": "0.0.33",
"ts-loader": "4.3.0",
"ts-node": "6.0.4",
"tslint": "5.10.0",
@ -199,7 +205,6 @@
"typescript-eslint-parser": "15.0.0",
"uglify-es": "3.3.9",
"url-loader": "1.0.1",
"uuid": "3.2.1",
"v-animate-css": "0.0.2",
"vue": "2.5.16",
"vue-cropperjs": "2.2.0",
@ -212,12 +217,7 @@
"vuedraggable": "2.16.0",
"vuex": "3.0.1",
"vuex-persistedstate": "^2.5.4",
"web-push": "3.3.1",
"webfinger.js": "2.6.6",
"webpack": "4.9.1",
"webpack-cli": "2.1.4",
"websocket": "1.0.26",
"ws": "5.2.0",
"xev": "2.0.0"
"webpack-cli": "2.1.4"
}
}

View File

@ -42,7 +42,7 @@ html
| JavaScriptを有効にしてください
br
| Please turn on your JavaScript
div#ini: p
span .
span .
span .
div#ini.
<svg viewBox="0 0 50 50">
<path fill=#{themeColor} 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>

View File

@ -3,15 +3,15 @@ import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Server stream connection
* Notes stats stream connection
*/
export class ServerStream extends Stream {
export class NotesStatsStream extends Stream {
constructor(os: MiOS) {
super(os, 'server');
super(os, 'notes-stats');
}
}
export class ServerStreamManager extends StreamManager<ServerStream> {
export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> {
private os: MiOS;
constructor(os: MiOS) {
@ -22,7 +22,7 @@ export class ServerStreamManager extends StreamManager<ServerStream> {
public getConnection() {
if (this.connection == null) {
this.connection = new ServerStream(this.os);
this.connection = new NotesStatsStream(this.os);
}
return this.connection;

View File

@ -0,0 +1,30 @@
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
* Server stats stream connection
*/
export class ServerStatsStream extends Stream {
constructor(os: MiOS) {
super(os, 'server-stats');
}
}
export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> {
private os: MiOS;
constructor(os: MiOS) {
super();
this.os = os;
}
public getConnection() {
if (this.connection == null) {
this.connection = new ServerStatsStream(this.os);
}
return this.connection;
}
}

View File

@ -2,6 +2,7 @@ import Vue from 'vue';
import analogClock from './analog-clock.vue';
import menu from './menu.vue';
import noteHeader from './note-header.vue';
import signin from './signin.vue';
import signup from './signup.vue';
import forkit from './forkit.vue';
@ -31,6 +32,7 @@ import welcomeTimeline from './welcome-timeline.vue';
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
Vue.component('mk-note-header', noteHeader);
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit);

View File

@ -1,10 +1,10 @@
<template>
<div class="mk-menu">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact }" ref="popover">
<div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item in items">
<div v-if="item == null"></div>
<button v-else @click="clicked(item.onClick)" v-html="item.content"></button>
<div v-if="item === null"></div>
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text"></button>
</template>
</div>
</div>
@ -15,7 +15,25 @@ import Vue from 'vue';
import * as anime from 'animejs';
export default Vue.extend({
props: ['source', 'compact', 'items'],
props: {
source: {
required: true
},
items: {
type: Array,
required: true
},
compact: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
hukidasi: !this.compact
};
},
mounted() {
this.$nextTick(() => {
const popover = this.$refs.popover as any;
@ -24,18 +42,34 @@ export default Vue.extend({
const width = popover.offsetWidth;
const height = popover.offsetHeight;
let left;
let top;
if (this.compact) {
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
popover.style.left = (x - (width / 2)) + 'px';
popover.style.top = (y - (height / 2)) + 'px';
left = (x - (width / 2));
top = (y - (height / 2));
} else {
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
popover.style.left = (x - (width / 2)) + 'px';
popover.style.top = y + 'px';
left = (x - (width / 2));
top = y;
}
if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset;
this.hukidasi = false;
}
if (top + height - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - height + window.pageYOffset;
this.hukidasi = false;
}
popover.style.left = left + 'px';
popover.style.top = top + 'px';
anime({
targets: this.$refs.backdrop,
opacity: 1,
@ -113,14 +147,18 @@ $border-color = rgba(27, 31, 35, 0.15)
$balloon-size = 16px
&:not(.compact)
&.hukidasi
margin-top $balloon-size
transform-origin center -($balloon-size)
&:before
&:after
content ""
display block
position absolute
pointer-events none
&:before
top -($balloon-size * 2)
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent
@ -129,9 +167,6 @@ $border-color = rgba(27, 31, 35, 0.15)
border-bottom solid $balloon-size $border-color
&:after
content ""
display block
position absolute
top -($balloon-size * 2) + 1.5px
left s('calc(50% - %s)', $balloon-size)
border-top solid $balloon-size transparent

View File

@ -0,0 +1,117 @@
<template>
<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
<span class="is-admin" v-if="note.user.isAdmin">admin</span>
<span class="is-bot" v-if="note.user.isBot">bot</span>
<span class="is-cat" v-if="note.user.isCat">cat</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="app" v-if="note.app && !mini">via <b>{{ note.app.name }}</b></span>
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'">%fa:home%</template>
<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
<template v-if="note.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
note: {
type: Object,
required: true
},
mini: {
type: Boolean,
required: false,
default: false
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display flex
align-items baseline
white-space nowrap
> .avatar
flex-shrink 0
margin-right 8px
width 20px
height 20px
border-radius 100%
> .name
display block
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #627079
font-size 1em
font-weight bold
text-decoration none
text-overflow ellipsis
&:hover
text-decoration underline
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 .5em 0 0
padding 1px 6px
font-size 80%
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #c0c0c0
> .mobile
margin-right 8px
> .app
margin-right 8px
padding-right 8px
border-right solid 1px isDark ? #1c2023 : #eaeaea
> .visibility
margin-left 8px
.bvonvjxbwzaiskogyhbwgyxvcgserpmu[data-darkmode]
root(true)
.bvonvjxbwzaiskogyhbwgyxvcgserpmu:not([data-darkmode])
root(false)
</style>

View File

@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
ast = this.ast;
}
if (ast.filter(x => x.type != 'hashtag').length == 0) {
return;
}
while (
ast[ast.length - 1].type == 'hashtag' ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n')) {
ast.pop();
}
// Parse ast to DOM
const els = flatten(ast.map(token => {
switch (token.type) {
@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
case 'hashtag':
return createElement('a', {
attrs: {
href: `${url}/search?q=${token.content}`,
href: `${url}/tags/${token.content}`,
target: '_blank'
}
}, token.content);

View File

@ -1,6 +1,6 @@
<template>
<div class="mk-note-menu" style="position:initial">
<mk-menu ref="menu" :source="source" :compact="compact" :items="items" @closed="$destroy"/>
<div style="position:initial">
<mk-menu :source="source" :compact="compact" :items="items" @closed="closed"/>
</div>
</template>
@ -13,23 +13,27 @@ export default Vue.extend({
items() {
const items = [];
items.push({
content: '%i18n:@favorite%',
onClick: this.favorite
icon: '%fa:star%',
text: '%i18n:@favorite%',
action: this.favorite
});
if (this.note.userId == this.$store.state.i.id) {
items.push({
content: '%i18n:@pin%',
onClick: this.pin
icon: '%fa:thumbtack%',
text: '%i18n:@pin%',
action: this.pin
});
items.push({
content: '%i18n:@delete%',
onClick: this.del
icon: '%fa:trash-alt R%',
text: '%i18n:@delete%',
action: this.del
});
}
if (this.note.uri) {
items.push({
content: '%i18n:@remote%',
onClick: () => {
icon: '%fa:external-link-square-alt%',
text: '%i18n:@remote%',
action: () => {
window.open(this.note.uri, '_blank');
}
});
@ -63,8 +67,10 @@ export default Vue.extend({
});
},
close() {
this.$refs.menu.close();
closed() {
this.$nextTick(() => {
this.$destroy();
});
}
}
});

View File

@ -58,18 +58,21 @@ export default Vue.extend({
},
created() {
if (this.mode == 'relative' || this.mode == 'detail') {
this.tick();
this.tickId = setInterval(this.tick, 5000);
this.tickId = window.requestAnimationFrame(this.tick);
}
},
destroyed() {
if (this.mode === 'relative' || this.mode === 'detail') {
clearInterval(this.tickId);
window.clearTimeout(this.tickId);
}
},
methods: {
tick() {
this.now = new Date();
this.tickId = setTimeout(() => {
window.requestAnimationFrame(this.tick);
}, 10000);
}
}
});

View File

@ -203,6 +203,7 @@ root(isDark)
justify-content center
align-items center
margin-right 10px
width 16px
> *:last-child
flex 1 1 auto

View File

@ -0,0 +1,89 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
<defs>
<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="polygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="polylinePoints"
fill="none"
stroke="#fff"
stroke-width="2"/>
<circle
:cx="headX"
:cy="headY"
r="3"
fill="#fff"/>
</mask>
</defs>
<rect
x="-10" y="-10"
:width="viewBoxX + 20" :height="viewBoxY + 20"
:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
import * as uuid from 'uuid';
export default Vue.extend({
props: {
src: {
type: Array,
required: true
}
},
data() {
return {
viewBoxX: 50,
viewBoxY: 30,
gradientId: uuid(),
maskId: uuid(),
polylinePoints: '',
polygonPoints: '',
headX: null,
headY: null,
clock: null
};
},
watch: {
src() {
this.draw();
}
},
created() {
this.draw();
// Vueが何故かWatchを発動させない場合があるので
this.clock = setInterval(this.draw, 1000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
draw() {
const stats = this.src.slice().reverse();
const peak = Math.max.apply(null, stats) || 1;
const polylinePoints = stats.map((n, i) => [
i * (this.viewBoxX / (stats.length - 1)),
(1 - (n / peak)) * this.viewBoxY
]);
this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.headX = polylinePoints[polylinePoints.length - 1][0];
this.headY = polylinePoints[polylinePoints.length - 1][1];
}
}
});
</script>

View File

@ -0,0 +1,123 @@
<template>
<div class="mkw-hashtags">
<mk-widget-container :show-header="!props.compact">
<template slot="header">%fa:hashtag%%i18n:@title%</template>
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
<transition-group v-else tag="div" name="chart">
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
</div>
</transition-group>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import XChart from './hashtags.chart.vue';
export default define({
name: 'hashtags',
props: () => ({
compact: false
})
}).extend({
components: {
XChart
},
data() {
return {
stats: [],
fetching: true,
clock: null
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
fetch() {
(this as any).api('hashtags/trend').then(stats => {
this.stats = stats;
this.fetching = false;
});
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
.mkw-hashtags--body
> .fetching
> .empty
margin 0
padding 16px
text-align center
color #aaa
> [data-fa]
margin-right 4px
> div
.chart-enter
.chart-leave-to
opacity 0
transform translateY(-30px)
> *
transition transform .3s ease, opacity .3s ease
> div
display flex
align-items center
padding 14px 16px
&:not(:last-child)
border-bottom solid 1px isDark ? #393f4f : #eee
> .tag
flex 1
overflow hidden
font-size 14px
color isDark ? #9baec8 : #65727b
> a
display block
width 100%
white-space nowrap
overflow hidden
text-overflow ellipsis
color inherit
> p
margin 0
font-size 75%
opacity 0.7
> .chart
height 30px
.mkw-hashtags[data-darkmode]
root(true)
.mkw-hashtags:not([data-darkmode])
root(false)
</style>

View File

@ -4,6 +4,7 @@ import wAnalogClock from './analog-clock.vue';
import wVersion from './version.vue';
import wRss from './rss.vue';
import wServer from './server.vue';
import wPostsMonitor from './posts-monitor.vue';
import wMemo from './memo.vue';
import wBroadcast from './broadcast.vue';
import wCalendar from './calendar.vue';
@ -12,6 +13,7 @@ import wSlideshow from './slideshow.vue';
import wTips from './tips.vue';
import wDonation from './donation.vue';
import wNav from './nav.vue';
import wHashtags from './hashtags.vue';
Vue.component('mkw-analog-clock', wAnalogClock);
Vue.component('mkw-nav', wNav);
@ -22,6 +24,8 @@ Vue.component('mkw-tips', wTips);
Vue.component('mkw-donation', wDonation);
Vue.component('mkw-broadcast', wBroadcast);
Vue.component('mkw-server', wServer);
Vue.component('mkw-posts-monitor', wPostsMonitor);
Vue.component('mkw-memo', wMemo);
Vue.component('mkw-rss', wRss);
Vue.component('mkw-version', wVersion);
Vue.component('mkw-hashtags', wHashtags);

View File

@ -0,0 +1,211 @@
<template>
<div class="mkw-posts-monitor">
<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
<template slot="header">%fa:chart-line%%i18n:@title%</template>
<button slot="func" @click="toggle" title="%i18n:@toggle%">%fa:sort%</button>
<div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }" :data-darkmode="$store.state.device.darkmode">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2">
<defs>
<linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="localPolygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="localPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"/>
<circle
:cx="localHeadX"
:cy="localHeadY"
r="1.5"
fill="#fff"/>
</mask>
</defs>
<rect
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/>
<text x="1" y="5">Local</text>
</svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 1">
<defs>
<linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
</linearGradient>
<mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="fediPolygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="fediPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="1"/>
<circle
:cx="fediHeadX"
:cy="fediHeadY"
r="1.5"
fill="#fff"/>
</mask>
</defs>
<rect
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/>
<text x="1" y="5">Fedi</text>
</svg>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import * as uuid from 'uuid';
export default define({
name: 'server',
props: () => ({
design: 0,
view: 0
})
}).extend({
data() {
return {
connection: null,
connectionId: null,
viewBoxY: 30,
stats: [],
fediGradientId: uuid(),
fediMaskId: uuid(),
localGradientId: uuid(),
localMaskId: uuid(),
fediPolylinePoints: '',
localPolylinePoints: '',
fediPolygonPoints: '',
localPolygonPoints: '',
fediHeadX: null,
fediHeadY: null,
localHeadX: null,
localHeadY: null
};
},
computed: {
viewBoxX(): number {
return this.props.view == 0 ? 50 : 100;
}
},
watch: {
viewBoxX() {
this.draw();
}
},
mounted() {
this.connection = (this as any).os.streams.notesStatsStream.getConnection();
this.connectionId = (this as any).os.streams.notesStatsStream.use();
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send({
type: 'requestLog',
id: Math.random().toString()
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
(this as any).os.streams.notesStatsStream.dispose(this.connectionId);
},
methods: {
toggle() {
if (this.props.view == 2) {
this.props.view = 0;
} else {
this.props.view++;
}
this.save();
},
func() {
if (this.props.design == 2) {
this.props.design = 0;
} else {
this.props.design++;
}
this.save();
},
draw() {
const stats = this.props.view == 0 ? this.stats.slice(-50) : this.stats;
const fediPeak = Math.max.apply(null, stats.map(x => x.all)) || 1;
const localPeak = Math.max.apply(null, stats.map(x => x.local)) || 1;
const fediPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.all / fediPeak)) * this.viewBoxY]);
const localPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.local / localPeak)) * this.viewBoxY]);
this.fediPolylinePoints = fediPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.localPolylinePoints = localPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.fediPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.localPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.fediHeadX = fediPolylinePoints[fediPolylinePoints.length - 1][0];
this.fediHeadY = fediPolylinePoints[fediPolylinePoints.length - 1][1];
this.localHeadX = localPolylinePoints[localPolylinePoints.length - 1][0];
this.localHeadY = localPolylinePoints[localPolylinePoints.length - 1][1];
},
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
this.draw();
},
onStatsLog(statsLog) {
statsLog.forEach(stats => this.onStats(stats));
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
&.dual
> svg
width 50%
float left
&:first-child
padding-right 5px
&:last-child
padding-left 5px
> svg
display block
padding 10px
width 100%
> text
font-size 5px
fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
> tspan
opacity 0.5
&:after
content ""
display block
clear both
.qpdmibaztplkylerhdbllwcokyrfxeyj[data-darkmode]
root(true)
.qpdmibaztplkylerhdbllwcokyrfxeyj:not([data-darkmode])
root(false)
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="cpu-memory">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
@ -16,15 +16,20 @@
fill="none"
stroke="#fff"
stroke-width="1"/>
<circle
:cx="cpuHeadX"
:cy="cpuHeadY"
r="1.5"
fill="#fff"/>
</mask>
</defs>
<rect
x="-1" y="-1"
:width="viewBoxX + 2" :height="viewBoxY + 2"
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
</svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
@ -40,11 +45,16 @@
fill="none"
stroke="#fff"
stroke-width="1"/>
<circle
:cx="memHeadX"
:cy="memHeadY"
r="1.5"
fill="#fff"/>
</mask>
</defs>
<rect
x="-1" y="-1"
:width="viewBoxX + 2" :height="viewBoxY + 2"
x="-2" y="-2"
:width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
</svg>
@ -70,15 +80,25 @@ export default Vue.extend({
memPolylinePoints: '',
cpuPolygonPoints: '',
memPolygonPoints: '',
cpuHeadX: null,
cpuHeadY: null,
memHeadX: null,
memHeadY: null,
cpuP: '',
memP: ''
};
},
mounted() {
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send({
type: 'requestLog',
id: Math.random().toString()
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
},
methods: {
onStats(stats) {
@ -86,14 +106,24 @@ export default Vue.extend({
this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift();
this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' ');
this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' ');
const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]);
const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]);
this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
this.cpuP = (stats.cpu_usage * 100).toFixed(0);
this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
},
onStatsLog(statsLog) {
statsLog.forEach(stats => this.onStats(stats));
}
}
});

View File

@ -55,11 +55,11 @@ export default define({
this.fetching = false;
});
this.connection = (this as any).os.streams.serverStream.getConnection();
this.connectionId = (this as any).os.streams.serverStream.use();
this.connection = (this as any).os.streams.serverStatsStream.getConnection();
this.connectionId = (this as any).os.streams.serverStatsStream.use();
},
beforeDestroy() {
(this as any).os.streams.serverStream.dispose(this.connectionId);
(this as any).os.streams.serverStatsStream.dispose(this.connectionId);
},
methods: {
toggle() {

View File

@ -33,6 +33,7 @@ import MkHomeCustomize from './views/pages/home-customize.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
import MkSearch from './views/pages/search.vue';
import MkTag from './views/pages/tag.vue';
import MkOthello from './views/pages/othello.vue';
/**
@ -60,6 +61,7 @@ init(async (launch) => {
{ path: '/i/lists/:list', component: MkUserList },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
{ path: '/othello', component: MkOthello },
{ path: '/othello/:game', component: MkOthello },
{ path: '/@:user', component: MkUser },

View File

@ -1,5 +1,5 @@
<template>
<svg viewBox="0 0 21 7" preserveAspectRatio="none">
<svg viewBox="0 0 21 7">
<rect v-for="record in data" class="day"
width="1" height="1"
:x="record.x" :y="record.date.weekday"
@ -15,7 +15,7 @@
style="pointer-events: none;"/>
<rect class="today"
width="1" height="1"
:x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday"
:x="data[0].x" :y="data[0].date.weekday"
rx="1" ry="1"
fill="none"
stroke-width="0.1"
@ -33,7 +33,7 @@ export default Vue.extend({
const peak = Math.max.apply(null, this.data.map(d => d.total));
let x = 0;
this.data.reverse().forEach(d => {
this.data.slice().reverse().forEach(d => {
d.x = x;
d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();

View File

@ -1,5 +1,5 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
<title>%i18n:@total%<br/>%i18n:@notes%<br/>%i18n:@replies%<br/>%i18n:@renotes%</title>
<polyline
:points="pointsNote"
@ -55,7 +55,6 @@ export default Vue.extend({
};
},
created() {
this.data.reverse();
this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
this.render();
},
@ -63,10 +62,11 @@ export default Vue.extend({
render() {
const peak = Math.max.apply(null, this.data.map(d => d.total));
if (peak != 0) {
this.pointsNote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
this.pointsRenote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
const data = this.data.slice().reverse();
this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
}
},
onMousedown(e) {

View File

@ -1,15 +1,17 @@
<template>
<ul class="menu">
<li v-for="(item, i) in menu" :class="item.type">
<template v-if="item.type == 'item'">
<p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
</template>
<template v-if="item.type == 'link'">
<a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
</template>
<template v-else-if="item.type == 'nest'">
<p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
<me-nu :menu="item.menu" @x="click"/>
<li v-for="(item, i) in menu" :class="item ? item.type : item === null ? 'divider' : null">
<template v-if="item">
<template v-if="item.type == null || item.type == 'item'">
<p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
</template>
<template v-else-if="item.type == 'link'">
<a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
</template>
<template v-else-if="item.type == 'nest'">
<p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
<me-nu :menu="item.menu" @x="click"/>
</template>
</template>
</li>
</ul>

View File

@ -1,5 +1,5 @@
<template>
<div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}">
<div class="context-menu" @contextmenu.prevent="() => {}">
<x-menu :menu="menu" @x="click"/>
</div>
</template>
@ -17,6 +17,23 @@ export default Vue.extend({
props: ['x', 'y', 'menu'],
mounted() {
this.$nextTick(() => {
const width = this.$el.offsetWidth;
const height = this.$el.offsetHeight;
let x = this.x;
let y = this.y;
if (x + width - window.pageXOffset > window.innerWidth) {
x = window.innerWidth - width + window.pageXOffset;
}
if (y + height - window.pageYOffset > window.innerHeight) {
y = window.innerHeight - height + window.pageYOffset;
}
this.$el.style.left = x + 'px';
this.$el.style.top = y + 'px';
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.addEventListener('mousedown', this.onMousedown);
});
@ -38,7 +55,7 @@ export default Vue.extend({
return false;
},
click(item) {
if (item.onClick) item.onClick();
if (item.action) item.action();
this.close();
},
close() {
@ -59,7 +76,6 @@ root(isDark)
$item-height = 38px
$padding = 10px
display none
position fixed
top 0
left 0

View File

@ -66,37 +66,33 @@ export default Vue.extend({
type: 'item',
text: '%i18n:@contextmenu.rename%',
icon: '%fa:i-cursor%',
onClick: this.rename
action: this.rename
}, {
type: 'item',
text: '%i18n:@contextmenu.copy-url%',
icon: '%fa:link%',
onClick: this.copyUrl
action: this.copyUrl
}, {
type: 'link',
href: `${this.file.url}?download`,
text: '%i18n:@contextmenu.download%',
icon: '%fa:download%',
}, {
type: 'divider',
}, {
}, null, {
type: 'item',
text: '%i18n:common.delete%',
icon: '%fa:R trash-alt%',
onClick: this.deleteFile
}, {
type: 'divider',
}, {
action: this.deleteFile
}, null, {
type: 'nest',
text: '%i18n:@contextmenu.else-files%',
menu: [{
type: 'item',
text: '%i18n:@contextmenu.set-as-avatar%',
onClick: this.setAsAvatar
action: this.setAsAvatar
}, {
type: 'item',
text: '%i18n:@contextmenu.set-as-banner%',
onClick: this.setAsBanner
action: this.setAsBanner
}]
}, {
type: 'nest',
@ -104,7 +100,7 @@ export default Vue.extend({
menu: [{
type: 'item',
text: '%i18n:@contextmenu.add-app%...',
onClick: this.addApp
action: this.addApp
}]
}], {
closed: () => {

View File

@ -56,26 +56,22 @@ export default Vue.extend({
type: 'item',
text: '%i18n:@contextmenu.move-to-this-folder%',
icon: '%fa:arrow-right%',
onClick: this.go
action: this.go
}, {
type: 'item',
text: '%i18n:@contextmenu.show-in-new-window%',
icon: '%fa:R window-restore%',
onClick: this.newWindow
}, {
type: 'divider',
}, {
action: this.newWindow
}, null, {
type: 'item',
text: '%i18n:@contextmenu.rename%',
icon: '%fa:i-cursor%',
onClick: this.rename
}, {
type: 'divider',
}, {
action: this.rename
}, null, {
type: 'item',
text: '%i18n:common.delete%',
icon: '%fa:R trash-alt%',
onClick: this.deleteFolder
action: this.deleteFolder
}], {
closed: () => {
this.isContextmenuShowing = false;

View File

@ -140,17 +140,17 @@ export default Vue.extend({
type: 'item',
text: '%i18n:@contextmenu.create-folder%',
icon: '%fa:R folder%',
onClick: this.createFolder
action: this.createFolder
}, {
type: 'item',
text: '%i18n:@contextmenu.upload%',
icon: '%fa:upload%',
onClick: this.selectLocalFile
action: this.selectLocalFile
}, {
type: 'item',
text: '%i18n:@contextmenu.url-upload%',
icon: '%fa:cloud-upload-alt%',
onClick: this.urlUpload
action: this.urlUpload
}]);
},

View File

@ -23,6 +23,8 @@
<option value="post-form">%i18n:common.widgets.post-form%</option>
<option value="messaging">%i18n:common.widgets.messaging%</option>
<option value="memo">%i18n:common.widgets.memo%</option>
<option value="hashtags">%i18n:common.widgets.hashtags%</option>
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
<option value="server">%i18n:common.widgets.server%</option>
<option value="donation">%i18n:common.widgets.donation%</option>
<option value="nav">%i18n:common.widgets.nav%</option>

View File

@ -48,7 +48,7 @@
<mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div>

View File

@ -1,23 +1,8 @@
<template>
<div class="mk-note-preview" :title="title">
<mk-avatar class="avatar" :user="note.user"/>
<mk-avatar class="avatar" :user="note.user" v-if="!mini"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'">%fa:home%</template>
<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
<template v-if="note.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@ -30,7 +15,17 @@ import Vue from 'vue';
import dateStringify from '../../../common/scripts/date-stringify';
export default Vue.extend({
props: ['note'],
props: {
note: {
type: Object,
required: true
},
mini: {
type: Boolean,
required: false,
default: false
}
},
computed: {
title(): string {
return dateStringify(this.note.createdAt);
@ -56,43 +51,6 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
white-space nowrap
> .name
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #607073
font-size 1em
font-weight bold
text-decoration none
text-overflow ellipsis
&:hover
text-decoration underline
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #d1d8da
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #b2b8bb
> .mobile
margin-right 6px
> .visibility
margin-left 6px
> .body
> .text

View File

@ -2,25 +2,7 @@
<div class="sub" :title="title">
<mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
<span class="is-admin" v-if="note.user.isAdmin">admin</span>
<span class="is-bot" v-if="note.user.isBot">bot</span>
<span class="is-cat" v-if="note.user.isCat">cat</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'">%fa:home%</template>
<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
<template v-if="note.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="note"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@ -62,57 +44,8 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
> .header
margin-bottom 2px
white-space nowrap
> .name
display block
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #607073
font-size 1em
font-weight bold
text-decoration none
text-overflow ellipsis
&:hover
text-decoration underline
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 0.5em 0 0
padding 1px 5px
font-size 10px
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
margin 0 .5em 0 0
color isDark ? #606984 : #d1d8da
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #b2b8bb
> .mobile
margin-right 6px
> .visibility
margin-left 6px
> .body

View File

@ -14,26 +14,7 @@
<article>
<mk-avatar class="avatar" :user="p.user"/>
<div class="main">
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
<span class="is-admin" v-if="p.user.isAdmin">admin</span>
<span class="is-bot" v-if="p.user.isBot">bot</span>
<span class="is-cat" v-if="p.user.isCat">cat</span>
<span class="username"><mk-acct :user="p.user"/></span>
<div class="info">
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>
<span class="visibility" v-if="p.visibility != 'public'">
<template v-if="p.visibility == 'home'">%fa:home%</template>
<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
<template v-if="p.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="p"/>
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
@ -52,7 +33,7 @@
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
@ -409,64 +390,8 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
> .header
margin-bottom 4px
white-space nowrap
> .name
display block
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #627079
font-size 1em
font-weight bold
text-decoration none
text-overflow ellipsis
&:hover
text-decoration underline
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 .5em 0 0
padding 1px 6px
font-size 12px
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #c0c0c0
> .mobile
margin-right 8px
> .app
margin-right 8px
padding-right 8px
border-right solid 1px isDark ? #1c2023 : #eaeaea
> .visibility
margin-left 8px
> .body

View File

@ -206,7 +206,7 @@ root(isDark)
margin 0
padding 16px
overflow-wrap break-word
font-size 12px
font-size 13px
border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
&:last-child

View File

@ -17,7 +17,11 @@ export default Vue.extend({
},
methods: {
onSubmit() {
location.href = `/search?q=${encodeURIComponent(this.q)}`;
if (this.q.startsWith('#')) {
this.$router.push(`/tags/${encodeURIComponent(this.q.substr(1))}`);
} else {
this.$router.push(`/search?q=${encodeURIComponent(this.q)}`);
}
}
}
});

View File

@ -1,6 +1,6 @@
<template>
<div class="mk-ui" :style="style">
<x-header class="header"/>
<x-header class="header" v-show="!zenMode"/>
<div class="content">
<slot></slot>
</div>
@ -16,6 +16,11 @@ export default Vue.extend({
components: {
XHeader
},
data() {
return {
zenMode: false
};
},
computed: {
style(): any {
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
@ -39,6 +44,11 @@ export default Vue.extend({
e.preventDefault();
(this as any).apis.post();
}
if (e.which == 90) { // z
e.preventDefault();
this.zenMode = !this.zenMode;
}
}
}
});

View File

@ -0,0 +1,35 @@
<template>
<x-widgets-column v-if="column.type == 'widgets'" :column="column" :is-stacked="isStacked"/>
<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
</template>
<script lang="ts">
import Vue from 'vue';
import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-column.vue';
import XWidgetsColumn from './deck.widgets-column.vue';
export default Vue.extend({
components: {
XTlColumn,
XNotificationsColumn,
XWidgetsColumn
},
props: {
column: {
type: Object,
required: true
},
isStacked: {
type: Boolean,
required: false,
default: false
}
}
});
</script>

View File

@ -1,10 +1,22 @@
<template>
<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow }">
<header :class="{ indicate }">
<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow, active, isStacked, draghover, dragging, dropready }"
@dragover.prevent.stop="onDragover"
@dragenter.prevent="onDragenter"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
>
<header :class="{ indicate: count > 0 }"
draggable="true"
@click="toggleActive"
@dragstart="onDragstart"
@dragend="onDragend"
@contextmenu.prevent.stop="onContextmenu"
>
<slot name="header"></slot>
<button ref="menu" @click="showMenu">%fa:caret-down%</button>
<span class="count" v-if="count > 0">({{ count }})</span>
<button ref="menu" @click.stop="showMenu">%fa:caret-down%</button>
</header>
<div ref="body">
<div ref="body" v-show="active">
<slot></slot>
</div>
</div>
@ -13,12 +25,17 @@
<script lang="ts">
import Vue from 'vue';
import Menu from '../../../../common/views/components/menu.vue';
import contextmenu from '../../../api/contextmenu';
export default Vue.extend({
props: {
id: {
type: String,
required: false
column: {
type: Object,
required: true
},
isStacked: {
type: Boolean,
required: true
},
name: {
type: String,
@ -26,7 +43,8 @@ export default Vue.extend({
},
menu: {
type: Array,
required: false
required: false,
default: null
},
naked: {
type: Boolean,
@ -40,30 +58,69 @@ export default Vue.extend({
}
},
inject: {
getColumnVm: { from: 'getColumnVm' }
},
data() {
return {
indicate: false
count: 0,
active: true,
dragging: false,
draghover: false,
dropready: false
};
},
watch: {
active(v) {
if (v && this.isScrollTop()) {
this.$emit('top');
}
},
dragging(v) {
this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd');
}
},
provide() {
return {
column: this,
isScrollTop: this.isScrollTop,
indicate: v => this.indicate = v
count: v => this.count = v
};
},
mounted() {
this.$refs.body.addEventListener('scroll', this.onScroll, { passive: true });
this.$root.$on('deck.column.dragStart', this.onOtherDragStart);
this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd);
},
beforeDestroy() {
this.$refs.body.removeEventListener('scroll', this.onScroll);
this.$root.$off('deck.column.dragStart', this.onOtherDragStart);
this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd);
},
methods: {
onOtherDragStart() {
this.dropready = true;
},
onOtherDragEnd() {
this.dropready = false;
},
toggleActive() {
if (!this.isStacked) return;
const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id));
if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return;
this.active = !this.active;
},
isScrollTop() {
return this.$refs.body.scrollTop == 0;
return this.active && this.$refs.body.scrollTop == 0;
},
onScroll() {
@ -77,32 +134,60 @@ export default Vue.extend({
}
},
showMenu() {
getMenu() {
const items = [{
content: '%fa:pencil-alt% %i18n:common.deck.rename%',
onClick: () => {
icon: '%fa:pencil-alt%',
text: '%i18n:common.deck.rename%',
action: () => {
(this as any).apis.input({
title: '%i18n:common.deck.rename%',
default: this.name,
allowEmpty: false
}).then(name => {
this.$store.dispatch('settings/renameDeckColumn', { id: this.id, name });
this.$store.dispatch('settings/renameDeckColumn', { id: this.column.id, name });
});
}
}, null, {
content: '%fa:arrow-left% %i18n:common.deck.swap-left%',
onClick: () => {
this.$store.dispatch('settings/swapLeftDeckColumn', this.id);
icon: '%fa:arrow-left%',
text: '%i18n:common.deck.swap-left%',
action: () => {
this.$store.dispatch('settings/swapLeftDeckColumn', this.column.id);
}
}, {
content: '%fa:arrow-right% %i18n:common.deck.swap-right%',
onClick: () => {
this.$store.dispatch('settings/swapRightDeckColumn', this.id);
icon: '%fa:arrow-right%',
text: '%i18n:common.deck.swap-right%',
action: () => {
this.$store.dispatch('settings/swapRightDeckColumn', this.column.id);
}
}, null, {
content: '%fa:trash-alt R% %i18n:common.deck.remove%',
onClick: () => {
this.$store.dispatch('settings/removeDeckColumn', this.id);
}, this.isStacked ? {
icon: '%fa:arrow-up%',
text: '%i18n:common.deck.swap-up%',
action: () => {
this.$store.dispatch('settings/swapUpDeckColumn', this.column.id);
}
} : undefined, this.isStacked ? {
icon: '%fa:arrow-down%',
text: '%i18n:common.deck.swap-down%',
action: () => {
this.$store.dispatch('settings/swapDownDeckColumn', this.column.id);
}
} : undefined, null, {
icon: '%fa:window-restore R%',
text: '%i18n:common.deck.stack-left%',
action: () => {
this.$store.dispatch('settings/stackLeftDeckColumn', this.column.id);
}
}, this.isStacked ? {
icon: '%fa:window-maximize R%',
text: '%i18n:common.deck.pop-right%',
action: () => {
this.$store.dispatch('settings/popRightDeckColumn', this.column.id);
}
} : undefined, null, {
icon: '%fa:trash-alt R%',
text: '%i18n:common.deck.remove%',
action: () => {
this.$store.dispatch('settings/removeDeckColumn', this.column.id);
}
}];
@ -111,11 +196,63 @@ export default Vue.extend({
this.menu.reverse().forEach(i => items.unshift(i));
}
return items;
},
onContextmenu(e) {
contextmenu((this as any).os)(e, this.getMenu());
},
showMenu() {
this.os.new(Menu, {
source: this.$refs.menu,
compact: false,
items
items: this.getMenu()
});
},
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk-deck-column', this.column.id);
this.dragging = true;
},
onDragend(e) {
this.dragging = false;
},
onDragover(e) {
// 自分自身がドラッグされている場合
if (this.dragging) {
// 自分自身にはドロップさせない
e.dataTransfer.dropEffect = 'none';
return;
}
const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column';
e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
},
onDragenter() {
if (!this.dragging) this.draghover = true;
},
onDragleave() {
this.draghover = false;
},
onDrop(e) {
this.draghover = false;
this.$root.$emit('deck.column.dragEnd');
const id = e.dataTransfer.getData('mk-deck-column');
if (id != null && id != '') {
this.$store.dispatch('settings/swapDeckColumn', {
a: this.column.id,
b: id
});
}
}
}
});
@ -128,14 +265,30 @@ root(isDark)
$header-height = 42px
width 330px
min-width 330px
height 100%
background isDark ? #282C37 : #fff
border-radius 6px
box-shadow 0 2px 16px rgba(#000, 0.1)
overflow hidden
&.narrow
&.draghover
box-shadow 0 0 0 2px rgba($theme-color, 0.8)
&.dragging
box-shadow 0 0 0 2px rgba($theme-color, 0.4)
&.dropready
*
pointer-events none
&:not(.active)
flex-basis $header-height
min-height $header-height
&:not(.isStacked).narrow
width 285px
min-width 285px
&.naked
background rgba(#000, isDark ? 0.25 : 0.1)
@ -156,6 +309,13 @@ root(isDark)
color isDark ? #e3e5e8 : #888
background isDark ? #313543 : #fff
box-shadow 0 1px rgba(#000, 0.15)
cursor pointer
&, *
user-select none
*:not(button)
pointer-events none
&.indicate
box-shadow 0 3px 0 0 $theme-color
@ -164,12 +324,17 @@ root(isDark)
[data-fa]
margin-right 8px
> .count
margin-left 4px
opacity 0.5
> button
position absolute
top 0
right 0
width $header-height
line-height $header-height
font-size 16px
color isDark ? #9baec8 : #ccc
&:hover

View File

@ -1,5 +1,5 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null"/>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
</template>
<script lang="ts">
@ -23,6 +23,11 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
},
mediaView: {
type: Boolean,
required: false,
default: false
}
},

View File

@ -2,25 +2,7 @@
<div class="fnlfosztlhtptnongximhlbykxblytcq">
<mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span>
<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span>
<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'">%fa:home%</template>
<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
<template v-if="note.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@ -72,66 +54,8 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
> .header
margin-bottom 2px
white-space nowrap
> .avatar
flex-shrink 0
margin-right 8px
width 18px
height 18px
border-radius 100%
> .name
display block
margin 0 0.5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #607073
font-size 1em
font-weight 700
text-align left
text-decoration none
text-overflow ellipsis
&:hover
text-decoration underline
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 0.5em 0 0
padding 1px 5px
font-size 0.8em
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
text-align left
margin 0
color isDark ? #606984 : #d1d8da
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #b2b8bb
> .mobile
margin-right 6px
> .visibility
margin-left 6px
> .body

View File

@ -1,5 +1,5 @@
<template>
<div class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }">
<div v-if="!mediaView" class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }">
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
@ -14,25 +14,7 @@
<article>
<mk-avatar class="avatar" :user="p.user"/>
<div class="main">
<header>
<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
<span class="is-admin" v-if="p.user.isAdmin">admin</span>
<span class="is-bot" v-if="p.user.isBot">bot</span>
<span class="is-cat" v-if="p.user.isCat">cat</span>
<span class="username"><mk-acct :user="p.user"/></span>
<div class="info">
<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>
<span class="visibility" v-if="p.visibility != 'public'">
<template v-if="p.visibility == 'home'">%fa:home%</template>
<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
<template v-if="p.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="p" :mini="true"/>
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
@ -51,11 +33,11 @@
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
<mk-note-preview :note="p.renote" :mini="true"/>
</div>
</div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
@ -73,6 +55,14 @@
</div>
</article>
</div>
<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi">
<div v-if="note.media.length > 0">
<mk-media-list :media-list="note.media"/>
</div>
<div v-if="note.renote && note.renote.media.length > 0">
<mk-media-list :media-list="note.renote.media"/>
</div>
</div>
</template>
<script lang="ts">
@ -89,7 +79,17 @@ export default Vue.extend({
XSub
},
props: ['note'],
props: {
note: {
type: Object,
required: true
},
mediaView: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
@ -217,8 +217,18 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
mediaRoot(isDark)
font-size 13px
margin 4px 12px
&:first-child
margin-top 12px
&:last-child
margin-bottom 12px
root(isDark)
font-size 12px
font-size 13px
border-bottom solid 1px isDark ? #1c2023 : #eaeaea
&:last-of-type
@ -234,7 +244,7 @@ root(isDark)
> .renote
display flex
align-items center
padding 8px 16px
padding 8px 16px 0 16px
line-height 28px
white-space pre
color #9dbb00
@ -275,7 +285,7 @@ root(isDark)
> article
display flex
padding 16px 16px 9px
padding 16px 16px 4px
> .avatar
flex-shrink 0
@ -292,62 +302,6 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
white-space nowrap
> .avatar
flex-shrink 0
margin-right 8px
width 20px
height 20px
border-radius 100%
> .name
display block
margin 0 0.5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #627079
font-weight bold
text-decoration none
text-overflow ellipsis
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 0.5em 0 0
padding 1px 6px
font-size 0.8em
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
margin 0 0.5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #c0c0c0
> .mobile
margin-right 6px
> .visibility
margin-left 6px
> .body
> .cw
@ -482,7 +436,7 @@ root(isDark)
> footer
> button
margin 0
padding 8px
padding 4px 8px 8px 8px
background transparent
border none
box-shadow none
@ -510,4 +464,10 @@ root(isDark)
.zyjjkidcqjnlegkqebitfviomuqmseqk:not([data-darkmode])
root(false)
.srwrkujossgfuhrbnvqkybtzxpblgchi[data-darkmode]
mediaRoot(true)
.srwrkujossgfuhrbnvqkybtzxpblgchi:not([data-darkmode])
mediaRoot(false)
</style>

View File

@ -9,7 +9,7 @@
<transition-group name="mk-notes" class="transition">
<template v-for="(note, i) in _notes">
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
@ -28,24 +28,27 @@
<script lang="ts">
import Vue from 'vue';
import { url } from '../../../config';
import getNoteSummary from '../../../../../renderers/get-note-summary';
import XNote from './deck.note.vue';
const displayLimit = 30;
const displayLimit = 20;
export default Vue.extend({
components: {
XNote
},
inject: ['column', 'isScrollTop', 'indicate'],
inject: ['column', 'isScrollTop', 'count'],
props: {
more: {
type: Function,
required: false
},
mediaView: {
type: Boolean,
required: false,
default: false
}
},
@ -55,7 +58,6 @@ export default Vue.extend({
requestInitPromise: null as () => Promise<any[]>,
notes: [],
queue: [],
unreadCount: 0,
fetching: true,
moreFetching: false
};
@ -73,6 +75,12 @@ export default Vue.extend({
}
},
watch: {
queue(q) {
this.count(q.length);
}
},
created() {
this.column.$on('top', this.onTop);
this.column.$on('bottom', this.onBottom);
@ -141,7 +149,6 @@ export default Vue.extend({
}
} else {
this.queue.push(note);
this.indicate(true);
}
},
@ -156,7 +163,6 @@ export default Vue.extend({
releaseQueue() {
this.queue.forEach(n => this.prepend(n, true));
this.queue = [];
this.indicate(false);
},
async loadMore() {

View File

@ -112,7 +112,7 @@ export default Vue.extend({
root(isDark)
> .notification
padding 16px
font-size 12px
font-size 13px
overflow-wrap break-word
&:after

View File

@ -1,11 +1,9 @@
<template>
<div>
<x-column :id="column.id" :name="name">
<span slot="header">%fa:bell R%{{ name }}</span>
<x-column :name="name" :column="column" :is-stacked="isStacked">
<span slot="header">%fa:bell R%{{ name }}</span>
<x-notifications/>
</x-column>
</div>
<x-notifications/>
</x-column>
</template>
<script lang="ts">
@ -23,6 +21,10 @@ export default Vue.extend({
column: {
type: Object,
required: true
},
isStacked: {
type: Boolean,
required: true
}
},

View File

@ -21,20 +21,27 @@
import Vue from 'vue';
import XNotification from './deck.notification.vue';
const displayLimit = 20;
export default Vue.extend({
components: {
XNotification
},
inject: ['column', 'isScrollTop', 'count'],
data() {
return {
fetching: true,
fetchingMoreNotifications: false,
notifications: [],
queue: [],
moreNotifications: false,
connection: null,
connectionId: null
};
},
computed: {
_notifications(): any[] {
return (this.notifications as any).map(notification => {
@ -46,12 +53,22 @@ export default Vue.extend({
});
}
},
watch: {
queue(q) {
this.count(q.length);
}
},
mounted() {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
this.connection.on('notification', this.onNotification);
this.column.$on('top', this.onTop);
this.column.$on('bottom', this.onBottom);
const max = 10;
(this as any).api('i/notifications', {
@ -66,15 +83,20 @@ export default Vue.extend({
this.fetching = false;
});
},
beforeDestroy() {
this.connection.off('notification', this.onNotification);
(this as any).os.stream.dispose(this.connectionId);
this.column.$off('top', this.onTop);
this.column.$off('bottom', this.onBottom);
},
methods: {
fetchMoreNotifications() {
this.fetchingMoreNotifications = true;
const max = 30;
const max = 20;
(this as any).api('i/notifications', {
limit: max + 1,
@ -90,6 +112,7 @@ export default Vue.extend({
this.fetchingMoreNotifications = false;
});
},
onNotification(notification) {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
this.connection.send({
@ -97,7 +120,34 @@ export default Vue.extend({
id: notification.id
});
this.notifications.unshift(notification);
this.prepend(notification);
},
prepend(notification) {
if (this.isScrollTop()) {
// Prepend the notification
this.notifications.unshift(notification);
// オーバーフローしたら古い通知は捨てる
if (this.notifications.length >= displayLimit) {
this.notifications = this.notifications.slice(0, displayLimit);
}
} else {
this.queue.push(notification);
}
},
releaseQueue() {
this.queue.forEach(n => this.prepend(n));
this.queue = [];
},
onTop() {
this.releaseQueue();
},
onBottom() {
this.fetchMoreNotifications();
}
}
});

View File

@ -1,22 +1,20 @@
<template>
<div>
<x-column :id="column.id" :menu="menu" :name="name">
<span slot="header">
<template v-if="column.type == 'home'">%fa:home%</template>
<template v-if="column.type == 'local'">%fa:R comments%</template>
<template v-if="column.type == 'global'">%fa:globe%</template>
<template v-if="column.type == 'list'">%fa:list%</template>
<span>{{ name }}</span>
</span>
<x-column :menu="menu" :name="name" :column="column" :is-stacked="isStacked">
<span slot="header">
<template v-if="column.type == 'home'">%fa:home%</template>
<template v-if="column.type == 'local'">%fa:R comments%</template>
<template v-if="column.type == 'global'">%fa:globe%</template>
<template v-if="column.type == 'list'">%fa:list%</template>
<span>{{ name }}</span>
</span>
<div class="editor" v-if="edit">
<mk-switch v-model="column.isMediaOnly" @change="onChangeSettings" text="%i18n:@is-media-only%"/>
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
</div>
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly"/>
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly"/>
</x-column>
</div>
<div class="editor" style="padding:0 12px" v-if="edit">
<mk-switch v-model="column.isMediaOnly" @change="onChangeSettings" text="%i18n:@is-media-only%"/>
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
</div>
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
</x-column>
</template>
<script lang="ts">
@ -36,6 +34,10 @@ export default Vue.extend({
column: {
type: Object,
required: true
},
isStacked: {
type: Boolean,
required: true
}
},
@ -43,8 +45,9 @@ export default Vue.extend({
return {
edit: false,
menu: [{
content: '%fa:cog% %i18n:@edit%',
onClick: () => {
icon: '%fa:cog%',
text: '%i18n:@edit%',
action: () => {
this.edit = !this.edit;
}
}]

View File

@ -1,5 +1,5 @@
<template>
<x-notes ref="timeline" :more="existMore ? more : null"/>
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
</template>
<script lang="ts">
@ -23,6 +23,11 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
},
mediaView: {
type: Boolean,
required: false,
default: false
}
},

View File

@ -1,13 +1,13 @@
<template>
<mk-ui :class="$style.root">
<div class="qlvquzbjribqcaozciifydkngcwtyzje" :data-darkmode="$store.state.device.darkmode">
<template v-for="column in columns">
<x-widgets-column v-if="column.type == 'widgets'" :key="column.id" :column="column"/>
<x-notifications-column v-if="column.type == 'notifications'" :key="column.id" :column="column"/>
<x-tl-column v-if="column.type == 'home'" :key="column.id" :column="column"/>
<x-tl-column v-if="column.type == 'local'" :key="column.id" :column="column"/>
<x-tl-column v-if="column.type == 'global'" :key="column.id" :column="column"/>
<x-tl-column v-if="column.type == 'list'" :key="column.id" :column="column"/>
<template v-for="ids in layout">
<div v-if="ids.length > 1" class="folder">
<template v-for="id, i in ids">
<x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true"/>
</template>
</div>
<x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])"/>
</template>
<button ref="add" @click="add" title="%i18n:common.deck.add-column%">%fa:plus%</button>
</div>
@ -16,27 +16,34 @@
<script lang="ts">
import Vue from 'vue';
import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-column.vue';
import XWidgetsColumn from './deck.widgets-column.vue';
import XColumnCore from './deck.column-core.vue';
import Menu from '../../../../common/views/components/menu.vue';
import MkUserListsWindow from '../../components/user-lists-window.vue';
import * as uuid from 'uuid';
export default Vue.extend({
components: {
XTlColumn,
XNotificationsColumn,
XWidgetsColumn
XColumnCore
},
computed: {
columns() {
columns(): any[] {
if (this.$store.state.settings.deck == null) return [];
return this.$store.state.settings.deck.columns;
},
layout(): any[] {
if (this.$store.state.settings.deck == null) return [];
if (this.$store.state.settings.deck.layout == null) return this.$store.state.settings.deck.columns.map(c => [c.id]);
return this.$store.state.settings.deck.layout;
}
},
provide() {
return {
getColumnVm: this.getColumnVm
};
},
created() {
if (this.$store.state.settings.deck == null) {
const deck = {
@ -58,11 +65,23 @@ export default Vue.extend({
}]
};
deck.layout = deck.columns.map(c => [c.id]);
this.$store.dispatch('settings/set', {
key: 'deck',
value: deck
});
}
// 互換性のため
if (this.$store.state.settings.deck != null && this.$store.state.settings.deck.layout == null) {
this.$store.dispatch('settings/set', {
key: 'deck',
value: Object.assign({}, this.$store.state.settings.deck, {
layout: this.$store.state.settings.deck.columns.map(c => [c.id])
})
});
}
},
mounted() {
@ -74,37 +93,45 @@ export default Vue.extend({
},
methods: {
getColumnVm(id) {
return this.$refs[id][0];
},
add() {
this.os.new(Menu, {
source: this.$refs.add,
compact: true,
items: [{
content: '%i18n:common.deck.home%',
onClick: () => {
icon: '%fa:home%',
text: '%i18n:common.deck.home%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'home'
});
}
}, {
content: '%i18n:common.deck.local%',
onClick: () => {
icon: '%fa:comments R%',
text: '%i18n:common.deck.local%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'local'
});
}
}, {
content: '%i18n:common.deck.global%',
onClick: () => {
icon: '%fa:globe%',
text: '%i18n:common.deck.global%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'global'
});
}
}, {
content: '%i18n:common.deck.list%',
onClick: () => {
icon: '%fa:list%',
text: '%i18n:common.deck.list%',
action: () => {
const w = (this as any).os.new(MkUserListsWindow);
w.$once('choosen', list => {
this.$store.dispatch('settings/addDeckColumn', {
@ -116,16 +143,18 @@ export default Vue.extend({
});
}
}, {
content: '%i18n:common.deck.notifications%',
onClick: () => {
icon: '%fa:bell R%',
text: '%i18n:common.deck.notifications%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'notifications'
});
}
}, {
content: '%i18n:common.deck.widgets%',
onClick: () => {
icon: '%fa:calculator%',
text: '%i18n:common.deck.widgets%',
action: () => {
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'widgets',
@ -159,6 +188,13 @@ root(isDark)
&:last-of-type
margin-right 0
&.folder
display flex
flex-direction column
> *:not(:last-child)
margin-bottom 8px
> *
&:first-child
margin-left auto

View File

@ -1,57 +1,52 @@
<template>
<div class="wtdtxvecapixsepjtcupubtsmometobz">
<x-column :id="column.id" :menu="menu" :naked="true" :narrow="true" :name="name">
<span slot="header">%fa:calculator%{{ name }}</span>
<x-column :menu="menu" :naked="true" :narrow="true" :name="name" :column="column" :is-stacked="isStacked" class="wtdtxvecapixsepjtcupubtsmometobz">
<span slot="header">%fa:calculator%{{ name }}</span>
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
<template v-if="edit">
<header>
<select v-model="widgetAdderSelected">
<option value="profile">%i18n:common.widgets.profile%</option>
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
<option value="calendar">%i18n:common.widgets.calendar%</option>
<option value="timemachine">%i18n:common.widgets.timemachine%</option>
<option value="activity">%i18n:common.widgets.activity%</option>
<option value="rss">%i18n:common.widgets.rss%</option>
<option value="trends">%i18n:common.widgets.trends%</option>
<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
<option value="slideshow">%i18n:common.widgets.slideshow%</option>
<option value="version">%i18n:common.widgets.version%</option>
<option value="broadcast">%i18n:common.widgets.broadcast%</option>
<option value="notifications">%i18n:common.widgets.notifications%</option>
<option value="users">%i18n:common.widgets.users%</option>
<option value="polls">%i18n:common.widgets.polls%</option>
<option value="post-form">%i18n:common.widgets.post-form%</option>
<option value="messaging">%i18n:common.widgets.messaging%</option>
<option value="memo">%i18n:common.widgets.memo%</option>
<option value="server">%i18n:common.widgets.server%</option>
<option value="donation">%i18n:common.widgets.donation%</option>
<option value="nav">%i18n:common.widgets.nav%</option>
<option value="tips">%i18n:common.widgets.tips%</option>
</select>
<button @click="addWidget">%i18n:@add%</button>
</header>
<x-draggable
:list="column.widgets"
:options="{ handle: '.handle', animation: 150 }"
@sort="onWidgetSort"
>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id">
<header>
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
</header>
<div @click="widgetFunc(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
</div>
</x-draggable>
</template>
<template v-else>
<component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck"/>
</template>
</div>
</x-column>
</div>
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
<template v-if="edit">
<header>
<select v-model="widgetAdderSelected" @change="addWidget">
<option value="profile">%i18n:common.widgets.profile%</option>
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
<option value="calendar">%i18n:common.widgets.calendar%</option>
<option value="timemachine">%i18n:common.widgets.timemachine%</option>
<option value="activity">%i18n:common.widgets.activity%</option>
<option value="rss">%i18n:common.widgets.rss%</option>
<option value="trends">%i18n:common.widgets.trends%</option>
<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
<option value="slideshow">%i18n:common.widgets.slideshow%</option>
<option value="version">%i18n:common.widgets.version%</option>
<option value="broadcast">%i18n:common.widgets.broadcast%</option>
<option value="notifications">%i18n:common.widgets.notifications%</option>
<option value="users">%i18n:common.widgets.users%</option>
<option value="polls">%i18n:common.widgets.polls%</option>
<option value="post-form">%i18n:common.widgets.post-form%</option>
<option value="messaging">%i18n:common.widgets.messaging%</option>
<option value="memo">%i18n:common.widgets.memo%</option>
<option value="hashtags">%i18n:common.widgets.hashtags%</option>
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
<option value="server">%i18n:common.widgets.server%</option>
<option value="donation">%i18n:common.widgets.donation%</option>
<option value="nav">%i18n:common.widgets.nav%</option>
<option value="tips">%i18n:common.widgets.tips%</option>
</select>
</header>
<x-draggable
:list="column.widgets"
:options="{ animation: 150 }"
@sort="onWidgetSort"
>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
</x-draggable>
</template>
<template v-else>
<component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck"/>
</template>
</div>
</x-column>
</template>
<script lang="ts">
@ -70,6 +65,10 @@ export default Vue.extend({
column: {
type: Object,
required: true
},
isStacked: {
type: Boolean,
required: true
}
},
@ -90,8 +89,9 @@ export default Vue.extend({
created() {
this.menu = [{
content: '%fa:cog% %i18n:@edit%',
onClick: () => {
icon: '%fa:cog%',
text: '%i18n:@edit%',
action: () => {
this.edit = !this.edit;
}
}];
@ -137,6 +137,13 @@ export default Vue.extend({
root(isDark)
.gqpwvtwtprsbmnssnbicggtwqhmylhnq
> header
padding 16px
> *
width 100%
padding 4px
.widget, .customize-container
margin 8px
@ -144,7 +151,21 @@ root(isDark)
margin-top 0
.customize-container
background #fff
cursor move
> *:not(.remove)
pointer-events none
> .remove
position absolute
z-index 1
top 8px
right 8px
width 32px
height 32px
color #fff
background rgba(#000, 0.7)
border-radius 4px
> header
color isDark ? #fff : #000

View File

@ -0,0 +1,128 @@
<template>
<mk-ui>
<header :class="$style.header">
<h1>#{{ $route.params.tag }}</h1>
</header>
<div :class="$style.loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<p :class="$style.empty" v-if="!fetching && empty">%fa:search%{{ q }}に関する投稿は見つかりませんでした</p>
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
const limit = 20;
export default Vue.extend({
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
offset: 0,
empty: false
};
},
watch: {
$route: 'fetch'
},
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
window.addEventListener('scroll', this.onScroll, { passive: true });
this.fetch();
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
window.removeEventListener('scroll', this.onScroll);
},
methods: {
onDocumentKeydown(e) {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
if (e.which == 84) { // t
(this.$refs.timeline as any).focus();
}
}
},
fetch() {
this.fetching = true;
Progress.start();
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
}).then(notes => {
if (notes.length == 0) this.empty = true;
if (notes.length == limit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
Progress.done();
}, rej);
}));
},
more() {
this.offset += limit;
const promise = (this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
});
promise.then(notes => {
if (notes.length == limit + 1) {
notes.pop();
} else {
this.existMore = false;
}
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
return promise;
}
}
});
</script>
<style lang="stylus" module>
.header
width 100%
max-width 600px
margin 0 auto
color #555
.notes
width 600px
margin 0 auto
border solid 1px rgba(#000, 0.075)
border-radius 6px
overflow hidden
.loading
padding 64px 0
.empty
display block
margin 0 auto
padding 32px
max-width 400px
text-align center
color #999
> [data-fa]
display block
margin-bottom 16px
font-size 3em
color #ccc
</style>

View File

@ -32,42 +32,30 @@ body > noscript {
left: 0;
width: 100%;
height: 100%;
text-align: center;
background: #fff;
cursor: wait;
}
#ini > p {
display: block;
user-select: none;
margin: 32px;
font-size: 4em;
color: #555;
#ini > svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 64px;
height: 64px;
animation: ini 0.6s infinite linear;
}
#ini > p > span {
animation: ini 1.4s infinite ease-in-out both;
}
#ini > p > span:nth-child(1) {
animation-delay: 0s;
}
#ini > p > span:nth-child(2) {
animation-delay: 0.16s;
}
#ini > p > span:nth-child(3) {
animation-delay: 0.32s;
}
html[data-darkmode] #ini {
background: #191b22;
}
html[data-darkmode] #ini > p {
color: #fff;
}
@keyframes ini {
0%, 80%, 100% {
opacity: 1;
from {
transform: rotate(0deg);
}
40% {
opacity: 0;
to {
transform: rotate(360deg);
}
}

View File

@ -8,7 +8,8 @@ import Progress from './common/scripts/loading';
import Connection from './common/scripts/streaming/stream';
import { HomeStreamManager } from './common/scripts/streaming/home';
import { DriveStreamManager } from './common/scripts/streaming/drive';
import { ServerStreamManager } from './common/scripts/streaming/server';
import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats';
import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats';
import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index';
import { OthelloStreamManager } from './common/scripts/streaming/othello';
@ -104,14 +105,16 @@ export default class MiOS extends EventEmitter {
localTimelineStream: LocalTimelineStreamManager;
globalTimelineStream: GlobalTimelineStreamManager;
driveStream: DriveStreamManager;
serverStream: ServerStreamManager;
serverStatsStream: ServerStatsStreamManager;
notesStatsStream: NotesStatsStreamManager;
messagingIndexStream: MessagingIndexStreamManager;
othelloStream: OthelloStreamManager;
} = {
localTimelineStream: null,
globalTimelineStream: null,
driveStream: null,
serverStream: null,
serverStatsStream: null,
notesStatsStream: null,
messagingIndexStream: null,
othelloStream: null
};
@ -218,7 +221,8 @@ export default class MiOS extends EventEmitter {
this.store = initStore(this);
//#region Init stream managers
this.streams.serverStream = new ServerStreamManager(this);
this.streams.serverStatsStream = new ServerStatsStreamManager(this);
this.streams.notesStatsStream = new NotesStatsStreamManager(this);
this.once('signedin', () => {
// Init home stream manager

View File

@ -41,7 +41,7 @@
<mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
</div>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media" :raw="true"/>

View File

@ -2,26 +2,7 @@
<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<header>
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span>
<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span>
<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'">%fa:home%</template>
<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
<template v-if="note.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@ -79,64 +60,8 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
> .header
margin-bottom 2px
white-space nowrap
> .avatar
flex-shrink 0
margin-right 8px
width 18px
height 18px
border-radius 100%
> .name
display block
margin 0 .5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #607073
font-size 1em
font-weight 700
text-align left
text-decoration none
text-overflow ellipsis
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 0.5em 0 0
padding 1px 6px
font-size 0.8em
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #d1d8da
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #b2b8bb
> .mobile
margin-right 6px
> .visibility
margin-left 6px
> .body

View File

@ -2,26 +2,7 @@
<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<header>
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span>
<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span>
<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="note | notePage">
<mk-time :time="note.createdAt"/>
</router-link>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'">%fa:home%</template>
<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
<template v-if="note.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@ -92,66 +73,8 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
> .header
margin-bottom 2px
white-space nowrap
> .avatar
flex-shrink 0
margin-right 8px
width 18px
height 18px
border-radius 100%
> .name
display block
margin 0 0.5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #607073
font-size 1em
font-weight 700
text-align left
text-decoration none
text-overflow ellipsis
&:hover
text-decoration underline
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 0.5em 0 0
padding 1px 5px
font-size 0.8em
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
text-align left
margin 0
color isDark ? #606984 : #d1d8da
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #b2b8bb
> .mobile
margin-right 6px
> .visibility
margin-left 6px
> .body

View File

@ -14,26 +14,7 @@
<article>
<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<header>
<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
<span class="is-admin" v-if="p.user.isAdmin">admin</span>
<span class="is-bot" v-if="p.user.isBot">bot</span>
<span class="is-cat" v-if="p.user.isCat">cat</span>
<span class="username"><mk-acct :user="p.user"/></span>
<div class="info">
<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
<router-link class="created-at" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>
<span class="visibility" v-if="p.visibility != 'public'">
<template v-if="p.visibility == 'home'">%fa:home%</template>
<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
<template v-if="p.visibility == 'private'">%fa:lock%</template>
</span>
</div>
</header>
<mk-note-header class="header" :note="p" :mini="true"/>
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
@ -52,7 +33,7 @@
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
@ -358,65 +339,10 @@ root(isDark)
flex 1
min-width 0
> header
display flex
align-items baseline
white-space nowrap
> .header
@media (min-width 500px)
margin-bottom 2px
> .avatar
flex-shrink 0
margin-right 8px
width 20px
height 20px
border-radius 100%
> .name
display block
margin 0 0.5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #627079
font-weight bold
text-decoration none
text-overflow ellipsis
> .is-admin
> .is-bot
> .is-cat
align-self center
margin 0 0.5em 0 0
padding 1px 6px
font-size 0.8em
color isDark ? #758188 : #aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
&.is-admin
border-color isDark ? #d42c41 : #f56a7b
color isDark ? #d42c41 : #f56a7b
> .username
margin 0 0.5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
> *
color isDark ? #606984 : #c0c0c0
> .mobile
margin-right 6px
> .visibility
margin-left 6px
> .body
@media (min-width 700px)
font-size 1.1em

View File

@ -15,6 +15,8 @@
<option value="rss">%i18n:common.widgets.rss%</option>
<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
<option value="slideshow">%i18n:common.widgets.slideshow%</option>
<option value="hashtags">%i18n:common.widgets.hashtags%</option>
<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
<option value="version">%i18n:common.widgets.version%</option>
<option value="server">%i18n:common.widgets.server%</option>
<option value="memo">%i18n:common.widgets.memo%</option>

View File

@ -5,10 +5,10 @@
// Detect an old browser
if (!('fetch' in window)) {
alert(
'お使いのブラウザが古いためMisskeyを動作させることができません。' +
'お使いのブラウザ(またはOS)が古いためMisskeyを動作させることができません。' +
'バージョンを最新のものに更新するか、別のブラウザをお試しください。' +
'\n\n' +
'Your browser seems outdated. ' +
'Your browser (or your OS) seems outdated. ' +
'To run Misskey, please update your browser to latest version or try other browsers.');
}

View File

@ -173,23 +173,34 @@ export default (os: MiOS) => new Vuex.Store({
},
addDeckColumn(state, column) {
if (state.deck.columns == null) state.deck.columns = [];
state.deck.columns.push(column);
state.deck.layout.push([column.id]);
},
removeDeckColumn(state, id) {
if (state.deck.columns == null) return;
state.deck.columns = state.deck.columns.filter(c => c.id != id);
state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
swapDeckColumn(state, x) {
const a = x.a;
const b = x.b;
const aX = state.deck.layout.findIndex(ids => ids.indexOf(a) != -1);
const aY = state.deck.layout[aX].findIndex(id => id == a);
const bX = state.deck.layout.findIndex(ids => ids.indexOf(b) != -1);
const bY = state.deck.layout[bX].findIndex(id => id == b);
state.deck.layout[aX][aY] = b;
state.deck.layout[bX][bY] = a;
},
swapLeftDeckColumn(state, id) {
if (state.deck.columns == null) return;
state.deck.columns.some((c, i) => {
if (c.id == id) {
const left = state.deck.columns[i - 1];
state.deck.layout.some((ids, i) => {
if (ids.indexOf(id) != -1) {
const left = state.deck.layout[i - 1];
if (left) {
state.deck.columns[i - 1] = state.deck.columns[i];
state.deck.columns[i] = left;
state.deck.layout[i - 1] = state.deck.layout[i];
state.deck.layout[i] = left;
}
return true;
}
@ -197,28 +208,68 @@ export default (os: MiOS) => new Vuex.Store({
},
swapRightDeckColumn(state, id) {
if (state.deck.columns == null) return;
state.deck.columns.some((c, i) => {
if (c.id == id) {
const right = state.deck.columns[i + 1];
state.deck.layout.some((ids, i) => {
if (ids.indexOf(id) != -1) {
const right = state.deck.layout[i + 1];
if (right) {
state.deck.columns[i + 1] = state.deck.columns[i];
state.deck.columns[i] = right;
state.deck.layout[i + 1] = state.deck.layout[i];
state.deck.layout[i] = right;
}
return true;
}
});
},
swapUpDeckColumn(state, id) {
const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
ids.some((x, i) => {
if (x == id) {
const up = ids[i - 1];
if (up) {
ids[i - 1] = id;
ids[i] = up;
}
return true;
}
});
},
swapDownDeckColumn(state, id) {
const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
ids.some((x, i) => {
if (x == id) {
const down = ids[i + 1];
if (down) {
ids[i + 1] = id;
ids[i] = down;
}
return true;
}
});
},
stackLeftDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
const left = state.deck.layout[i - 1];
if (left) state.deck.layout[i - 1].push(id);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
popRightDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
state.deck.layout.splice(i + 1, 0, [id]);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
addDeckWidget(state, x) {
if (state.deck.columns == null) return;
const column = state.deck.columns.find(c => c.id == x.id);
if (column == null) return;
column.widgets.unshift(x.widget);
},
removeDeckWidget(state, x) {
if (state.deck.columns == null) return;
const column = state.deck.columns.find(c => c.id == x.id);
if (column == null) return;
column.widgets = column.widgets.filter(w => w.id != x.widget.id);
@ -267,6 +318,11 @@ export default (os: MiOS) => new Vuex.Store({
ctx.dispatch('saveDeck');
},
swapDeckColumn(ctx, id) {
ctx.commit('swapDeckColumn', id);
ctx.dispatch('saveDeck');
},
swapLeftDeckColumn(ctx, id) {
ctx.commit('swapLeftDeckColumn', id);
ctx.dispatch('saveDeck');
@ -277,6 +333,26 @@ export default (os: MiOS) => new Vuex.Store({
ctx.dispatch('saveDeck');
},
swapUpDeckColumn(ctx, id) {
ctx.commit('swapUpDeckColumn', id);
ctx.dispatch('saveDeck');
},
swapDownDeckColumn(ctx, id) {
ctx.commit('swapDownDeckColumn', id);
ctx.dispatch('saveDeck');
},
stackLeftDeckColumn(ctx, id) {
ctx.commit('stackLeftDeckColumn', id);
ctx.dispatch('saveDeck');
},
popRightDeckColumn(ctx, id) {
ctx.commit('popRightDeckColumn', id);
ctx.dispatch('saveDeck');
},
addDeckWidget(ctx, x) {
ctx.commit('addDeckWidget', x);
ctx.dispatch('saveDeck');

View File

@ -0,0 +1,60 @@
import Note from '../models/note';
// 10分
const interval = 1000 * 60 * 10;
async function tick() {
const res = await Note.aggregate([{
$match: {
createdAt: {
$gt: new Date(Date.now() - interval)
},
tags: {
$exists: true,
$ne: []
}
}
}, {
$unwind: '$tags'
}, {
$group: {
_id: '$tags',
count: {
$sum: 1
}
}
}, {
$group: {
_id: null,
tags: {
$push: {
tag: '$_id',
count: '$count'
}
}
}
}, {
$project: {
_id: false,
tags: true
}
}]) as {
tags: Array<{
tag: string;
count: number;
}>
};
const stats = res.tags
.sort((a, b) => a.count - b.count)
.map(tag => [tag.tag, tag.count])
.slice(0, 10);
console.log(stats);
process.send(stats);
}
tick();
setInterval(tick, interval);

View File

@ -0,0 +1,20 @@
import * as childProcess from 'child_process';
import Xev from 'xev';
const ev = new Xev();
export default function() {
const log = [];
const p = childProcess.fork(__dirname + '/hashtags-stats-child.js');
p.on('message', stats => {
ev.emit('hashtagsStats', stats);
log.push(stats);
if (log.length > 30) log.shift();
});
ev.on('requestHashTagsStatsLog', id => {
ev.emit('hashtagsStatsLog:' + id, log);
});
}

View File

@ -0,0 +1,26 @@
import Note from '../models/note';
const interval = 5000;
async function tick() {
const [all, local] = await Promise.all([Note.count({
createdAt: {
$gte: new Date(Date.now() - interval)
}
}), Note.count({
createdAt: {
$gte: new Date(Date.now() - interval)
},
'_user.host': null
})]);
const stats = {
all, local
};
process.send(stats);
}
tick();
setInterval(tick, interval);

View File

@ -0,0 +1,20 @@
import * as childProcess from 'child_process';
import Xev from 'xev';
const ev = new Xev();
export default function() {
const log = [];
const p = childProcess.fork(__dirname + '/notes-stats-child.js');
p.on('message', stats => {
ev.emit('notesStats', stats);
log.push(stats);
if (log.length > 100) log.shift();
});
ev.on('requestNotesStatsLog', id => {
ev.emit('notesStatsLog:' + id, log);
});
}

View File

@ -5,14 +5,22 @@ import Xev from 'xev';
const ev = new Xev();
const interval = 1000;
/**
* Report stats regularly
* Report server stats regularly
*/
export default function() {
setInterval(() => {
const log = [];
ev.on('requestServerStatsLog', id => {
ev.emit('serverStatsLog:' + id, log);
});
async function tick() {
osUtils.cpuUsage(cpuUsage => {
const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/');
ev.emit('stats', {
const stats = {
cpu_usage: cpuUsage,
mem: {
total: os.totalmem(),
@ -21,7 +29,14 @@ export default function() {
disk,
os_uptime: os.uptime(),
process_uptime: process.uptime()
});
};
ev.emit('serverStats', stats);
log.push(stats);
if (log.length > 50) log.shift();
});
}, 1000);
}
tick();
setInterval(tick, interval);
}

View File

@ -17,7 +17,8 @@ import ProgressBar from './utils/cli/progressbar';
import EnvironmentInfo from './utils/environmentInfo';
import MachineInfo from './utils/machineInfo';
import DependencyInfo from './utils/dependencyInfo';
import stats from './utils/stats';
import serverStats from './daemons/server-stats';
import notesStats from './daemons/notes-stats';
import loadConfig from './config/load';
import { Config } from './config/types';
@ -49,7 +50,8 @@ function main() {
masterMain(opt);
ev.mount();
stats();
serverStats();
notesStats();
} else {
workerMain(opt);
}

View File

@ -16,6 +16,10 @@ import Following from './following';
const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId');
Note.createIndex('tags', { sparse: true });
Note.createIndex({
createdAt: -1
});
export default Note;
export function isValidText(text: string): boolean {

View File

@ -48,6 +48,8 @@ type IUserBase = {
usernameLower: string;
avatarId: mongo.ObjectID;
bannerId: mongo.ObjectID;
avatarUrl?: string;
bannerUrl?: string;
wallpaperId: mongo.ObjectID;
data: any;
description: string;
@ -405,13 +407,17 @@ export const pack = (
delete _user.publicKey;
}
_user.avatarUrl = _user.avatarId != null
? `${config.drive_url}/${_user.avatarId}`
: `${config.drive_url}/default-avatar.jpg`;
if (_user.avatarUrl == null) {
_user.avatarUrl = _user.avatarId != null
? `${config.drive_url}/${_user.avatarId}`
: `${config.drive_url}/default-avatar.jpg`;
}
_user.bannerUrl = _user.bannerId != null
? `${config.drive_url}/${_user.bannerId}`
: null;
if (_user.bannerUrl == null) {
_user.bannerUrl = _user.bannerId != null
? `${config.drive_url}/${_user.bannerId}`
: null;
}
_user.wallpaperUrl = _user.wallpaperId != null
? `${config.drive_url}/${_user.wallpaperId}`

View File

@ -9,6 +9,7 @@ import webFinger from '../../webfinger';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file';
const log = debug('misskey:activitypub');
@ -117,19 +118,33 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
}
//#region アイコンとヘッダー画像をフェッチ
const [avatarId, bannerId] = (await Promise.all([
const [avatar, banner] = (await Promise.all<IDriveFile>([
person.icon,
person.image
].map(img =>
img == null
? Promise.resolve(null)
: resolveImage(user, img)
))).map(file => file != null ? file._id : null);
)));
User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
const avatarId = avatar ? avatar._id : null;
const bannerId = banner ? banner._id : null;
const avatarUrl = avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null;
const bannerUrl = banner && banner.metadata.isMetaOnly ? banner.metadata.url : null;
await User.update({ _id: user._id }, {
$set: {
avatarId,
bannerId,
avatarUrl,
bannerUrl
}
});
user.avatarId = avatarId;
user.bannerId = bannerId;
user.avatarUrl = avatarUrl;
user.bannerUrl = bannerUrl;
//#endregion
return user;
@ -190,21 +205,23 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
const summaryDOM = JSDOM.fragment(person.summary);
// アイコンとヘッダー画像をフェッチ
const [avatarId, bannerId] = (await Promise.all([
const [avatar, banner] = (await Promise.all<IDriveFile>([
person.icon,
person.image
].map(img =>
img == null
? Promise.resolve(null)
: resolveImage(exist, img)
))).map(file => file != null ? file._id : null);
)));
// Update user
await User.update({ _id: exist._id }, {
$set: {
updatedAt: new Date(),
avatarId,
bannerId,
avatarId: avatar ? avatar._id : null,
bannerId: banner ? banner._id : null,
avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null,
bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null,
description: summaryDOM.textContent,
followersCount,
followingCount,

View File

@ -2,6 +2,6 @@ import config from '../../../config';
export default tag => ({
type: 'Hashtag',
href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
name: '#' + tag
});

View File

@ -525,6 +525,9 @@ const endpoints: Endpoint[] = [
{
name: 'notes/search'
},
{
name: 'notes/search_by_tag'
},
{
name: 'notes/timeline',
withCredential: true,
@ -625,6 +628,11 @@ const endpoints: Endpoint[] = [
withCredential: true
},
{
name: 'hashtags/trend',
withCredential: true
},
{
name: 'messaging/history',
withCredential: true,

View File

@ -0,0 +1,136 @@
import Note from '../../../../models/note';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
*/
const rangeA = 1000 * 60 * 30; // 30分
const rangeB = 1000 * 60 * 120; // 2時間
const coefficient = 1.5; // 「n倍」の部分
const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
const max = 5;
/**
* Get trends of hashtags
*/
module.exports = () => new Promise(async (res, rej) => {
//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
const data = await Note.aggregate([{
$match: {
createdAt: {
$gt: new Date(Date.now() - rangeA)
},
tags: {
$exists: true,
$ne: []
}
}
}, {
$unwind: '$tags'
}, {
$group: {
_id: { tags: '$tags', userId: '$userId' }
}
}]) as Array<{
_id: {
tags: string;
userId: any;
}
}>;
//#endregion
if (data.length == 0) {
return res([]);
}
const tags = [];
// カウント
data.map(x => x._id).forEach(x => {
const i = tags.findIndex(tag => tag.name == x.tags);
if (i != -1) {
tags[i].count++;
} else {
tags.push({
name: x.tags,
count: 1
});
}
});
// 最低要求投稿者数を下回るならカットする
const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
//#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
const hotsPromises = limitedTags.map(async tag => {
const passedCount = (await Note.distinct('userId', {
tags: tag.name,
createdAt: {
$lt: new Date(Date.now() - rangeA),
$gt: new Date(Date.now() - rangeB)
}
}) as any).length;
if (tag.count >= (passedCount * coefficient)) {
return tag;
} else {
return null;
}
});
//#endregion
// タグを人気順に並べ替え
let hots = (await Promise.all(hotsPromises))
.filter(x => x != null)
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, max);
//#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
if (hots.length < max) {
hots = hots.concat(tags
.filter(tag => hots.indexOf(tag.name) == -1)
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, max - hots.length));
}
//#endregion
//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
const countPromises: Array<Promise<any[]>> = [];
const range = 20;
// 10分
const interval = 1000 * 60 * 10;
for (let i = 0; i < range; i++) {
countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', {
tags: tag,
createdAt: {
$lt: new Date(Date.now() - (interval * i)),
$gt: new Date(Date.now() - (interval * (i + 1)))
}
}))));
}
const countsLog = await Promise.all(countPromises);
const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', {
tags: tag,
createdAt: {
$gt: new Date(Date.now() - (interval * range))
}
})));
//#endregion
const stats = hots.map((tag, i) => ({
tag,
chart: countsLog.map(counts => counts[i].length),
usersCount: totalCounts[i].length
}));
res(stats);
});

View File

@ -140,7 +140,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
}
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
if (text === undefined && files === null && renote === null && poll === undefined) {
if ((text === undefined || text === null) && files === null && renote === null && poll === undefined) {
return rej('text, mediaIds, renoteId or poll is required');
}

View File

@ -0,0 +1,329 @@
import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note';
import User from '../../../../models/user';
import Mute from '../../../../models/mute';
import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note';
/**
* Search notes by tag
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'tag' parameter
const [tag, tagError] = $.str.get(params.tag);
if (tagError) return rej('invalid tag param');
// Get 'includeUserIds' parameter
const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional().get(params.includeUserIds);
if (includeUserIdsErr) return rej('invalid includeUserIds param');
// Get 'excludeUserIds' parameter
const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional().get(params.excludeUserIds);
if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
// Get 'includeUserUsernames' parameter
const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional().get(params.includeUserUsernames);
if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
// Get 'excludeUserUsernames' parameter
const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional().get(params.excludeUserUsernames);
if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
// Get 'following' parameter
const [following = null, followingErr] = $.bool.optional().nullable().get(params.following);
if (followingErr) return rej('invalid following param');
// Get 'mute' parameter
const [mute = 'mute_all', muteErr] = $.str.optional().get(params.mute);
if (muteErr) return rej('invalid mute param');
// Get 'reply' parameter
const [reply = null, replyErr] = $.bool.optional().nullable().get(params.reply);
if (replyErr) return rej('invalid reply param');
// Get 'renote' parameter
const [renote = null, renoteErr] = $.bool.optional().nullable().get(params.renote);
if (renoteErr) return rej('invalid renote param');
// Get 'media' parameter
const [media = null, mediaErr] = $.bool.optional().nullable().get(params.media);
if (mediaErr) return rej('invalid media param');
// Get 'poll' parameter
const [poll = null, pollErr] = $.bool.optional().nullable().get(params.poll);
if (pollErr) return rej('invalid poll param');
// Get 'sinceDate' parameter
const [sinceDate, sinceDateErr] = $.num.optional().get(params.sinceDate);
if (sinceDateErr) throw 'invalid sinceDate param';
// Get 'untilDate' parameter
const [untilDate, untilDateErr] = $.num.optional().get(params.untilDate);
if (untilDateErr) throw 'invalid untilDate param';
// Get 'offset' parameter
const [offset = 0, offsetErr] = $.num.optional().min(0).get(params.offset);
if (offsetErr) return rej('invalid offset param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $.num.optional().range(1, 30).get(params.limit);
if (limitErr) return rej('invalid limit param');
let includeUsers = includeUserIds;
if (includeUserUsernames != null) {
const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
}))).filter(id => id != null);
includeUsers = includeUsers.concat(ids);
}
let excludeUsers = excludeUserIds;
if (excludeUserUsernames != null) {
const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
}))).filter(id => id != null);
excludeUsers = excludeUsers.concat(ids);
}
search(res, rej, me, tag, includeUsers, excludeUsers, following,
mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit);
});
async function search(
res, rej, me, tag, includeUserIds, excludeUserIds, following,
mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) {
let q: any = {
$and: [{
tags: tag
}]
};
const push = x => q.$and.push(x);
if (includeUserIds && includeUserIds.length != 0) {
push({
userId: {
$in: includeUserIds
}
});
} else if (excludeUserIds && excludeUserIds.length != 0) {
push({
userId: {
$nin: excludeUserIds
}
});
}
if (following != null && me != null) {
const ids = await getFriendIds(me._id, false);
push({
userId: following ? {
$in: ids
} : {
$nin: ids.concat(me._id)
}
});
}
if (me != null) {
const mutes = await Mute.find({
muterId: me._id,
deletedAt: { $exists: false }
});
const mutedUserIds = mutes.map(m => m.muteeId);
switch (mute) {
case 'mute_all':
push({
userId: {
$nin: mutedUserIds
},
'_reply.userId': {
$nin: mutedUserIds
},
'_renote.userId': {
$nin: mutedUserIds
}
});
break;
case 'mute_related':
push({
'_reply.userId': {
$nin: mutedUserIds
},
'_renote.userId': {
$nin: mutedUserIds
}
});
break;
case 'mute_direct':
push({
userId: {
$nin: mutedUserIds
}
});
break;
case 'direct_only':
push({
userId: {
$in: mutedUserIds
}
});
break;
case 'related_only':
push({
$or: [{
'_reply.userId': {
$in: mutedUserIds
}
}, {
'_renote.userId': {
$in: mutedUserIds
}
}]
});
break;
case 'all_only':
push({
$or: [{
userId: {
$in: mutedUserIds
}
}, {
'_reply.userId': {
$in: mutedUserIds
}
}, {
'_renote.userId': {
$in: mutedUserIds
}
}]
});
break;
}
}
if (reply != null) {
if (reply) {
push({
replyId: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
replyId: {
$exists: false
}
}, {
replyId: null
}]
});
}
}
if (renote != null) {
if (renote) {
push({
renoteId: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
renoteId: {
$exists: false
}
}, {
renoteId: null
}]
});
}
}
if (media != null) {
if (media) {
push({
mediaIds: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
mediaIds: {
$exists: false
}
}, {
mediaIds: null
}]
});
}
}
if (poll != null) {
if (poll) {
push({
poll: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
poll: {
$exists: false
}
}, {
poll: null
}]
});
}
}
if (sinceDate) {
push({
createdAt: {
$gt: new Date(sinceDate)
}
});
}
if (untilDate) {
push({
createdAt: {
$lt: new Date(untilDate)
}
});
}
if (q.$and.length == 0) {
q = {};
}
// Search notes
const notes = await Note
.find(q, {
sort: {
_id: -1
},
limit: max,
skip: offset
});
// Serialize
res(await Promise.all(notes.map(note => pack(note, me))));
}

View File

@ -0,0 +1,35 @@
import * as websocket from 'websocket';
import Xev from 'xev';
const ev = new Xev();
export default function(request: websocket.request, connection: websocket.connection): void {
const onStats = stats => {
connection.send(JSON.stringify({
type: 'stats',
body: stats
}));
};
connection.on('message', async data => {
const msg = JSON.parse(data.utf8Data);
switch (msg.type) {
case 'requestLog':
ev.once('notesStatsLog:' + msg.id, statsLog => {
connection.send(JSON.stringify({
type: 'statsLog',
body: statsLog
}));
});
ev.emit('requestNotesStatsLog', msg.id);
break;
}
});
ev.addListener('notesStats', onStats);
connection.on('close', () => {
ev.removeListener('notesStats', onStats);
});
}

View File

@ -0,0 +1,35 @@
import * as websocket from 'websocket';
import Xev from 'xev';
const ev = new Xev();
export default function(request: websocket.request, connection: websocket.connection): void {
const onStats = stats => {
connection.send(JSON.stringify({
type: 'stats',
body: stats
}));
};
connection.on('message', async data => {
const msg = JSON.parse(data.utf8Data);
switch (msg.type) {
case 'requestLog':
ev.once('serverStatsLog:' + msg.id, statsLog => {
connection.send(JSON.stringify({
type: 'statsLog',
body: statsLog
}));
});
ev.emit('requestServerStatsLog', msg.id);
break;
}
});
ev.addListener('serverStats', onStats);
connection.on('close', () => {
ev.removeListener('serverStats', onStats);
});
}

View File

@ -1,19 +0,0 @@
import * as websocket from 'websocket';
import Xev from 'xev';
const ev = new Xev();
export default function(request: websocket.request, connection: websocket.connection): void {
const onStats = stats => {
connection.send(JSON.stringify({
type: 'stats',
body: stats
}));
};
ev.addListener('stats', onStats);
connection.on('close', () => {
ev.removeListener('stats', onStats);
});
}

View File

@ -12,7 +12,8 @@ import messagingStream from './stream/messaging';
import messagingIndexStream from './stream/messaging-index';
import othelloGameStream from './stream/othello-game';
import othelloStream from './stream/othello';
import serverStream from './stream/server';
import serverStatsStream from './stream/server-stats';
import notesStatsStream from './stream/notes-stats';
import requestsStream from './stream/requests';
import { ParsedUrlQuery } from 'querystring';
import authenticate from './authenticate';
@ -28,8 +29,13 @@ module.exports = (server: http.Server) => {
ws.on('request', async (request) => {
const connection = request.accept();
if (request.resourceURL.pathname === '/server') {
serverStream(request, connection);
if (request.resourceURL.pathname === '/server-stats') {
serverStatsStream(request, connection);
return;
}
if (request.resourceURL.pathname === '/notes-stats') {
notesStatsStream(request, connection);
return;
}

View File

@ -164,8 +164,8 @@ export default async function(
'metadata.deletedAt': { $exists: false }
});
if (much !== null) {
log('file with same hash is found');
if (much) {
log(`file with same hash is found: ${much._id}`);
return much;
}
}

View File

@ -94,7 +94,7 @@ export default async (user: IUser, data: {
if (data.visibility == null) data.visibility = 'public';
if (data.viaMobile == null) data.viaMobile = false;
const tags = data.tags || [];
let tags = data.tags || [];
let tokens: any[] = null;
@ -114,6 +114,8 @@ export default async (user: IUser, data: {
});
}
tags = tags.filter(tag => tag.length <= 100);
if (data.visibleUsers) {
data.visibleUsers = data.visibleUsers.filter(x => x != null);
}

View File

@ -25,8 +25,10 @@ const handlers = {
hashtag({ document }, { hashtag }) {
const a = document.createElement('a');
a.href = '/search?q=#' + hashtag;
a.textContent = hashtag;
a.href = config.url + '/tags/' + hashtag;
a.textContent = '#' + hashtag;
a.setAttribute('rel', 'tag');
document.body.appendChild(a);
},
'inline-code'({ document }, { code }) {