Compare commits
	
		
			90 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f632ec50c1 | ||
|   | a55d15214b | ||
|   | f1709a2cc2 | ||
|   | effa542958 | ||
|   | e8bf742c87 | ||
|   | 2e6652edce | ||
|   | 230c204b48 | ||
|   | 3755c600b1 | ||
|   | 24513fc0a3 | ||
|   | 0a79a6564a | ||
|   | 562bb5842b | ||
|   | ec3ca3032e | ||
|   | 890770c275 | ||
|   | 9ed58a1b4e | ||
|   | 08984be2fe | ||
|   | e3ade148ca | ||
|   | 34c0eff89f | ||
|   | 40aba47a47 | ||
|   | 6736f51134 | ||
|   | 9d826d6e52 | ||
|   | 902d9bc7a5 | ||
|   | b6c86e2845 | ||
|   | 34dffdfc8f | ||
|   | a56f3f1d89 | ||
|   | 88dc4c83cb | ||
|   | 5a28dc0198 | ||
|   | 40d2650d49 | ||
|   | 545e83efb1 | ||
|   | d4b00a5482 | ||
|   | c2b1bbeec5 | ||
|   | 8c8f165a6e | ||
|   | 04553de230 | ||
|   | 2776934728 | ||
|   | 0064dbb010 | ||
|   | d52e671adf | ||
|   | 6017dc2dff | ||
|   | 937f7cbd60 | ||
|   | f8b3f66904 | ||
|   | 9d5701f35a | ||
|   | dff65810c6 | ||
|   | 6752cf1d64 | ||
|   | 8336910a59 | ||
|   | 957a1149e0 | ||
|   | e8719ff6e6 | ||
|   | 28b63298e5 | ||
|   | dd4dee8095 | ||
|   | c47818fed4 | ||
|   | e53c383908 | ||
|   | 55c9c0436b | ||
|   | 66b79e5e24 | ||
|   | 514b830910 | ||
|   | e4f799bf1d | ||
|   | b383427d3d | ||
|   | e969518139 | ||
|   | 113fe294bd | ||
|   | a4d92f781f | ||
|   | 414cac49c3 | ||
|   | 95b157ac3e | ||
|   | 8e3d884081 | ||
|   | 9def6fcadd | ||
|   | 7837bd44fc | ||
|   | a6c3663155 | ||
|   | 0b5afadbb8 | ||
|   | 43864f0da4 | ||
|   | 6a0d9d70ed | ||
|   | 63c6dce68e | ||
|   | 53422ffcb2 | ||
|   | 38ca514f53 | ||
|   | caea0f0376 | ||
|   | 25a8b26977 | ||
|   | bcaefe8d62 | ||
|   | 46f1e8c599 | ||
|   | 16230f320e | ||
|   | ace6419aef | ||
|   | 77fb9eb2be | ||
|   | aa7fc7c893 | ||
|   | 8fc170109f | ||
|   | ad12d00d7e | ||
|   | fa5ea45726 | ||
|   | 4b6c113251 | ||
|   | 3548290ff2 | ||
|   | b165b90c40 | ||
|   | 4ffe9c908b | ||
|   | a135f75e71 | ||
|   | cbc61ba03d | ||
|   | 5aa58da918 | ||
|   | b083430011 | ||
|   | a8946b0404 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0303bccc61 | ||
|   | f3ce8564ea | 
| @@ -141,39 +141,28 @@ workflows: | ||||
|                 - l10n_develop | ||||
|                 - imgbot | ||||
|                 - patch-autogen | ||||
|       - build: | ||||
|       - hold: | ||||
|           type: approval | ||||
|           filters: | ||||
|             branches: | ||||
|               ignore: | ||||
|                 - l10n_develop | ||||
|                 - imgbot | ||||
|                 - patch-autogen | ||||
|               ignore: master | ||||
|       - build: | ||||
|           requires: | ||||
|             - hold | ||||
|       - test: | ||||
|           executor: with-redis | ||||
|           requires: | ||||
|             - build | ||||
|           filters: | ||||
|             branches: | ||||
|               ignore: | ||||
| #                - master | ||||
|                 - l10n_develop | ||||
|                 - imgbot | ||||
|                 - patch-autogen | ||||
|       - test: | ||||
|           without_redis: true | ||||
|           requires: | ||||
|             - build | ||||
|           filters: | ||||
| #            branches: | ||||
| #              only: master | ||||
|             branches: | ||||
|               ignore: | ||||
| #                - master | ||||
|                 - l10n_develop | ||||
|                 - imgbot | ||||
|                 - patch-autogen | ||||
|   docker: | ||||
|     jobs: | ||||
|       - ok: | ||||
|           filters: | ||||
|             branches: | ||||
|               ignore: master | ||||
|       - hold: | ||||
|           type: approval | ||||
|           filters: | ||||
|   | ||||
							
								
								
									
										50
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,6 +1,56 @@ | ||||
| ChangeLog | ||||
| ========= | ||||
|  | ||||
| 10.86.2 | ||||
| ---------- | ||||
| * 別タブでルートより下を開いたときにはデッキにしないように | ||||
| * 横のナビゲーションバーの改善 | ||||
| * MIDIファイルがオーディオ扱いになる問題を修正 | ||||
| * ミュートワードで正規表現を使えるように | ||||
| * デッキで無効になったタイムラインに警告を表示するように | ||||
| * デザインの調整 | ||||
| * その他細かな修正 | ||||
|  | ||||
| 10.86.1 | ||||
| ---------- | ||||
| * ナビゲーションバーの「ホーム」を「タイムライン」に改称 | ||||
| * モバイル版でユーザーページが二重に描画される問題を修正 | ||||
| * ユーザー一覧の「もっと読み込む」の動作がおかしい問題を修正 | ||||
| * デザインの調整 | ||||
|  | ||||
| 10.86.0 | ||||
| ---------- | ||||
| * Exploreページを実装 | ||||
| * UIを改良 | ||||
| * その他細かな修正 | ||||
|  | ||||
| 10.85.2 | ||||
| ---------- | ||||
| * デッキから フォロー/フォロワー ページに行けるように | ||||
| * ナビゲーションが発生したときに最上部までスクロールように | ||||
| * 検索結果でページ遷移が発生する問題を修正 | ||||
| * デザインの調整 | ||||
|  | ||||
| 10.85.1 | ||||
| ---------- | ||||
| * ローカルのみ投稿をログイン画面のタイムラインに表示しないように | ||||
| * ナビゲーションバーを横にしてるとデッキに行けない問題を修正 | ||||
|  | ||||
| 10.85.0 | ||||
| ---------- | ||||
| * デスクトップ版のUIを改良 | ||||
| * 投稿ハイライトページを実装 | ||||
| * 無効化されているタイムラインのフォールバック | ||||
| * 既にフォローされている場合はフォローリクエストを生成しないように | ||||
| * その他細かな修正 | ||||
|  | ||||
| 10.84.2 | ||||
| ---------- | ||||
| * GIF画像にGIFバッジを表示 | ||||
| * よく話すユーザーからサスペンドされたユーザーを隠すなど | ||||
| * nodeinfoが重い問題を修正 | ||||
| * ハッシュタグクラウド取得が重い問題を軽減 | ||||
|  | ||||
| 10.84.1 | ||||
| ---------- | ||||
| * deckにフォローされていますマークを追加 | ||||
|   | ||||
| @@ -8,7 +8,6 @@ WORKDIR /misskey | ||||
|  | ||||
| FROM base AS builder | ||||
|  | ||||
| RUN unlink /usr/bin/free | ||||
| RUN apk add --no-cache \ | ||||
|     autoconf \ | ||||
|     automake \ | ||||
| @@ -20,7 +19,6 @@ RUN apk add --no-cache \ | ||||
|     make \ | ||||
|     nasm \ | ||||
|     pkgconfig \ | ||||
|     procps \ | ||||
|     python \ | ||||
|     zlib-dev | ||||
| RUN npm i -g yarn | ||||
|   | ||||
| @@ -94,7 +94,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). | ||||
| <!-- PATREON_START --> | ||||
| <table><tr> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/1?token-time=2145916800&token-hash=WeuDzzz24cRXJogyIkU-mxARqkdyms-rcZKbO-GpGjw%3D" alt="weep" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=prtYqPOiSHBulhM7NU0VzMaWx39-9ntdq25b6kafDNA%3D" alt="negao" width="100"></td> | ||||
| <td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/3?token-time=2145916800&token-hash=c8HeVqLtmdgH-gSBJg8i10gmOcwllM87MDHeznl3el0%3D" alt="Melilot" width="100"></td> | ||||
| <td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/3?token-time=2145916800&token-hash=LtV2lRi3L2jOWMLwccr9qWYfPrFlzIo2jYZHKzHEb6k%3D" alt="Xeltica" width="100"></td> | ||||
| @@ -102,7 +102,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=1FlxS9MEgmNGH_RHUVHbO5hIXB5I1z0lvA33CTvYvjA%3D" alt="gutfuckllc" width="100"></td> | ||||
| </tr><tr> | ||||
| <td><a href="https://www.patreon.com/weepjp">weep</a></td> | ||||
| <td><a href="https://www.patreon.com/negao">negao</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=12059069">naga_rus</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td> | ||||
| <td><a href="https://www.patreon.com/Xeltica">Xeltica</a></td> | ||||
| @@ -115,6 +115,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). | ||||
| <td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=2PsbFNw0tnubZzgSXD01R6hIgncfiElG7H7HX2Y3dyo%3D" alt="nemu" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3?token-time=2145916800&token-hash=9JtETp0X8gI280Ne1E8bxn6j4Lw5o2k4mJkICx97V_k%3D" alt="YUKIMOCHI" width="100"></td> | ||||
| <td><img src="https://c8.patreon.com/2/200/17463605" alt="Sampot" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17195955/be45e5e14c3e48b2bee0456c84e19df4/4?token-time=2145916800&token-hash=SbdZeN5SmsuT9stD6v0jN1z0hftg0FmRiCTxysU0Ihw%3D" alt="Damillora" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8241184/39e18850e87a449e9c9a71acb3310ebd/3?token-time=2145916800&token-hash=gMq30aylxu5v3G8pRhWR5jeRBbYWEoRKjGbNeiCQz5g%3D" alt="Acid Chicken" width="100"></td> | ||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/2?token-time=2145916800&token-hash=zcwFxb2zopzWwksKVU1YpfAEjsl4yKT02aQ6yiAFRiQ%3D" alt="natalie" width="100"></td> | ||||
| @@ -125,6 +126,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). | ||||
| <td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td> | ||||
| <td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=17463605">Sampot</a></td> | ||||
| <td><a href="https://www.patreon.com/damillora">Damillora</a></td> | ||||
| <td><a href="https://www.patreon.com/acid_chicken">Acid Chicken</a></td> | ||||
| <td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td> | ||||
| @@ -142,7 +144,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). | ||||
| <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> | ||||
| </tr></table> | ||||
|  | ||||
| **Last updated:** Wed, 06 Feb 2019 18:18:05 UTC | ||||
| **Last updated:** Fri, 15 Feb 2019 19:12:06 UTC | ||||
| <!-- PATREON_END --> | ||||
|  | ||||
| :four_leaf_clover: Copyright | ||||
|   | ||||
| @@ -122,6 +122,8 @@ CentOSで1024以下のポートを使用してMisskeyを使用する場合は`Ex | ||||
| 4. `npm run build` | ||||
| 5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する | ||||
|  | ||||
| なにか問題が発生した場合は、`npm run clean`すると直る場合があります。 | ||||
|  | ||||
| ---------------------------------------------------------------- | ||||
|  | ||||
| なにかお困りのことがありましたらお気軽にご連絡ください。 | ||||
|   | ||||
| @@ -1591,13 +1591,13 @@ mobile/views/pages/user/home.vue: | ||||
|   activity: "アクティビティ" | ||||
|   keywords: "キーワード" | ||||
|   domains: "頻出ドメイン" | ||||
|   frequently-replied-users: "よく会話するユーザー" | ||||
|   frequently-replied-users: "よく話すユーザー" | ||||
|   followers-you-know: "知り合いのフォロワー" | ||||
|   last-used-at: "最終ログイン" | ||||
| mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "投稿はありません" | ||||
| mobile/views/pages/user/home.photos.vue: | ||||
|   | ||||
| @@ -1591,13 +1591,13 @@ mobile/views/pages/user/home.vue: | ||||
|   activity: "アクティビティ" | ||||
|   keywords: "Schlagwörter" | ||||
|   domains: "頻出ドメイン" | ||||
|   frequently-replied-users: "よく会話するユーザー" | ||||
|   frequently-replied-users: "よく話すユーザー" | ||||
|   followers-you-know: "知り合いのフォロワー" | ||||
|   last-used-at: "最終ログイン" | ||||
| mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "投稿はありません" | ||||
| mobile/views/pages/user/home.photos.vue: | ||||
|   | ||||
| @@ -1591,13 +1591,13 @@ mobile/views/pages/user/home.vue: | ||||
|   activity: "アクティビティ" | ||||
|   keywords: "キーワード" | ||||
|   domains: "頻出ドメイン" | ||||
|   frequently-replied-users: "よく会話するユーザー" | ||||
|   frequently-replied-users: "よく話すユーザー" | ||||
|   followers-you-know: "知り合いのフォロワー" | ||||
|   last-used-at: "最終ログイン" | ||||
| mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "投稿はありません" | ||||
| mobile/views/pages/user/home.photos.vue: | ||||
|   | ||||
| @@ -1591,13 +1591,13 @@ mobile/views/pages/user/home.vue: | ||||
|   activity: "アクティビティ" | ||||
|   keywords: "キーワード" | ||||
|   domains: "頻出ドメイン" | ||||
|   frequently-replied-users: "よく会話するユーザー" | ||||
|   frequently-replied-users: "よく話すユーザー" | ||||
|   followers-you-know: "知り合いのフォロワー" | ||||
|   last-used-at: "最終ログイン" | ||||
| mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "投稿はありません" | ||||
| mobile/views/pages/user/home.photos.vue: | ||||
|   | ||||
| @@ -28,6 +28,8 @@ common: | ||||
|   load-more: "もっと読み込む" | ||||
|   enter-password: "パスワードを入力してください" | ||||
|   2fa: "二段階認証" | ||||
|   customize-home: "ホームをカスタマイズ" | ||||
|   featured-notes: "ハイライト" | ||||
|  | ||||
|   got-it: "わかった" | ||||
|   customization-tips: | ||||
| @@ -58,6 +60,11 @@ common: | ||||
|   trash: "ゴミ箱" | ||||
|   drive: "ドライブ" | ||||
|   messaging: "トーク" | ||||
|   deck: "デッキ" | ||||
|   timeline: "タイムライン" | ||||
|   explore: "みつける" | ||||
|   following: "フォロー中" | ||||
|   followers: "フォロワー" | ||||
|  | ||||
|   weekday-short: | ||||
|     sunday: "日" | ||||
| @@ -213,6 +220,11 @@ auth/views/index.vue: | ||||
|   error: "セッションが存在しません。" | ||||
|   sign-in: "サインインしてください" | ||||
|  | ||||
| common/views/pages/explore.vue: | ||||
|   verified-users: "公式アカウント" | ||||
|   popular-users: "人気のユーザー" | ||||
|   recently-updated-users: "最近投稿したユーザー" | ||||
|  | ||||
| common/views/components/games/reversi/reversi.vue: | ||||
|   matching: | ||||
|     waiting-for: "{}を待っています" | ||||
| @@ -862,6 +874,9 @@ desktop/views/components/renote-form.vue: | ||||
| desktop/views/components/renote-form-window.vue: | ||||
|   title: "この投稿をRenoteしますか?" | ||||
|  | ||||
| desktop/views/components/timeline.core.vue: | ||||
|   empty: "投稿がありません" | ||||
|  | ||||
| desktop/views/pages/user-following-or-followers.vue: | ||||
|   following: "{user}のフォロー" | ||||
|   followers: "{user}のフォロワー" | ||||
| @@ -893,14 +908,10 @@ desktop/views/components/settings.vue: | ||||
|   web-search-engine-desc: "例: https://www.google.com/?#q={{query}}" | ||||
|   auto-popout: "ウィンドウの自動ポップアウト" | ||||
|   auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。" | ||||
|   deck-nav: "デッキ内ナビゲーション" | ||||
|   deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。" | ||||
|   keep-cw: "CW保持" | ||||
|   keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。" | ||||
|   deck-default: "デッキをデフォルトのUIにする" | ||||
|  | ||||
|   display: "デザインと表示" | ||||
|   customize: "ホームをカスタマイズ" | ||||
|   wallpaper: "壁紙" | ||||
|   choose-wallpaper: "壁紙を選択" | ||||
|   delete-wallpaper: "壁紙を削除" | ||||
| @@ -1076,15 +1087,12 @@ desktop/views/components/ui.header.account.vue: | ||||
|   favorites: "お気に入り" | ||||
|   lists: "リスト" | ||||
|   follow-requests: "フォロー申請" | ||||
|   customize: "ホームのカスタマイズ" | ||||
|   admin: "管理" | ||||
|   settings: "設定" | ||||
|   signout: "サインアウト" | ||||
|   dark: "闇に飲まれる" | ||||
|  | ||||
| desktop/views/components/ui.header.nav.vue: | ||||
|   home: "ホーム" | ||||
|   deck: "デッキ" | ||||
|   game: "ゲーム" | ||||
|  | ||||
| desktop/views/components/ui.header.notifications.vue: | ||||
| @@ -1447,9 +1455,6 @@ desktop/views/pages/welcome.vue: | ||||
| desktop/views/pages/drive.vue: | ||||
|   title: "Misskey Drive" | ||||
|  | ||||
| desktop/views/pages/home-customize.vue: | ||||
|   title: "ホームのカスタマイズ" | ||||
|  | ||||
| desktop/views/pages/note.vue: | ||||
|   prev: "前の投稿" | ||||
|   next: "次の投稿" | ||||
| @@ -1490,10 +1495,6 @@ desktop/views/pages/user/user.photos.vue: | ||||
|   loading: "読み込み中" | ||||
|   no-photos: "写真はありません" | ||||
|  | ||||
| desktop/views/pages/user/user.profile.vue: | ||||
|   follows-you: "フォローされています" | ||||
|   menu: "メニュー" | ||||
|  | ||||
| desktop/views/pages/user/user.header.vue: | ||||
|   posts: "投稿" | ||||
|   following: "フォロー" | ||||
| @@ -1503,6 +1504,7 @@ desktop/views/pages/user/user.header.vue: | ||||
|   year: "年" | ||||
|   month: "月" | ||||
|   day: "日" | ||||
|   follows-you: "フォローされています" | ||||
|  | ||||
| desktop/views/pages/user/user.timeline.vue: | ||||
|   default: "投稿" | ||||
| @@ -1661,10 +1663,6 @@ mobile/views/components/user-timeline.vue: | ||||
|   no-notes: "このユーザーは投稿していないようです。" | ||||
|   no-notes-with-media: "メディア付き投稿はありません。" | ||||
|  | ||||
| mobile/views/components/users-list.vue: | ||||
|   all: "すべて" | ||||
|   known: "知り合い" | ||||
|  | ||||
| mobile/views/pages/favorites.vue: | ||||
|   title: "お気に入り" | ||||
|  | ||||
| @@ -1689,6 +1687,9 @@ mobile/views/pages/home.vue: | ||||
|   mentions: "あなた宛て" | ||||
|   messages: "メッセージ" | ||||
|  | ||||
| mobile/views/pages/home.timeline.vue: | ||||
|   empty: "投稿がありません" | ||||
|  | ||||
| mobile/views/pages/tag.vue: | ||||
|   no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。" | ||||
|  | ||||
| @@ -1791,7 +1792,7 @@ mobile/views/pages/user/home.vue: | ||||
|   activity: "アクティビティ" | ||||
|   keywords: "キーワード" | ||||
|   domains: "頻出ドメイン" | ||||
|   frequently-replied-users: "よく会話するユーザー" | ||||
|   frequently-replied-users: "よく話すユーザー" | ||||
|   followers-you-know: "知り合いのフォロワー" | ||||
|   last-used-at: "最終ログイン" | ||||
|  | ||||
| @@ -1799,7 +1800,7 @@ mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
|  | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
|  | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "投稿はありません" | ||||
| @@ -1827,6 +1828,9 @@ deck: | ||||
|   rename: "名前を変更" | ||||
|   stack-left: "左に重ねる" | ||||
|   pop-right: "右に出す" | ||||
|   disabled-timeline: | ||||
|     title: "無効化されたタイムライン" | ||||
|     description: "サーバーの運営者により、このタイムラインは使用できない状態に設定されています。" | ||||
|  | ||||
| deck/deck.tl-column.vue: | ||||
|   is-media-only: "メディア投稿のみ" | ||||
|   | ||||
| @@ -1591,13 +1591,13 @@ mobile/views/pages/user/home.vue: | ||||
|   activity: "アクティビティ" | ||||
|   keywords: "Nøkkelord" | ||||
|   domains: "頻出ドメイン" | ||||
|   frequently-replied-users: "よく会話するユーザー" | ||||
|   frequently-replied-users: "よく話すユーザー" | ||||
|   followers-you-know: "知り合いのフォロワー" | ||||
|   last-used-at: "最終ログイン" | ||||
| mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "投稿はありません" | ||||
| mobile/views/pages/user/home.photos.vue: | ||||
|   | ||||
| @@ -1597,7 +1597,7 @@ mobile/views/pages/user/home.vue: | ||||
| mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "Nenhuma mensagem" | ||||
| mobile/views/pages/user/home.photos.vue: | ||||
|   | ||||
| @@ -1591,13 +1591,13 @@ mobile/views/pages/user/home.vue: | ||||
|   activity: "アクティビティ" | ||||
|   keywords: "キーワード" | ||||
|   domains: "頻出ドメイン" | ||||
|   frequently-replied-users: "よく会話するユーザー" | ||||
|   frequently-replied-users: "よく話すユーザー" | ||||
|   followers-you-know: "知り合いのフォロワー" | ||||
|   last-used-at: "最終ログイン" | ||||
| mobile/views/pages/user/home.followers-you-know.vue: | ||||
|   no-users: "知り合いのユーザーはいません" | ||||
| mobile/views/pages/user/home.friends.vue: | ||||
|   no-users: "よく会話するユーザーはいません" | ||||
|   no-users: "よく話すユーザーはいません" | ||||
| mobile/views/pages/user/home.notes.vue: | ||||
|   no-notes: "投稿はありません" | ||||
| mobile/views/pages/user/home.photos.vue: | ||||
|   | ||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <i@syuilo.com>", | ||||
| 	"version": "10.84.1", | ||||
| 	"clientVersion": "2.0.14252", | ||||
| 	"version": "10.86.2", | ||||
| 	"clientVersion": "2.0.14342", | ||||
| 	"codename": "nighthike", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| @@ -103,7 +103,7 @@ | ||||
| 		"bcryptjs": "2.4.3", | ||||
| 		"bee-queue": "1.2.2", | ||||
| 		"bootstrap-vue": "2.0.0-rc.11", | ||||
| 		"cafy": "12.1.0", | ||||
| 		"cafy": "14.0.1", | ||||
| 		"chai": "4.2.0", | ||||
| 		"chai-http": "4.2.1", | ||||
| 		"chalk": "2.4.2", | ||||
| @@ -229,11 +229,11 @@ | ||||
| 		"uuid": "3.3.2", | ||||
| 		"v-animate-css": "0.0.3", | ||||
| 		"video-thumbnail-generator": "1.1.3", | ||||
| 		"vue": "2.6.5", | ||||
| 		"vue": "2.6.6", | ||||
| 		"vue-color": "2.7.0", | ||||
| 		"vue-content-loading": "1.5.3", | ||||
| 		"vue-cropperjs": "3.0.0", | ||||
| 		"vue-i18n": "8.8.0", | ||||
| 		"vue-i18n": "8.8.1", | ||||
| 		"vue-js-modal": "1.3.28", | ||||
| 		"vue-loader": "15.6.2", | ||||
| 		"vue-marquee-text-component": "1.1.1", | ||||
| @@ -242,7 +242,7 @@ | ||||
| 		"vue-sequential-entrance": "1.1.3", | ||||
| 		"vue-style-loader": "4.1.2", | ||||
| 		"vue-svg-inline-loader": "1.2.10", | ||||
| 		"vue-template-compiler": "2.6.5", | ||||
| 		"vue-template-compiler": "2.6.6", | ||||
| 		"vuedraggable": "2.17.0", | ||||
| 		"vuewordcloud": "18.7.11", | ||||
| 		"vuex": "3.1.0", | ||||
|   | ||||
| @@ -339,7 +339,7 @@ export default Vue.extend({ | ||||
| 			}); | ||||
|  | ||||
| 			return !confirm.canceled; | ||||
| 		} | ||||
| 		}, | ||||
|  | ||||
| 		fetchUsers() { | ||||
| 			this.$root.api('admin/show-users', { | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import { url as instanceUrl } from '../../config'; | ||||
| import * as url from '../../../../prelude/url'; | ||||
|  | ||||
| export function getStaticImageUrl(url: string): string { | ||||
| 	const u = new URL(url); | ||||
| export function getStaticImageUrl(baseUrl: string): string { | ||||
| 	const u = new URL(baseUrl); | ||||
| 	const dummy = `${u.host}${u.pathname}`;	// 拡張子がないとキャッシュしてくれないCDNがあるので | ||||
| 	let result = `${instanceUrl}/proxy/${dummy}?url=${encodeURIComponent(u.href)}`; | ||||
| 	result += '&static=1'; | ||||
| 	return result; | ||||
| 	return `${instanceUrl}/proxy/${dummy}?${url.query({ | ||||
| 		url: u.href, | ||||
| 		static: '1' | ||||
| 	})}`; | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,8 @@ export default function(me, settings, note) { | ||||
|  | ||||
| 	const includesMutedWords = (text: string) => | ||||
| 		text | ||||
| 			? settings.mutedWords.some(q => q.length > 0 && !q.some(word => !text.includes(word))) | ||||
| 			? settings.mutedWords.some(q => q.length > 0 && !q.some(word => | ||||
| 				word.startsWith('/') && word.endsWith('/') ? !(new RegExp(word.substr(1, word.length - 2)).test(text)) : !text.includes(word))) | ||||
| 			: false; | ||||
|  | ||||
| 	return ( | ||||
|   | ||||
							
								
								
									
										18
									
								
								src/client/app/common/size.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/client/app/common/size.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| export default { | ||||
| 	install(Vue) { | ||||
| 		Vue.directive('size', { | ||||
| 			inserted(el, binding) { | ||||
| 				const query = binding.value; | ||||
| 				const width = el.clientWidth; | ||||
| 				for (const q of query) { | ||||
| 					if (q.lt && (width <= q.lt)) { | ||||
| 						el.classList.add(q.class); | ||||
| 					} | ||||
| 					if (q.gt && (width >= q.gt)) { | ||||
| 						el.classList.add(q.class); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
| <div class="mk-activity"> | ||||
| <div> | ||||
| 	<div ref="chart"></div> | ||||
| </div> | ||||
| </template> | ||||
| @@ -9,7 +9,17 @@ import Vue from 'vue'; | ||||
| import ApexCharts from 'apexcharts'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: ['user'], | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		limit: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: 21 | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| @@ -21,7 +31,7 @@ export default Vue.extend({ | ||||
| 		this.$root.api('charts/user/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			span: 'day', | ||||
| 			limit: 21 | ||||
| 			limit: this.limit | ||||
| 		}).then(stats => { | ||||
| 			const normal = []; | ||||
| 			const reply = []; | ||||
| @@ -32,7 +42,7 @@ export default Vue.extend({ | ||||
| 			const m = now.getMonth(); | ||||
| 			const d = now.getDate(); | ||||
| 
 | ||||
| 			for (let i = 0; i < 21; i++) { | ||||
| 			for (let i = 0; i < this.limit; i++) { | ||||
| 				const x = new Date(y, m, d - i); | ||||
| 				normal.push([ | ||||
| 					x, | ||||
| @@ -99,10 +109,3 @@ export default Vue.extend({ | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| .mk-activity | ||||
| 	max-width 600px | ||||
| 	margin 0 auto | ||||
| 
 | ||||
| </style> | ||||
							
								
								
									
										11
									
								
								src/client/app/common/views/components/dummy.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/client/app/common/views/components/dummy.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<slot></slot> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| }); | ||||
| </script> | ||||
| @@ -1,8 +1,9 @@ | ||||
| <template> | ||||
| <button class="wfliddvnhxvyusikowhxozkyxyenqxqr" | ||||
| 	:class="{ wait, block, mini, active: isFollowing || hasPendingFollowRequestFromYou }" | ||||
| 	:class="{ wait, block, inline, mini, active: isFollowing || hasPendingFollowRequestFromYou }" | ||||
| 	@click="onClick" | ||||
| 	:disabled="wait" | ||||
| 	:inline="inline" | ||||
| > | ||||
| 	<template v-if="!wait"> | ||||
| 		<fa :icon="iconAndText[0]"/> <template v-if="!mini">{{ iconAndText[1] }}</template> | ||||
| @@ -28,6 +29,11 @@ export default Vue.extend({ | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		inline: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		mini: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| @@ -128,6 +134,9 @@ export default Vue.extend({ | ||||
| 	border solid 1px var(--primary) | ||||
| 	border-radius 36px | ||||
|  | ||||
| 	&.inline | ||||
| 		display inline-block | ||||
|  | ||||
| 	&.mini | ||||
| 		padding 0 | ||||
| 		min-width 0 | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| import dummy from './dummy.vue'; | ||||
| import userName from './user-name.vue'; | ||||
| import followButton from './follow-button.vue'; | ||||
| import error from './error.vue'; | ||||
| @@ -32,6 +33,7 @@ import urlPreview from './url-preview.vue'; | ||||
| import fileTypeIcon from './file-type-icon.vue'; | ||||
| import emoji from './emoji.vue'; | ||||
| import welcomeTimeline from './welcome-timeline.vue'; | ||||
| import userList from './user-list.vue'; | ||||
| import uiInput from './ui/input.vue'; | ||||
| import uiButton from './ui/button.vue'; | ||||
| import uiHorizonGroup from './ui/horizon-group.vue'; | ||||
| @@ -46,6 +48,7 @@ import formButton from './ui/form/button.vue'; | ||||
| import formRadio from './ui/form/radio.vue'; | ||||
|  | ||||
| Vue.component('mfm', misskeyFlavoredMarkdown); | ||||
| Vue.component('mk-dummy', dummy); | ||||
| Vue.component('mk-user-name', userName); | ||||
| Vue.component('mk-follow-button', followButton); | ||||
| Vue.component('mk-error', error); | ||||
| @@ -77,6 +80,7 @@ Vue.component('mk-url-preview', urlPreview); | ||||
| Vue.component('mk-file-type-icon', fileTypeIcon); | ||||
| Vue.component('mk-emoji', emoji); | ||||
| Vue.component('mk-welcome-timeline', welcomeTimeline); | ||||
| Vue.component('mk-user-list', userList); | ||||
| Vue.component('ui-input', uiInput); | ||||
| Vue.component('ui-button', uiButton); | ||||
| Vue.component('ui-horizon-group', uiHorizonGroup); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 		<b>{{ $t('sensitive') }}</b> | ||||
| 		<span>{{ $t('click-to-show') }}</span> | ||||
| 	</div> | ||||
| 	<div class="audio" v-else-if="media.type.startsWith('audio')"> | ||||
| 	<div class="audio" v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'"> | ||||
| 		<audio class="audio" | ||||
| 			:src="media.url" | ||||
| 			:title="media.name" | ||||
|   | ||||
| @@ -10,7 +10,9 @@ | ||||
| 	:style="style" | ||||
| 	:title="image.name" | ||||
| 	@click.prevent="onClick" | ||||
| ></a> | ||||
| > | ||||
| 	<div v-if="image.type === 'image/gif'">GIF</div> | ||||
| </a> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| @@ -76,6 +78,20 @@ export default Vue.extend({ | ||||
| 	background-size contain | ||||
| 	background-repeat no-repeat | ||||
|  | ||||
| 	> div | ||||
| 		background-color var(--text) | ||||
| 		border-radius var(--round) | ||||
| 		color var(--secondary) | ||||
| 		display inline-block | ||||
| 		font-size 14px | ||||
| 		font-weight bold | ||||
| 		left 12px | ||||
| 		opacity .5 | ||||
| 		padding 0 6px | ||||
| 		text-align center | ||||
| 		top 12px | ||||
| 		pointer-events none | ||||
|  | ||||
| .qjewsnkgzzxlxtzncydssfbgjibiehcy | ||||
| 	display flex | ||||
| 	justify-content center | ||||
|   | ||||
							
								
								
									
										161
									
								
								src/client/app/common/views/components/user-list.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/client/app/common/views/components/user-list.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| <template> | ||||
| <ui-container :body-togglable="true"> | ||||
| 	<template slot="header"><slot></slot></template> | ||||
|  | ||||
| 	<mk-error v-if="!fetching && !inited" @retry="init()"/> | ||||
|  | ||||
| 	<div class="efvhhmdq" v-size="[{ lt: 500, class: 'narrow' }]"> | ||||
| 		<div class="user" v-for="user in us"> | ||||
| 			<mk-avatar class="avatar" :user="user"/> | ||||
| 			<div class="body"> | ||||
| 				<div class="name"> | ||||
| 					<router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link> | ||||
| 					<p class="username">@{{ user | acct }}</p> | ||||
| 				</div> | ||||
| 				<div class="description" v-if="user.description" :title="user.description"> | ||||
| 					<mfm :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<button class="more" :class="{ fetching: fetchingMoreUsers }" v-if="cursor != null" @click="fetchMoreUsers()" :disabled="fetchingMoreUsers"> | ||||
| 			<template v-if="fetchingMoreUsers"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreUsers ? $t('@.loading') : $t('@.load-more') }} | ||||
| 		</button> | ||||
| 	</div> | ||||
| </ui-container> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		makePromise: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 		iconOnly: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			fetchingMoreUsers: false, | ||||
| 			us: [], | ||||
| 			inited: false, | ||||
| 			cursor: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.init(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		init() { | ||||
| 			this.fetching = true; | ||||
| 			this.makePromise().then(x => { | ||||
| 				if (Array.isArray(x)) { | ||||
| 					this.us = x; | ||||
| 				} else { | ||||
| 					this.us = x.users; | ||||
| 					this.cursor = x.cursor; | ||||
| 				} | ||||
| 				this.inited = true; | ||||
| 				this.fetching = false; | ||||
| 			}, e => { | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		fetchMoreUsers() { | ||||
| 			this.fetchingMoreUsers = true; | ||||
| 			this.makePromise(this.cursor).then(x => { | ||||
| 				this.us = this.us.concat(x.users); | ||||
| 				this.cursor = x.cursor; | ||||
| 				this.fetchingMoreUsers = false; | ||||
| 			}, e => { | ||||
| 				this.fetchingMoreUsers = false; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .efvhhmdq | ||||
| 	&.narrow | ||||
| 		> .user > .body > .name | ||||
| 			width 100% | ||||
|  | ||||
| 		> .user > .body > .description | ||||
| 			display none | ||||
|  | ||||
| 	> .user | ||||
| 		display flex | ||||
| 		padding 16px | ||||
| 		border-bottom solid 1px var(--faceDivider) | ||||
|  | ||||
| 		&:last-child | ||||
| 			border-bottom none | ||||
|  | ||||
| 		> .avatar | ||||
| 			display block | ||||
| 			flex-shrink 0 | ||||
| 			margin 0 12px 0 0 | ||||
| 			width 42px | ||||
| 			height 42px | ||||
| 			border-radius 8px | ||||
|  | ||||
| 		> .body | ||||
| 			display flex | ||||
| 			width calc(100% - 54px) | ||||
|  | ||||
| 			> .name | ||||
| 				width 45% | ||||
|  | ||||
| 				> .name | ||||
| 					margin 0 | ||||
| 					font-size 16px | ||||
| 					line-height 24px | ||||
| 					color var(--text) | ||||
|  | ||||
| 				> .username | ||||
| 					display block | ||||
| 					margin 0 | ||||
| 					font-size 15px | ||||
| 					line-height 16px | ||||
| 					color var(--text) | ||||
| 					opacity 0.7 | ||||
|  | ||||
| 			> .description | ||||
| 				width 55% | ||||
| 				color var(--text) | ||||
| 				line-height 42px | ||||
| 				white-space nowrap | ||||
| 				overflow hidden | ||||
| 				text-overflow ellipsis | ||||
| 				opacity 0.7 | ||||
| 				font-size 14px | ||||
|  | ||||
| 	> .more | ||||
| 		display block | ||||
| 		width 100% | ||||
| 		padding 16px | ||||
| 		color var(--text) | ||||
| 		border-top solid var(--lineWidth) rgba(#000, 0.05) | ||||
|  | ||||
| 		&:hover | ||||
| 			background rgba(#000, 0.025) | ||||
|  | ||||
| 		&:active | ||||
| 			background rgba(#000, 0.05) | ||||
|  | ||||
| 		&.fetching | ||||
| 			cursor wait | ||||
|  | ||||
| 		> [data-icon] | ||||
| 			margin-right 4px | ||||
|  | ||||
| </style> | ||||
| @@ -76,6 +76,7 @@ export default Vue.extend({ | ||||
| 			if (note.replyId != null) return; | ||||
| 			if (note.renoteId != null) return; | ||||
| 			if (note.poll != null) return; | ||||
| 			if (note.localOnly) return; | ||||
|  | ||||
| 			this.notes.unshift(note); | ||||
| 		}, | ||||
|   | ||||
							
								
								
									
										50
									
								
								src/client/app/common/views/pages/explore.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/client/app/common/views/pages/explore.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<mk-user-list :make-promise="verifiedUsers"> | ||||
| 		<span><fa :icon="faBookmark"/> {{ $t('verified-users') }}</span> | ||||
| 	</mk-user-list> | ||||
| 	<mk-user-list :make-promise="popularUsers"> | ||||
| 		<span><fa :icon="faChartLine"/> {{ $t('popular-users') }}</span> | ||||
| 	</mk-user-list> | ||||
| 	<mk-user-list :make-promise="recentlyUpdatedUsers"> | ||||
| 		<span><fa :icon="faCommentAlt"/> {{ $t('recently-updated-users') }}</span> | ||||
| 	</mk-user-list> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import { faChartLine } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/pages/explore.vue'), | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			verifiedUsers: () => this.$root.api('users', { | ||||
| 				state: 'verified', | ||||
| 				origin: 'local', | ||||
| 				sort: '+follower', | ||||
| 				limit: 10 | ||||
| 			}), | ||||
| 			popularUsers: () => this.$root.api('users', { | ||||
| 				state: 'alive', | ||||
| 				origin: 'local', | ||||
| 				sort: '+follower', | ||||
| 				limit: 10 | ||||
| 			}), | ||||
| 			recentlyUpdatedUsers: () => this.$root.api('users', { | ||||
| 				origin: 'local', | ||||
| 				sort: '+updatedAt', | ||||
| 				limit: 10 | ||||
| 			}), | ||||
| 			faBookmark, faChartLine, faCommentAlt | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| </style> | ||||
							
								
								
									
										30
									
								
								src/client/app/common/views/pages/followers.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/client/app/common/views/pages/followers.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<mk-user-list :make-promise="makePromise">{{ $t('@.followers') }}</mk-user-list> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import i18n from '../../../i18n'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(''), | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			makePromise: cursor => this.$root.api('users/followers', { | ||||
| 				...parseAcct(this.$route.params.user), | ||||
| 				limit: 30, | ||||
| 				cursor: cursor ? cursor : undefined | ||||
| 			}).then(x => { | ||||
| 				return { | ||||
| 					users: x.users, | ||||
| 					cursor: x.next | ||||
| 				}; | ||||
| 			}), | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										27
									
								
								src/client/app/common/views/pages/following.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/client/app/common/views/pages/following.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<mk-user-list :make-promise="makePromise">{{ $t('@.following') }}</mk-user-list> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			makePromise: cursor => this.$root.api('users/following', { | ||||
| 				...parseAcct(this.$route.params.user), | ||||
| 				limit: 30, | ||||
| 				cursor: cursor ? cursor : undefined | ||||
| 			}).then(x => { | ||||
| 				return { | ||||
| 					users: x.users, | ||||
| 					cursor: x.next | ||||
| 				}; | ||||
| 			}), | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| @@ -1,10 +1,10 @@ | ||||
| <template> | ||||
| <div class="mkw-analog-clock"> | ||||
| 	<mk-widget-container :naked="props.style % 2 === 0" :show-header="false"> | ||||
| 	<ui-container :naked="props.style % 2 === 0" :show-header="false"> | ||||
| 		<div class="mkw-analog-clock--body"> | ||||
| 			<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="anltbovirfeutcigvwgmgxipejaeozxi"> | ||||
| 	<mk-widget-container :show-header="false" :naked="props.design == 1"> | ||||
| 	<ui-container :show-header="false" :naked="props.design == 1"> | ||||
| 		<div class="anltbovirfeutcigvwgmgxipejaeozxi-body" | ||||
| 			:data-found="announcements && announcements.length != 0" | ||||
| 			:data-melt="props.design == 1" | ||||
| @@ -23,7 +23,7 @@ | ||||
| 			</p> | ||||
| 			<a v-if="announcements.length > 1" @click="next">{{ $t('next') }} >></a> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'"> | ||||
| 	<mk-widget-container :naked="props.design == 1" :show-header="false"> | ||||
| 	<ui-container :naked="props.design == 1" :show-header="false"> | ||||
| 		<div class="mkw-calendar--body"> | ||||
| 			<div class="calendar" :data-is-holiday="isHoliday"> | ||||
| 				<p class="month-and-year"> | ||||
| @@ -31,7 +31,7 @@ | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| <template> | ||||
| <div class="mkw-hashtags"> | ||||
| 	<mk-widget-container :show-header="!props.compact"> | ||||
| 	<ui-container :show-header="!props.compact"> | ||||
| 		<template slot="header"><fa icon="hashtag"/>{{ $t('title') }}</template> | ||||
|  | ||||
| 		<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'"> | ||||
| 			<mk-trends/> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import wSlideshow from './slideshow.vue'; | ||||
| import wTips from './tips.vue'; | ||||
| import wNav from './nav.vue'; | ||||
| import wHashtags from './hashtags.vue'; | ||||
| import wInstance from './instance.vue'; | ||||
|  | ||||
| Vue.component('mkw-analog-clock', wAnalogClock); | ||||
| Vue.component('mkw-nav', wNav); | ||||
| @@ -27,3 +28,4 @@ Vue.component('mkw-memo', wMemo); | ||||
| Vue.component('mkw-rss', wRss); | ||||
| Vue.component('mkw-version', wVersion); | ||||
| Vue.component('mkw-hashtags', wHashtags); | ||||
| Vue.component('mkw-instance', wInstance); | ||||
|   | ||||
							
								
								
									
										14
									
								
								src/client/app/common/views/widgets/instance.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/client/app/common/views/widgets/instance.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| <template> | ||||
| <div class="mkw-instance"> | ||||
| 	<ui-container> | ||||
| 		<mk-instance/> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import define from '../../../common/define-widget'; | ||||
| export default define({ | ||||
| 	name: 'instance' | ||||
| }); | ||||
| </script> | ||||
| @@ -1,13 +1,13 @@ | ||||
| <template> | ||||
| <div class="mkw-memo"> | ||||
| 	<mk-widget-container :show-header="!props.compact"> | ||||
| 	<ui-container :show-header="!props.compact"> | ||||
| 		<template slot="header"><fa :icon="['far', 'sticky-note']"/>{{ $t('title') }}</template> | ||||
|  | ||||
| 		<div class="mkw-memo--body"> | ||||
| 			<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> | ||||
| 			<button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| <template> | ||||
| <div class="mkw-nav"> | ||||
| 	<mk-widget-container> | ||||
| 	<ui-container> | ||||
| 		<div class="mkw-nav--body"> | ||||
| 			<mk-nav/> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2"> | ||||
| 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||
| 	<ui-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||
| 		<template slot="header"><fa icon="camera"/>{{ $t('title') }}</template> | ||||
|  | ||||
| 		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||
| @@ -13,7 +13,7 @@ | ||||
| 			></div> | ||||
| 		</div> | ||||
| 		<p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mkw-posts-monitor"> | ||||
| 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||
| 	<ui-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||
| 		<template slot="header"><fa icon="chart-line"/>{{ $t('title') }}</template> | ||||
| 		<button slot="func" @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button> | ||||
|  | ||||
| @@ -64,7 +64,7 @@ | ||||
| 				<text x="1" y="5">Fedi</text> | ||||
| 			</svg> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mkw-rss"> | ||||
| 	<mk-widget-container :show-header="!props.compact"> | ||||
| 	<ui-container :show-header="!props.compact"> | ||||
| 		<template slot="header"><fa icon="rss-square"/>RSS</template> | ||||
| 		<button slot="func" title="設定" @click="setting"><fa icon="cog"/></button> | ||||
|  | ||||
| @@ -10,7 +10,7 @@ | ||||
| 				<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mkw-server"> | ||||
| 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||
| 	<ui-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||
| 		<template slot="header"><fa icon="server"/>{{ $t('title') }}</template> | ||||
| 		<button slot="func" @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button> | ||||
|  | ||||
| @@ -13,7 +13,7 @@ | ||||
| 			<x-uptimes v-show="props.view == 4" :connection="connection"/> | ||||
| 			<x-info v-show="props.view == 5" :connection="connection" :meta="meta"/> | ||||
| 		</template> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -12,19 +12,12 @@ import init from '../init'; | ||||
| import fuckAdBlock from '../common/scripts/fuck-ad-block'; | ||||
| import composeNotification from '../common/scripts/compose-notification'; | ||||
|  | ||||
| import MkIndex from './views/pages/index.vue'; | ||||
| import MkHome from './views/pages/home.vue'; | ||||
| import MkDeck from './views/pages/deck/deck.vue'; | ||||
| import MkUser from './views/pages/user/user.vue'; | ||||
| import MkHome from './views/home/home.vue'; | ||||
| import MkDeck from './views/deck/deck.vue'; | ||||
| import MkUserFollowingOrFollowers from './views/pages/user-following-or-followers.vue'; | ||||
| import MkFavorites from './views/pages/favorites.vue'; | ||||
| import MkSelectDrive from './views/pages/selectdrive.vue'; | ||||
| import MkDrive from './views/pages/drive.vue'; | ||||
| 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 MkReversi from './views/pages/games/reversi.vue'; | ||||
| import MkShare from './views/pages/share.vue'; | ||||
| import MkFollow from '../common/views/pages/follow.vue'; | ||||
| @@ -36,6 +29,7 @@ import PostFormWindow from './views/components/post-form-window.vue'; | ||||
| import RenoteFormWindow from './views/components/renote-form-window.vue'; | ||||
| import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue'; | ||||
| import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue'; | ||||
| import MkHomeTimeline from './views/home/timeline.vue'; | ||||
| import Notification from './views/components/ui-notification.vue'; | ||||
|  | ||||
| import { url } from '../config'; | ||||
| @@ -44,7 +38,7 @@ import MiOS from '../mios'; | ||||
| /** | ||||
|  * init | ||||
|  */ | ||||
| init(async (launch) => { | ||||
| init(async (launch, os) => { | ||||
| 	Vue.mixin({ | ||||
| 		methods: { | ||||
| 			$contextmenu(e, menu, opts?) { | ||||
| @@ -134,31 +128,52 @@ init(async (launch) => { | ||||
| 	const router = new VueRouter({ | ||||
| 		mode: 'history', | ||||
| 		routes: [ | ||||
| 			{ path: '/', name: 'index', component: MkIndex }, | ||||
| 			{ path: '/home', name: 'home', component: MkHome }, | ||||
| 			{ path: '/deck', name: 'deck', component: MkDeck }, | ||||
| 			{ path: '/i/customize-home', component: MkHomeCustomize }, | ||||
| 			{ path: '/i/favorites', component: MkFavorites }, | ||||
| 			os.store.getters.isSignedIn && os.store.state.device.deckMode && document.location.pathname === '/' | ||||
| 				? { path: '/', name: 'index', component: MkDeck, children: [ | ||||
| 					{ path: '/@:user', name: 'user', component: () => import('./views/deck/deck.user-column.vue').then(m => m.default), children: [ | ||||
| 						{ path: '', name: 'user', component: () => import('./views/deck/deck.user-column.home.vue').then(m => m.default) }, | ||||
| 						{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, | ||||
| 						{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, | ||||
| 					]}, | ||||
| 					{ path: '/notes/:note', name: 'note', component: () => import('./views/deck/deck.note-column.vue').then(m => m.default) }, | ||||
| 					{ path: '/search', component: () => import('./views/deck/deck.search-column.vue').then(m => m.default) }, | ||||
| 					{ path: '/tags/:tag', name: 'tag', component: () => import('./views/deck/deck.hashtag-column.vue').then(m => m.default) }, | ||||
| 					{ path: '/featured', component: () => import('./views/deck/deck.featured-column.vue').then(m => m.default) }, | ||||
| 					{ path: '/explore', component: () => import('./views/deck/deck.explore-column.vue').then(m => m.default) }, | ||||
| 					{ path: '/i/favorites', component: () => import('./views/deck/deck.favorites-column.vue').then(m => m.default) } | ||||
| 				]} | ||||
| 				: { path: '/', component: MkHome, children: [ | ||||
| 					{ path: '', name: 'index', component: MkHomeTimeline }, | ||||
| 					{ path: '/@:user', component: () => import('./views/home/user/index.vue').then(m => m.default), children: [ | ||||
| 						{ path: '', name: 'user', component: () => import('./views/home/user/user.home.vue').then(m => m.default) }, | ||||
| 						{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, | ||||
| 						{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, | ||||
| 					]}, | ||||
| 					{ path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) }, | ||||
| 					{ path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) }, | ||||
| 					{ path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) }, | ||||
| 					{ path: '/featured', name: 'featured', component: () => import('./views/home/featured.vue').then(m => m.default) }, | ||||
| 					{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, | ||||
| 					{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, | ||||
| 				]}, | ||||
| 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | ||||
| 			{ path: '/i/drive', component: MkDrive }, | ||||
| 			{ path: '/i/drive/folder/:folder', component: MkDrive }, | ||||
| 			{ path: '/i/settings', component: MkSettings }, | ||||
| 			{ path: '/selectdrive', component: MkSelectDrive }, | ||||
| 			{ path: '/search', component: MkSearch }, | ||||
| 			{ path: '/tags/:tag', name: 'tag', component: MkTag }, | ||||
| 			{ path: '/share', component: MkShare }, | ||||
| 			{ path: '/games/reversi/:game?', component: MkReversi }, | ||||
| 			{ path: '/@:user', name: 'user', component: MkUser }, | ||||
| 			{ path: '/@:user/following', name: 'userFollowing', component: MkUserFollowingOrFollowers }, | ||||
| 			{ path: '/@:user/followers', name: 'userFollowers', component: MkUserFollowingOrFollowers }, | ||||
| 			{ path: '/notes/:note', name: 'note', component: MkNote }, | ||||
| 			{ path: '/authorize-follow', component: MkFollow }, | ||||
| 			{ path: '/deck', redirect: '/' }, | ||||
| 			{ path: '*', component: MkNotFound } | ||||
| 		] | ||||
| 		], | ||||
| 		scrollBehavior(to, from, savedPosition) { | ||||
| 			return { x: 0, y: 0 }; | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	// Launch the app | ||||
| 	const [app, os] = launch(router); | ||||
| 	const [app, _] = launch(router); | ||||
|  | ||||
| 	if (os.store.getters.isSignedIn) { | ||||
| 		/** | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div class="mk-activity"> | ||||
| 	<mk-widget-container :show-header="design == 0" :naked="design == 2"> | ||||
| 	<ui-container :show-header="design == 0" :naked="design == 2"> | ||||
| 		<template slot="header"><fa icon="chart-bar"/>{{ $t('title') }}</template> | ||||
| 		<button slot="func" :title="$t('toggle')" @click="toggle"><fa icon="sort"/></button> | ||||
|  | ||||
| @@ -9,7 +9,7 @@ | ||||
| 			<x-calendar v-show="view == 0" :data="[].concat(activity)"/> | ||||
| 			<x-chart v-show="view == 1" :data="[].concat(activity)"/> | ||||
| 		</template> | ||||
| 	</mk-widget-container> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -160,6 +160,7 @@ export default Vue.extend({ | ||||
| 			color #222 | ||||
|  | ||||
| 		> [data-icon] | ||||
| 			box-sizing initial | ||||
| 			padding 14px | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -1,396 +0,0 @@ | ||||
| <template> | ||||
| <div class="mk-home" :data-customize="customize"> | ||||
| 	<div class="customize" v-if="customize"> | ||||
| 		<router-link to="/"><fa icon="check"/>{{ $t('done') }}</router-link> | ||||
| 		<div> | ||||
| 			<div class="adder"> | ||||
| 				<p>{{ $t('add-widget') }}</p> | ||||
| 				<select v-model="widgetAdderSelected"> | ||||
| 					<option value="profile">{{ $t('@.widgets.profile') }}</option> | ||||
| 					<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> | ||||
| 					<option value="calendar">{{ $t('@.widgets.calendar') }}</option> | ||||
| 					<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option> | ||||
| 					<option value="activity">{{ $t('@.widgets.activity') }}</option> | ||||
| 					<option value="rss">{{ $t('@.widgets.rss') }}</option> | ||||
| 					<option value="trends">{{ $t('@.widgets.trends') }}</option> | ||||
| 					<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> | ||||
| 					<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> | ||||
| 					<option value="version">{{ $t('@.widgets.version') }}</option> | ||||
| 					<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option> | ||||
| 					<option value="notifications">{{ $t('@.widgets.notifications') }}</option> | ||||
| 					<option value="users">{{ $t('@.widgets.users') }}</option> | ||||
| 					<option value="polls">{{ $t('@.widgets.polls') }}</option> | ||||
| 					<option value="post-form">{{ $t('@.widgets.post-form') }}</option> | ||||
| 					<option value="messaging">{{ $t('@.widgets.messaging') }}</option> | ||||
| 					<option value="memo">{{ $t('@.widgets.memo') }}</option> | ||||
| 					<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> | ||||
| 					<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> | ||||
| 					<option value="server">{{ $t('@.widgets.server') }}</option> | ||||
| 					<option value="nav">{{ $t('@.widgets.nav') }}</option> | ||||
| 					<option value="tips">{{ $t('@.widgets.tips') }}</option> | ||||
| 				</select> | ||||
| 				<button @click="addWidget">{{ $t('add') }}</button> | ||||
| 			</div> | ||||
| 			<div class="trash"> | ||||
| 				<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable> | ||||
| 				<p>{{ $t('@.trash') }}</p> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }"> | ||||
| 		<template v-if="customize"> | ||||
| 			<x-draggable v-for="place in ['left', 'right']" | ||||
| 				:list="widgets[place]" | ||||
| 				:class="place" | ||||
| 				:data-place="place" | ||||
| 				:options="{ group: 'x', animation: 150 }" | ||||
| 				@sort="onWidgetSort" | ||||
| 				:key="place" | ||||
| 			> | ||||
| 				<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)"> | ||||
| 					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/> | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| 			<div class="main"> | ||||
| 				<a @click="hint">{{ $t('@.customization-tips.title') }}</a> | ||||
| 				<div> | ||||
| 					<mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/> | ||||
| 					<mk-timeline ref="tl" @loaded="onTlLoaded"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 		<template v-else> | ||||
| 			<div v-for="place in ['left', 'right']" :class="place"> | ||||
| 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp" platform="desktop"/> | ||||
| 			</div> | ||||
| 			<div class="main"> | ||||
| 				<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/> | ||||
| 				<mk-timeline class="tl" ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import * as uuid from 'uuid'; | ||||
|  | ||||
| const defaultDesktopHomeWidgets = { | ||||
| 	left: [ | ||||
| 		'profile', | ||||
| 		'calendar', | ||||
| 		'activity', | ||||
| 		'rss', | ||||
| 		'hashtags', | ||||
| 		'photo-stream', | ||||
| 		'version' | ||||
| 	], | ||||
| 	right: [ | ||||
| 		'broadcast', | ||||
| 		'notifications', | ||||
| 		'users', | ||||
| 		'polls', | ||||
| 		'server', | ||||
| 		'nav', | ||||
| 		'tips' | ||||
| 	] | ||||
| }; | ||||
|  | ||||
| //#region Construct home data | ||||
| const _defaultDesktopHomeWidgets = []; | ||||
|  | ||||
| for (const widget of defaultDesktopHomeWidgets.left) { | ||||
| 	_defaultDesktopHomeWidgets.push({ | ||||
| 		name: widget, | ||||
| 		id: uuid(), | ||||
| 		place: 'left', | ||||
| 		data: {} | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| for (const widget of defaultDesktopHomeWidgets.right) { | ||||
| 	_defaultDesktopHomeWidgets.push({ | ||||
| 		name: widget, | ||||
| 		id: uuid(), | ||||
| 		place: 'right', | ||||
| 		data: {} | ||||
| 	}); | ||||
| } | ||||
| //#endregion | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/home.vue'), | ||||
| 	components: { | ||||
| 		XDraggable | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		customize: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		mode: { | ||||
| 			type: String, | ||||
| 			default: 'timeline' | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			connection: null, | ||||
| 			widgetAdderSelected: null, | ||||
| 			trash: [] | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		home(): any[] { | ||||
| 			return this.$store.state.settings.home || []; | ||||
| 		}, | ||||
| 		left(): any[] { | ||||
| 			return this.home.filter(w => w.place == 'left'); | ||||
| 		}, | ||||
| 		right(): any[] { | ||||
| 			return this.home.filter(w => w.place == 'right'); | ||||
| 		}, | ||||
| 		widgets(): any { | ||||
| 			return { | ||||
| 				left: this.left, | ||||
| 				right: this.right | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		if (this.$store.state.settings.home == null) { | ||||
| 			this.$root.api('i/update_home', { | ||||
| 				home: _defaultDesktopHomeWidgets | ||||
| 			}).then(() => { | ||||
| 				this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets); | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		hint() { | ||||
| 			this.$root.dialog({ | ||||
| 				title: this.$t('@.customization-tips.title'), | ||||
| 				text: this.$t('@.customization-tips.paragraph') | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onTlLoaded() { | ||||
| 			this.$emit('loaded'); | ||||
| 		}, | ||||
|  | ||||
| 		onWidgetContextmenu(widgetId) { | ||||
| 			const w = (this.$refs[widgetId] as any)[0]; | ||||
| 			if (w.func) w.func(); | ||||
| 		}, | ||||
|  | ||||
| 		onWidgetSort() { | ||||
| 			this.saveHome(); | ||||
| 		}, | ||||
|  | ||||
| 		onTrash(evt) { | ||||
| 			this.saveHome(); | ||||
| 		}, | ||||
|  | ||||
| 		addWidget() { | ||||
| 			this.$store.dispatch('settings/addHomeWidget', { | ||||
| 				name: this.widgetAdderSelected, | ||||
| 				id: uuid(), | ||||
| 				place: 'left', | ||||
| 				data: {} | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		saveHome() { | ||||
| 			const left = this.widgets.left; | ||||
| 			const right = this.widgets.right; | ||||
| 			this.$store.commit('settings/setHome', left.concat(right)); | ||||
| 			for (const w of left) w.place = 'left'; | ||||
| 			for (const w of right) w.place = 'right'; | ||||
| 			this.$root.api('i/update_home', { | ||||
| 				home: this.home | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		warp(date) { | ||||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-home | ||||
| 	display block | ||||
|  | ||||
| 	&[data-customize] | ||||
| 		padding-top 48px | ||||
| 		background-image url('/assets/desktop/grid.svg') | ||||
|  | ||||
| 		> .main > .main | ||||
| 			> a | ||||
| 				display block | ||||
| 				margin-bottom 8px | ||||
| 				text-align center | ||||
|  | ||||
| 			> div | ||||
| 				cursor not-allowed !important | ||||
|  | ||||
| 				> * | ||||
| 					pointer-events none | ||||
|  | ||||
| 	&:not([data-customize]) | ||||
| 		> .main > *:empty | ||||
| 			display none | ||||
|  | ||||
| 	> .customize | ||||
| 		position fixed | ||||
| 		z-index 1000 | ||||
| 		top 0 | ||||
| 		left 0 | ||||
| 		width 100% | ||||
| 		height 48px | ||||
| 		color var(--text) | ||||
| 		background var(--desktopHeaderBg) | ||||
| 		box-shadow 0 1px 1px rgba(#000, 0.075) | ||||
|  | ||||
| 		> a | ||||
| 			display block | ||||
| 			position absolute | ||||
| 			z-index 1001 | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding 0 16px | ||||
| 			line-height 48px | ||||
| 			text-decoration none | ||||
| 			color var(--primaryForeground) | ||||
| 			background var(--primary) | ||||
| 			transition background 0.1s ease | ||||
|  | ||||
| 			&:hover | ||||
| 				background var(--primaryLighten10) | ||||
|  | ||||
| 			&:active | ||||
| 				background var(--primaryDarken10) | ||||
| 				transition background 0s ease | ||||
|  | ||||
| 			> [data-icon] | ||||
| 				margin-right 8px | ||||
|  | ||||
| 		> div | ||||
| 			display flex | ||||
| 			margin 0 auto | ||||
| 			max-width 1220px - 32px | ||||
|  | ||||
| 			> div | ||||
| 				width 50% | ||||
|  | ||||
| 				&.adder | ||||
| 					> p | ||||
| 						display inline | ||||
| 						line-height 48px | ||||
|  | ||||
| 				&.trash | ||||
| 					border-left solid 1px var(--faceDivider) | ||||
|  | ||||
| 					> div | ||||
| 						width 100% | ||||
| 						height 100% | ||||
|  | ||||
| 					> p | ||||
| 						position absolute | ||||
| 						top 0 | ||||
| 						left 0 | ||||
| 						width 100% | ||||
| 						line-height 48px | ||||
| 						margin 0 | ||||
| 						text-align center | ||||
| 						pointer-events none | ||||
|  | ||||
| 	> .main | ||||
| 		display flex | ||||
| 		justify-content center | ||||
| 		margin 0 auto | ||||
| 		max-width 1240px | ||||
|  | ||||
| 		> * | ||||
| 			.customize-container | ||||
| 				cursor move | ||||
| 				border-radius 6px | ||||
|  | ||||
| 				&:hover | ||||
| 					box-shadow 0 0 8px rgba(64, 120, 200, 0.3) | ||||
|  | ||||
| 				> * | ||||
| 					pointer-events none | ||||
|  | ||||
| 		> .main | ||||
| 			padding 16px | ||||
| 			width calc(100% - 280px * 2) | ||||
| 			order 2 | ||||
|  | ||||
| 			> .form | ||||
| 				margin-bottom 16px | ||||
| 				box-shadow var(--shadow) | ||||
| 				border-radius var(--round) | ||||
|  | ||||
| 		&.side | ||||
| 			> .main | ||||
| 				width calc(100% - 280px) | ||||
| 				max-width 680px | ||||
|  | ||||
| 		> *:not(.main) | ||||
| 			width 280px | ||||
| 			padding 16px 0 16px 0 | ||||
|  | ||||
| 			> *:not(:last-child) | ||||
| 				margin-bottom 16px | ||||
|  | ||||
| 		> .left | ||||
| 			padding-left 16px | ||||
| 			order 1 | ||||
|  | ||||
| 		> .right | ||||
| 			padding-right 16px | ||||
| 			order 3 | ||||
|  | ||||
| 		&.side | ||||
| 			@media (max-width 1000px) | ||||
| 				> *:not(.main) | ||||
| 					display none | ||||
|  | ||||
| 				> .main | ||||
| 					width 100% | ||||
| 					max-width 700px | ||||
| 					margin 0 auto | ||||
|  | ||||
| 		&:not(.side) | ||||
| 			@media (max-width 1200px) | ||||
| 				> *:not(.main) | ||||
| 					display none | ||||
|  | ||||
| 				> .main | ||||
| 					width 100% | ||||
| 					max-width 700px | ||||
| 					margin 0 auto | ||||
|  | ||||
| </style> | ||||
| @@ -2,8 +2,6 @@ import Vue from 'vue'; | ||||
|  | ||||
| import ui from './ui.vue'; | ||||
| import uiNotification from './ui-notification.vue'; | ||||
| import home from './home.vue'; | ||||
| import timeline from './timeline.vue'; | ||||
| import notes from './notes.vue'; | ||||
| import subNoteContent from './sub-note-content.vue'; | ||||
| import window from './window.vue'; | ||||
| @@ -20,12 +18,10 @@ import activity from './activity.vue'; | ||||
| import friendsMaker from './friends-maker.vue'; | ||||
| import userCard from './user-card.vue'; | ||||
| import userListTimeline from './user-list-timeline.vue'; | ||||
| import widgetContainer from './widget-container.vue'; | ||||
| import uiContainer from './ui-container.vue'; | ||||
|  | ||||
| Vue.component('mk-ui', ui); | ||||
| Vue.component('mk-ui-notification', uiNotification); | ||||
| Vue.component('mk-home', home); | ||||
| Vue.component('mk-timeline', timeline); | ||||
| Vue.component('mk-notes', notes); | ||||
| Vue.component('mk-sub-note-content', subNoteContent); | ||||
| Vue.component('mk-window', window); | ||||
| @@ -42,4 +38,4 @@ Vue.component('mk-activity', activity); | ||||
| Vue.component('mk-friends-maker', friendsMaker); | ||||
| Vue.component('mk-user-card', userCard); | ||||
| Vue.component('mk-user-list-timeline', userListTimeline); | ||||
| Vue.component('mk-widget-container', widgetContainer); | ||||
| Vue.component('ui-container', uiContainer); | ||||
|   | ||||
| @@ -31,9 +31,6 @@ | ||||
| 				<ui-switch v-model="autoPopout">{{ $t('auto-popout') }} | ||||
| 					<span slot="desc">{{ $t('auto-popout-desc') }}</span> | ||||
| 				</ui-switch> | ||||
| 				<ui-switch v-model="deckNav">{{ $t('deck-nav') }} | ||||
| 					<span slot="desc">{{ $t('deck-nav-desc') }}</span> | ||||
| 				</ui-switch> | ||||
| 				<ui-switch v-model="keepCw">{{ $t('keep-cw') }} | ||||
| 					<span slot="desc">{{ $t('keep-cw-desc') }}</span> | ||||
| 				</ui-switch> | ||||
| @@ -89,9 +86,6 @@ | ||||
| 				<ui-radio v-model="navbar" value="left">{{ $t('navbar-position-left') }}</ui-radio> | ||||
| 				<ui-radio v-model="navbar" value="right">{{ $t('navbar-position-right') }}</ui-radio> | ||||
| 			</section> | ||||
| 			<section> | ||||
| 				<ui-switch v-model="deckDefault">{{ $t('deck-default') }}</ui-switch> | ||||
| 			</section> | ||||
| 			<section> | ||||
| 				<ui-switch v-model="darkmode">{{ $t('dark-mode') }}</ui-switch> | ||||
| 				<ui-switch v-model="useShadow">{{ $t('use-shadow') }}</ui-switch> | ||||
| @@ -337,11 +331,6 @@ export default Vue.extend({ | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		deckNav: { | ||||
| 			get() { return this.$store.state.settings.deckNav; }, | ||||
| 			set(value) { this.$store.commit('settings/set', { key: 'deckNav', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		keepCw: { | ||||
| 			get() { return this.$store.state.settings.keepCw; }, | ||||
| 			set(value) { this.$store.commit('settings/set', { key: 'keepCw', value }); } | ||||
| @@ -367,11 +356,6 @@ export default Vue.extend({ | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		deckDefault: { | ||||
| 			get() { return this.$store.state.device.deckDefault; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckDefault', value }); } | ||||
| 		}, | ||||
|  | ||||
| 		enableSounds: { | ||||
| 			get() { return this.$store.state.device.enableSounds; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } | ||||
| @@ -534,8 +518,7 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		customizeHome() { | ||||
| 			this.$router.push('/i/customize-home'); | ||||
| 			this.$emit('done'); | ||||
| 			location.href = '/?customize'; | ||||
| 		}, | ||||
| 		updateWallpaper() { | ||||
| 			this.$chooseDriveFile({ | ||||
|   | ||||
| @@ -1,260 +0,0 @@ | ||||
| <template> | ||||
| <div class="mk-timeline"> | ||||
| 	<header> | ||||
| 		<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> | ||||
| 		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> | ||||
| 		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> | ||||
| 		<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> | ||||
| 		<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> | ||||
| 		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> | ||||
| 		<div class="buttons"> | ||||
| 			<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> | ||||
| 			<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> | ||||
| 			<button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button> | ||||
| 			<button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button> | ||||
| 		</div> | ||||
| 	</header> | ||||
| 	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> | ||||
| 	<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> | ||||
| 	<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||
| 	<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||
| 	<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||
| 	<x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> | ||||
| 	<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> | ||||
| 	<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XCore from './timeline.core.vue'; | ||||
| import Menu from '../../../common/views/components/menu.vue'; | ||||
| import MkSettingsWindow from './settings-window.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/timeline.vue'), | ||||
| 	components: { | ||||
| 		XCore | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			src: 'home', | ||||
| 			list: null, | ||||
| 			tagTl: null, | ||||
| 			enableLocalTimeline: false, | ||||
| 			enableGlobalTimeline: false, | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		src() { | ||||
| 			this.saveSrc(); | ||||
| 		}, | ||||
|  | ||||
| 		list(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.tagTl = null; | ||||
| 		}, | ||||
|  | ||||
| 		tagTl(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.list = null; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin; | ||||
| 			this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin; | ||||
| 		}); | ||||
|  | ||||
| 		if (this.$store.state.device.tl) { | ||||
| 			this.src = this.$store.state.device.tl.src; | ||||
| 			if (this.src == 'list') { | ||||
| 				this.list = this.$store.state.device.tl.arg; | ||||
| 			} else if (this.src == 'tag') { | ||||
| 				this.tagTl = this.$store.state.device.tl.arg; | ||||
| 			} | ||||
| 		} else if (this.$store.state.i.followingCount == 0) { | ||||
| 			this.src = 'hybrid'; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		(this.$refs.tl as any).$once('loaded', () => { | ||||
| 			this.$emit('loaded'); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		saveSrc() { | ||||
| 			this.$store.commit('device/setTl', { | ||||
| 				src: this.src, | ||||
| 				arg: this.src == 'list' ? this.list : this.tagTl | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		}, | ||||
|  | ||||
| 		warp(date) { | ||||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		}, | ||||
|  | ||||
| 		async chooseList() { | ||||
| 			const lists = await this.$root.api('users/lists/list'); | ||||
|  | ||||
| 			let menu = [{ | ||||
| 				icon: 'plus', | ||||
| 				text: this.$t('add-list'), | ||||
| 				action: () => { | ||||
| 					this.$root.dialog({ | ||||
| 						title: this.$t('list-name'), | ||||
| 						input: true | ||||
| 					}).then(async ({ canceled, result: title }) => { | ||||
| 						if (canceled) return; | ||||
| 						const list = await this.$root.api('users/lists/create', { | ||||
| 							title | ||||
| 						}); | ||||
|  | ||||
| 						this.list = list; | ||||
| 						this.src = 'list'; | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
|  | ||||
| 			if (lists.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
|  | ||||
| 			menu = menu.concat(lists.map(list => ({ | ||||
| 				icon: 'list', | ||||
| 				text: list.title, | ||||
| 				action: () => { | ||||
| 					this.list = list; | ||||
| 					this.src = 'list'; | ||||
| 				} | ||||
| 			}))); | ||||
|  | ||||
| 			this.$root.new(Menu, { | ||||
| 				source: this.$refs.listButton, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		chooseTag() { | ||||
| 			let menu = [{ | ||||
| 				icon: 'plus', | ||||
| 				text: this.$t('add-tag-timeline'), | ||||
| 				action: () => { | ||||
| 					this.$root.new(MkSettingsWindow, { | ||||
| 						initialPage: 'hashtags' | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
|  | ||||
| 			if (this.$store.state.settings.tagTimelines.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
|  | ||||
| 			menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({ | ||||
| 				icon: 'hashtag', | ||||
| 				text: t.title, | ||||
| 				action: () => { | ||||
| 					this.tagTl = t; | ||||
| 					this.src = 'tag'; | ||||
| 				} | ||||
| 			}))); | ||||
|  | ||||
| 			this.$root.new(Menu, { | ||||
| 				source: this.$refs.tagButton, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-timeline | ||||
| 	background var(--face) | ||||
| 	box-shadow var(--shadow) | ||||
| 	border-radius var(--round) | ||||
| 	overflow hidden | ||||
|  | ||||
| 	> header | ||||
| 		padding 0 8px | ||||
| 		z-index 10 | ||||
| 		background var(--faceHeader) | ||||
| 		box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) | ||||
|  | ||||
| 		> .buttons | ||||
| 			position absolute | ||||
| 			z-index 2 | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding-right 8px | ||||
|  | ||||
| 			> button | ||||
| 				padding 0 8px | ||||
| 				font-size 0.9em | ||||
| 				line-height 42px | ||||
| 				color var(--faceTextButton) | ||||
|  | ||||
| 				> .badge | ||||
| 					position absolute | ||||
| 					top -4px | ||||
| 					right 4px | ||||
| 					font-size 10px | ||||
| 					color var(--notificationIndicator) | ||||
|  | ||||
| 				&:hover | ||||
| 					color var(--faceTextButtonHover) | ||||
|  | ||||
| 				&[data-active] | ||||
| 					color var(--primary) | ||||
| 					cursor default | ||||
|  | ||||
| 					&:before | ||||
| 						content "" | ||||
| 						display block | ||||
| 						position absolute | ||||
| 						bottom 0 | ||||
| 						left 0 | ||||
| 						width 100% | ||||
| 						height 2px | ||||
| 						background var(--primary) | ||||
|  | ||||
| 		> span | ||||
| 			display inline-block | ||||
| 			padding 0 10px | ||||
| 			line-height 42px | ||||
| 			font-size 12px | ||||
| 			user-select none | ||||
|  | ||||
| 			&[data-active] | ||||
| 				color var(--primary) | ||||
| 				cursor default | ||||
| 				font-weight bold | ||||
|  | ||||
| 				&:before | ||||
| 					content "" | ||||
| 					display block | ||||
| 					position absolute | ||||
| 					bottom 0 | ||||
| 					left -8px | ||||
| 					width calc(100% + 16px) | ||||
| 					height 2px | ||||
| 					background var(--primary) | ||||
|  | ||||
| 			&:not([data-active]) | ||||
| 				color var(--desktopTimelineSrc) | ||||
| 				cursor pointer | ||||
|  | ||||
| 				&:hover | ||||
| 					color var(--desktopTimelineSrcHover) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										117
									
								
								src/client/app/desktop/views/components/ui-container.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/client/app/desktop/views/components/ui-container.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| <template> | ||||
| <div class="kedshtep" :class="{ naked, inDeck }"> | ||||
| 	<header v-if="showHeader"> | ||||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<slot name="func"></slot> | ||||
| 		<button v-if="bodyTogglable" @click="() => showBody = !showBody"> | ||||
| 			<template v-if="showBody"><fa icon="angle-up"/></template> | ||||
| 			<template v-else><fa icon="angle-down"/></template> | ||||
| 		</button> | ||||
| 	</header> | ||||
| 	<div v-show="showBody"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		showHeader: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		}, | ||||
| 		naked: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		bodyTogglable: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	inject: { | ||||
| 		inDeck: { | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showBody: true | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .kedshtep | ||||
| 	overflow hidden | ||||
|  | ||||
| 	&:not(.inDeck) | ||||
| 		background var(--face) | ||||
| 		box-shadow var(--shadow) | ||||
| 		border-radius var(--round) | ||||
|  | ||||
| 		& + .kedshtep | ||||
| 			margin-top 16px | ||||
|  | ||||
| 		&.naked | ||||
| 			background transparent !important | ||||
| 			box-shadow none !important | ||||
|  | ||||
| 		> header | ||||
| 			background var(--faceHeader) | ||||
|  | ||||
| 			> .title | ||||
| 				z-index 1 | ||||
| 				margin 0 | ||||
| 				padding 0 16px | ||||
| 				line-height 42px | ||||
| 				font-size 0.9em | ||||
| 				font-weight bold | ||||
| 				color var(--faceHeaderText) | ||||
| 				box-shadow 0 var(--lineWidth) rgba(#000, 0.07) | ||||
|  | ||||
| 				> [data-icon] | ||||
| 					margin-right 6px | ||||
|  | ||||
| 				&:empty | ||||
| 					display none | ||||
|  | ||||
| 			> button | ||||
| 				position absolute | ||||
| 				z-index 2 | ||||
| 				top 0 | ||||
| 				right 0 | ||||
| 				padding 0 | ||||
| 				width 42px | ||||
| 				font-size 0.9em | ||||
| 				line-height 42px | ||||
| 				color var(--faceTextButton) | ||||
|  | ||||
| 				&:hover | ||||
| 					color var(--faceTextButtonHover) | ||||
|  | ||||
| 				&:active | ||||
| 					color var(--faceTextButtonActive) | ||||
|  | ||||
| 	&.inDeck | ||||
| 		background var(--face) | ||||
|  | ||||
| 		> header | ||||
| 			margin 0 | ||||
| 			padding 8px 16px | ||||
| 			font-size 12px | ||||
| 			color var(--text) | ||||
| 			background var(--deckColumnBg) | ||||
|  | ||||
| 			> button | ||||
| 				position absolute | ||||
| 				top 0 | ||||
| 				right 8px | ||||
| 				padding 8px 6px | ||||
| 				font-size 14px | ||||
| 				color var(--text) | ||||
|  | ||||
| </style> | ||||
| @@ -44,13 +44,6 @@ | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 			<ul> | ||||
| 				<li> | ||||
| 					<router-link to="/i/customize-home"> | ||||
| 						<i><fa icon="wrench"/></i> | ||||
| 						<span>{{ $t('customize') }}</span> | ||||
| 						<i><fa icon="angle-right"/></i> | ||||
| 					</router-link> | ||||
| 				</li> | ||||
| 				<li> | ||||
| 					<router-link to="/i/settings"> | ||||
| 						<i><fa icon="cog"/></i> | ||||
| @@ -67,6 +60,13 @@ | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 			<ul> | ||||
| 				<li @click="toggleDeckMode"> | ||||
| 					<p> | ||||
| 						<span>{{ $t('@.deck') }}</span> | ||||
| 						<template v-if="$store.state.device.deckMode"><i><fa :icon="faHome"/></i></template> | ||||
| 						<template v-else><i><fa :icon="faColumns"/></i></template> | ||||
| 					</p> | ||||
| 				</li> | ||||
| 				<li @click="dark"> | ||||
| 					<p> | ||||
| 						<span>{{ $t('dark') }}</span> | ||||
| @@ -97,12 +97,14 @@ import MkFollowRequestsWindow from './received-follow-requests-window.vue'; | ||||
| import MkSettingsWindow from './settings-window.vue'; | ||||
| import MkDriveWindow from './drive-window.vue'; | ||||
| import contains from '../../../common/scripts/contains'; | ||||
| import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/ui.header.account.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			isOpen: false | ||||
| 			isOpen: false, | ||||
| 			faHome, faColumns | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| @@ -161,7 +163,11 @@ export default Vue.extend({ | ||||
| 				key: 'darkmode', | ||||
| 				value: !this.$store.state.device.darkmode | ||||
| 			}); | ||||
| 		} | ||||
| 		}, | ||||
| 		toggleDeckMode() { | ||||
| 			this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.deckMode }); | ||||
| 			location.reload(); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| <template> | ||||
| <div class="toltmoik"> | ||||
| 	<button @click="open()" :title="$t('@.messaging')"> | ||||
| 		<i class="bell"><fa :icon="faComments"/></i> | ||||
| 		<i class="circle" v-if="hasUnreadMessagingMessage"><fa icon="circle"/></i> | ||||
| 	</button> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import MkMessagingWindow from './messaging-window.vue'; | ||||
| import { faComments } from '@fortawesome/free-regular-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			faComments | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		hasUnreadMessagingMessage(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		open() { | ||||
| 			this.$root.new(MkMessagingWindow); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .toltmoik | ||||
| 	> button | ||||
| 		display block | ||||
| 		margin 0 | ||||
| 		padding 0 | ||||
| 		width 32px | ||||
| 		color var(--desktopHeaderFg) | ||||
| 		border none | ||||
| 		background transparent | ||||
| 		cursor pointer | ||||
|  | ||||
| 		* | ||||
| 			pointer-events none | ||||
|  | ||||
| 		&:hover | ||||
| 		&[data-active='true'] | ||||
| 			color var(--desktopHeaderHoverFg) | ||||
|  | ||||
| 		> i.bell | ||||
| 			font-size 1.2em | ||||
| 			line-height 48px | ||||
|  | ||||
| 		> i.circle | ||||
| 			margin-left -5px | ||||
| 			vertical-align super | ||||
| 			font-size 10px | ||||
| 			color var(--notificationIndicator) | ||||
|  | ||||
| </style> | ||||
| @@ -1,38 +1,22 @@ | ||||
| <template> | ||||
| <div class="nav"> | ||||
| 	<ul> | ||||
| 		<template v-if="$store.getters.isSignedIn"> | ||||
| 			<template v-if="$store.state.device.deckDefault"> | ||||
| 				<li class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop"> | ||||
| 					<router-link to="/"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link> | ||||
| 				</li> | ||||
| 				<li class="home" :class="{ active: $route.name == 'home' }" @click="goToTop"> | ||||
| 					<router-link to="/home"><fa icon="home"/><p>{{ $t('home') }}</p></router-link> | ||||
| 				</li> | ||||
| 			</template> | ||||
| 			<template v-else> | ||||
| 				<li class="home" :class="{ active: $route.name == 'home' || $route.name == 'index' }" @click="goToTop"> | ||||
| 					<router-link to="/"><fa icon="home"/><p>{{ $t('home') }}</p></router-link> | ||||
| 				</li> | ||||
| 				<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> | ||||
| 					<router-link to="/deck"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link> | ||||
| 				</li> | ||||
| 			</template> | ||||
| 			<li class="messaging"> | ||||
| 				<a @click="messaging"> | ||||
| 					<fa icon="comments"/> | ||||
| 					<p>{{ $t('@.messaging') }}</p> | ||||
| 					<template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template> | ||||
| 				</a> | ||||
| 			</li> | ||||
| 			<li class="game"> | ||||
| 				<a @click="game"> | ||||
| 					<fa icon="gamepad"/> | ||||
| 					<p>{{ $t('game') }}</p> | ||||
| 					<template v-if="hasGameInvitations"><fa icon="circle"/></template> | ||||
| 				</a> | ||||
| 			</li> | ||||
| 		</template> | ||||
| 		<li v-if="!$store.state.device.deckMode" class="timeline" :class="{ active: $route.name == 'index' }" @click="goToTop"> | ||||
| 			<router-link to="/"><fa icon="home"/><p>{{ $t('@.timeline') }}</p></router-link> | ||||
| 		</li> | ||||
| 		<li class="featured" :class="{ active: $route.name == 'featured' }"> | ||||
| 			<router-link to="/featured"><fa :icon="faNewspaper"/><p>{{ $t('@.featured-notes') }}</p></router-link> | ||||
| 		</li> | ||||
| 		<li class="explore" :class="{ active: $route.name == 'explore' }"> | ||||
| 			<router-link to="/explore"><fa :icon="faHashtag"/><p>{{ $t('@.explore') }}</p></router-link> | ||||
| 		</li> | ||||
| 		<li class="game"> | ||||
| 			<a @click="game"> | ||||
| 				<fa icon="gamepad"/> | ||||
| 				<p>{{ $t('game') }}</p> | ||||
| 				<template v-if="hasGameInvitations"><fa icon="circle"/></template> | ||||
| 			</a> | ||||
| 		</li> | ||||
| 	</ul> | ||||
| </div> | ||||
| </template> | ||||
| @@ -40,22 +24,18 @@ | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import MkMessagingWindow from './messaging-window.vue'; | ||||
| import MkGameWindow from './game-window.vue'; | ||||
| import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/ui.header.nav.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			hasGameInvitations: false, | ||||
| 			connection: null | ||||
| 			connection: null, | ||||
| 			faNewspaper, faHashtag | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		hasUnreadMessagingMessage(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| @@ -78,10 +58,6 @@ export default Vue.extend({ | ||||
| 			this.hasGameInvitations = false; | ||||
| 		}, | ||||
|  | ||||
| 		messaging() { | ||||
| 			this.$root.new(MkMessagingWindow); | ||||
| 		}, | ||||
|  | ||||
| 		game() { | ||||
| 			this.$root.new(MkGameWindow); | ||||
| 		}, | ||||
| @@ -126,7 +102,7 @@ export default Vue.extend({ | ||||
| 				display inline-block | ||||
| 				z-index 1 | ||||
| 				height 100% | ||||
| 				padding 0 24px | ||||
| 				padding 0 20px | ||||
| 				font-size 13px | ||||
| 				font-variant small-caps | ||||
| 				color var(--desktopHeaderFg) | ||||
|   | ||||
| @@ -16,9 +16,10 @@ | ||||
| 				<div class="right"> | ||||
| 					<x-search/> | ||||
| 					<x-account v-if="$store.getters.isSignedIn"/> | ||||
| 					<x-messaging v-if="$store.getters.isSignedIn"/> | ||||
| 					<x-notifications v-if="$store.getters.isSignedIn"/> | ||||
| 					<x-post v-if="$store.getters.isSignedIn"/> | ||||
| 					<x-clock v-if="$store.state.settings.showClockOnHeader"/> | ||||
| 					<x-clock v-if="$store.state.settings.showClockOnHeader" class="clock"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| @@ -37,6 +38,7 @@ import XAccount from './ui.header.account.vue'; | ||||
| import XNotifications from './ui.header.notifications.vue'; | ||||
| import XPost from './ui.header.post.vue'; | ||||
| import XClock from './ui.header.clock.vue'; | ||||
| import XMessaging from './ui.header.messaging.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
| @@ -45,6 +47,7 @@ export default Vue.extend({ | ||||
| 		XSearch, | ||||
| 		XAccount, | ||||
| 		XNotifications, | ||||
| 		XMessaging, | ||||
| 		XPost, | ||||
| 		XClock | ||||
| 	}, | ||||
| @@ -116,7 +119,7 @@ export default Vue.extend({ | ||||
| 			> .container | ||||
| 				display flex | ||||
| 				width 100% | ||||
| 				max-width 1300px | ||||
| 				max-width 1208px | ||||
| 				margin 0 auto | ||||
|  | ||||
| 				> * | ||||
| @@ -152,7 +155,7 @@ export default Vue.extend({ | ||||
| 						vertical-align top | ||||
|  | ||||
| 					@media (max-width 1100px) | ||||
| 						> .mk-ui-header-search | ||||
| 						> .clock | ||||
| 							display none | ||||
|  | ||||
| </style> | ||||
|   | ||||
| @@ -6,24 +6,16 @@ | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="nav" v-if="$store.getters.isSignedIn"> | ||||
| 			<template v-if="$store.state.device.deckDefault"> | ||||
| 				<div class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop"> | ||||
| 					<router-link to="/"><fa icon="columns"/></router-link> | ||||
| 				</div> | ||||
| 				<div class="home" :class="{ active: $route.name == 'home' }" @click="goToTop"> | ||||
| 					<router-link to="/home"><fa icon="home"/></router-link> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 			<template v-else> | ||||
| 				<div class="home" :class="{ active: $route.name == 'home' || $route.name == 'index' }" @click="goToTop"> | ||||
| 			<template v-if="!$store.state.device.deckMode"> | ||||
| 				<div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop"> | ||||
| 					<router-link to="/"><fa icon="home"/></router-link> | ||||
| 				</div> | ||||
| 				<div class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> | ||||
| 					<router-link to="/deck"><fa icon="columns"/></router-link> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 			<div class="messaging"> | ||||
| 				<a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a> | ||||
| 			<div class="featured" :class="{ active: $route.name == 'featured' }"> | ||||
| 				<router-link to="/featured"><fa :icon="faNewspaper"/></router-link> | ||||
| 			</div> | ||||
| 			<div class="explore" :class="{ active: $route.name == 'explore' }"> | ||||
| 				<router-link to="/explore"><fa :icon="faHashtag"/></router-link> | ||||
| 			</div> | ||||
| 			<div class="game"> | ||||
| 				<a @click="game"><fa icon="gamepad"/><template v-if="hasGameInvitations"><fa icon="circle"/></template></a> | ||||
| @@ -37,30 +29,34 @@ | ||||
| 			<div ref="notificationsButton" :class="{ active: showNotifications }"> | ||||
| 				<a @click="notifications"><fa :icon="['far', 'bell']"/></a> | ||||
| 			</div> | ||||
| 			<div class="messaging"> | ||||
| 				<a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<a @click="settings"><fa icon="cog"/></a> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="account"> | ||||
| 			<router-link :to="`/@${ $store.state.i.username }`"> | ||||
| 				<mk-avatar class="avatar" :user="$store.state.i"/> | ||||
| 			</router-link> | ||||
|  | ||||
| 			<div class="nav menu"> | ||||
| 				<div class="signout"> | ||||
| 					<a @click="signout"><fa icon="power-off"/></a> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<router-link to="/i/favorites"><fa icon="star"/></router-link> | ||||
| 				</div> | ||||
| 				<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> | ||||
| 					<a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a> | ||||
| 				</div> | ||||
| 			<div class="signout"> | ||||
| 				<a @click="signout"><fa icon="power-off"/></a> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<router-link to="/i/favorites"><fa icon="star"/></router-link> | ||||
| 			</div> | ||||
| 			<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> | ||||
| 				<a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a> | ||||
| 			</div> | ||||
| 			<div class="account"> | ||||
| 				<router-link :to="`/@${ $store.state.i.username }`"> | ||||
| 					<mk-avatar class="avatar" :user="$store.state.i"/> | ||||
| 				</router-link> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<template v-if="$store.state.device.deckMode"> | ||||
| 					<a @click="toggleDeckMode(false)"><fa icon="home"/></a> | ||||
| 				</template> | ||||
| 				<template v-else> | ||||
| 					<a @click="toggleDeckMode(true)"><fa icon="columns"/></a> | ||||
| 				</template> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="nav dark"> | ||||
| 			<div> | ||||
| 				<a @click="dark"><template v-if="$store.state.device.darkmode"><fa icon="moon"/></template><template v-else><fa :icon="['far', 'moon']"/></template></a> | ||||
| 			</div> | ||||
| @@ -85,6 +81,7 @@ import MkDriveWindow from './drive-window.vue'; | ||||
| import MkMessagingWindow from './messaging-window.vue'; | ||||
| import MkGameWindow from './game-window.vue'; | ||||
| import contains from '../../../common/scripts/contains'; | ||||
| import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/ui.sidebar.vue'), | ||||
| @@ -92,7 +89,8 @@ export default Vue.extend({ | ||||
| 		return { | ||||
| 			hasGameInvitations: false, | ||||
| 			connection: null, | ||||
| 			showNotifications: false | ||||
| 			showNotifications: false, | ||||
| 			faNewspaper, faHashtag | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| @@ -122,6 +120,11 @@ export default Vue.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		toggleDeckMode(deck) { | ||||
| 			this.$store.commit('device/set', { key: 'deckMode', value: deck }); | ||||
| 			location.reload(); | ||||
| 		}, | ||||
|  | ||||
| 		onReversiInvited() { | ||||
| 			this.hasGameInvitations = true; | ||||
| 		}, | ||||
| @@ -273,44 +276,23 @@ export default Vue.extend({ | ||||
|  | ||||
| 		> .nav.bottom | ||||
| 			position absolute | ||||
| 			bottom 128px | ||||
| 			bottom 0 | ||||
| 			left 0 | ||||
|  | ||||
| 		> .account | ||||
| 			position absolute | ||||
| 			bottom 64px | ||||
| 			left 0 | ||||
| 			width $width | ||||
| 			height $width | ||||
| 			padding 14px | ||||
| 			> .account | ||||
| 				width $width | ||||
| 				height $width | ||||
| 				padding 14px | ||||
|  | ||||
| 			> .menu | ||||
| 				display none | ||||
| 				position absolute | ||||
| 				bottom 64px | ||||
| 				left 0 | ||||
| 				background var(--desktopHeaderBg) | ||||
|  | ||||
| 			&:hover | ||||
| 				> .menu | ||||
| 				> * | ||||
| 					display block | ||||
|  | ||||
| 			> *:not(.menu) | ||||
| 				display block | ||||
| 				width 100% | ||||
| 				height 100% | ||||
|  | ||||
| 				> .avatar | ||||
| 					pointer-events none | ||||
| 					width 100% | ||||
| 					height 100% | ||||
|  | ||||
| 		> .dark | ||||
| 			position absolute | ||||
| 			bottom 0 | ||||
| 			left 0 | ||||
| 			width $width | ||||
| 			height $width | ||||
| 					> .avatar | ||||
| 						pointer-events none | ||||
| 						width 100% | ||||
| 						height 100% | ||||
|  | ||||
| 	> .notifications | ||||
| 		position fixed | ||||
|   | ||||
| @@ -41,7 +41,6 @@ export default Vue.extend({ | ||||
| 	height 280px | ||||
| 	overflow hidden | ||||
| 	font-size 13px | ||||
| 	text-align center | ||||
| 	background $bg | ||||
| 	box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) | ||||
| 	color var(--faceText) | ||||
| @@ -54,7 +53,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	> .avatar | ||||
| 		display block | ||||
| 		margin -40px auto 0 auto | ||||
| 		margin -40px 0 0 16px | ||||
| 		width 80px | ||||
| 		height 80px | ||||
| 		border-radius 100% | ||||
| @@ -67,6 +66,7 @@ export default Vue.extend({ | ||||
|  | ||||
| 	> .body | ||||
| 		padding 0px 24px | ||||
| 		margin-top -40px | ||||
|  | ||||
| 		> .name | ||||
| 			font-size 120% | ||||
|   | ||||
| @@ -1,74 +0,0 @@ | ||||
| <template> | ||||
| <div class="mk-widget-container" :class="{ naked }"> | ||||
| 	<header v-if="showHeader"> | ||||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<slot name="func"></slot> | ||||
| 	</header> | ||||
| 	<slot></slot> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		showHeader: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		}, | ||||
| 		naked: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-widget-container | ||||
| 	background var(--face) | ||||
| 	box-shadow var(--shadow) | ||||
| 	border-radius var(--round) | ||||
| 	overflow hidden | ||||
|  | ||||
| 	&.naked | ||||
| 		background transparent !important | ||||
| 		box-shadow none !important | ||||
|  | ||||
| 	> header | ||||
| 		background var(--faceHeader) | ||||
|  | ||||
| 		> .title | ||||
| 			z-index 1 | ||||
| 			margin 0 | ||||
| 			padding 0 16px | ||||
| 			line-height 42px | ||||
| 			font-size 0.9em | ||||
| 			font-weight bold | ||||
| 			color var(--faceHeaderText) | ||||
| 			box-shadow 0 var(--lineWidth) rgba(#000, 0.07) | ||||
|  | ||||
| 			> [data-icon] | ||||
| 				margin-right 6px | ||||
|  | ||||
| 			&:empty | ||||
| 				display none | ||||
|  | ||||
| 		> button | ||||
| 			position absolute | ||||
| 			z-index 2 | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding 0 | ||||
| 			width 42px | ||||
| 			font-size 0.9em | ||||
| 			line-height 42px | ||||
| 			color var(--faceTextButton) | ||||
|  | ||||
| 			&:hover | ||||
| 				color var(--faceTextButtonHover) | ||||
|  | ||||
| 			&:active | ||||
| 				color var(--faceTextButtonActive) | ||||
|  | ||||
| </style> | ||||
| @@ -27,9 +27,9 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import Menu from '../../../../common/views/components/menu.vue'; | ||||
| import { countIf } from '../../../../../../prelude/array'; | ||||
| import i18n from '../../../i18n'; | ||||
| import Menu from '../../../common/views/components/menu.vue'; | ||||
| import { countIf } from '../../../../../prelude/array'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('deck'), | ||||
| @@ -65,6 +65,16 @@ export default Vue.extend({ | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			count: 0, | ||||
| 			active: true, | ||||
| 			dragging: false, | ||||
| 			draghover: false, | ||||
| 			dropready: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		isTemporaryColumn(): boolean { | ||||
| 			return this.column == null; | ||||
| @@ -84,16 +94,6 @@ export default Vue.extend({ | ||||
| 		getColumnVm: { from: 'getColumnVm' } | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			count: 0, | ||||
| 			active: true, | ||||
| 			dragging: false, | ||||
| 			draghover: false, | ||||
| 			dropready: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		active(v) { | ||||
| 			if (v && this.isScrollTop()) { | ||||
| @@ -109,7 +109,8 @@ export default Vue.extend({ | ||||
| 		return { | ||||
| 			column: this, | ||||
| 			isScrollTop: this.isScrollTop, | ||||
| 			count: v => this.count = v | ||||
| 			count: v => this.count = v, | ||||
| 			inDeck: !this.naked | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| @@ -245,10 +246,7 @@ export default Vue.extend({ | ||||
| 		}, | ||||
| 
 | ||||
| 		close() { | ||||
| 			this.$store.commit('device/set', { | ||||
| 				key: 'deckTemporaryColumn', | ||||
| 				value: null | ||||
| 			}); | ||||
| 			this.$router.push('/'); | ||||
| 		}, | ||||
| 
 | ||||
| 		goTop() { | ||||
| @@ -8,7 +8,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XDirect from './deck.direct.vue'; | ||||
| 
 | ||||
							
								
								
									
										34
									
								
								src/client/app/desktop/views/deck/deck.explore-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/client/app/desktop/views/deck/deck.explore-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <template> | ||||
| <x-column> | ||||
| 	<span slot="header"> | ||||
| 		<fa :icon="faHashtag"/>{{ $t('@.explore') }} | ||||
| 	</span> | ||||
|  | ||||
| 	<div> | ||||
| 		<x-explore/> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XExplore from '../../../common/views/pages/explore.vue'; | ||||
| import { faHashtag } from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
|  | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XExplore, | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			faHashtag | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										88
									
								
								src/client/app/desktop/views/deck/deck.favorites-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/client/app/desktop/views/deck/deck.favorites-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| <template> | ||||
| <x-column> | ||||
| 	<span slot="header"> | ||||
| 		<fa :icon="['fa', 'star']"/>{{ $t('favorites') }} | ||||
| 	</span> | ||||
|  | ||||
| 	<div> | ||||
| 		<x-notes ref="timeline" :more="existMore ? more : null"/> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
|  | ||||
| const fetchLimit = 10; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
|  | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XNotes, | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				this.$root.api('i/favorites', { | ||||
| 					limit: fetchLimit + 1, | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes.map(x => x.note)); | ||||
| 					this.fetching = false; | ||||
| 					this.$emit('loaded'); | ||||
| 				}, rej); | ||||
| 			})); | ||||
| 		}, | ||||
|  | ||||
| 		more() { | ||||
| 			this.moreFetching = true; | ||||
|  | ||||
| 			const promise = this.$root.api('i/favorites', { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: (this.$refs.timeline as any).tail().id, | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				for (const n of notes) { | ||||
| 					(this.$refs.timeline as any).append(n); | ||||
| 				} | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
|  | ||||
| 			return promise; | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			this.$refs.timeline.focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										60
									
								
								src/client/app/desktop/views/deck/deck.featured-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/client/app/desktop/views/deck/deck.featured-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <template> | ||||
| <x-column> | ||||
| 	<span slot="header"> | ||||
| 		<fa :icon="faNewspaper"/>{{ $t('@.featured-notes') }} | ||||
| 	</span> | ||||
|  | ||||
| 	<div> | ||||
| 		<x-notes ref="timeline" :more="null"/> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import { faNewspaper } from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
|  | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XNotes, | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			faNewspaper | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				this.$root.api('notes/featured', { | ||||
| 					limit: 20, | ||||
| 				}).then(notes => { | ||||
| 					notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); | ||||
| 					res(notes); | ||||
| 					this.fetching = false; | ||||
| 					this.$emit('loaded'); | ||||
| 				}, rej); | ||||
| 			})); | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			this.$refs.timeline.focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										119
									
								
								src/client/app/desktop/views/deck/deck.hashtag-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/client/app/desktop/views/deck/deck.hashtag-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| <template> | ||||
| <x-column> | ||||
| 	<span slot="header"> | ||||
| 		<fa icon="hashtag"/><span>{{ tag }}</span> | ||||
| 	</span> | ||||
|  | ||||
| 	<div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb"> | ||||
| 		<div ref="chart" class="chart"></div> | ||||
| 		<x-hashtag-tl :tag-tl="tagTl" class="tl"/> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XHashtagTl from './deck.hashtag-tl.vue'; | ||||
| import ApexCharts from 'apexcharts'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XHashtagTl | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		tag(): string { | ||||
| 			return this.$route.params.tag; | ||||
| 		}, | ||||
|  | ||||
| 		tagTl(): any { | ||||
| 			return { | ||||
| 				query: [[this.tag]] | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.$root.api('charts/hashtag', { | ||||
| 				tag: this.tag, | ||||
| 				span: 'hour', | ||||
| 				limit: 24 | ||||
| 			}).then(stats => { | ||||
| 				const local = []; | ||||
| 				const remote = []; | ||||
|  | ||||
| 				const now = new Date(); | ||||
| 				const y = now.getFullYear(); | ||||
| 				const m = now.getMonth(); | ||||
| 				const d = now.getDate(); | ||||
| 				const h = now.getHours(); | ||||
|  | ||||
| 				for (let i = 0; i < 24; i++) { | ||||
| 					const x = new Date(y, m, d, h - i); | ||||
| 					local.push([x, stats.local.count[i]]); | ||||
| 					remote.push([x, stats.remote.count[i]]); | ||||
| 				} | ||||
|  | ||||
| 				const chart = new ApexCharts(this.$refs.chart, { | ||||
| 					chart: { | ||||
| 						type: 'area', | ||||
| 						height: 70, | ||||
| 						sparkline: { | ||||
| 							enabled: true | ||||
| 						}, | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						clipMarkers: false, | ||||
| 						padding: { | ||||
| 							top: 16, | ||||
| 							right: 16, | ||||
| 							bottom: 16, | ||||
| 							left: 16 | ||||
| 						} | ||||
| 					}, | ||||
| 					stroke: { | ||||
| 						curve: 'straight', | ||||
| 						width: 2 | ||||
| 					}, | ||||
| 					series: [{ | ||||
| 						name: 'Local', | ||||
| 						data: local | ||||
| 					}, { | ||||
| 						name: 'Remote', | ||||
| 						data: remote | ||||
| 					}], | ||||
| 					xaxis: { | ||||
| 						type: 'datetime', | ||||
| 					} | ||||
| 				}); | ||||
|  | ||||
| 				chart.render(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .xroyrflcmhhtmlwmyiwpfqiirqokfueb | ||||
| 	background var(--deckColumnBg) | ||||
|  | ||||
| 	> .chart | ||||
| 		margin-bottom 16px | ||||
| 		background var(--face) | ||||
|  | ||||
| 	> .tl | ||||
| 		background var(--face) | ||||
|  | ||||
| </style> | ||||
| @@ -8,7 +8,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XMentions from './deck.mentions.vue'; | ||||
| 
 | ||||
| @@ -18,10 +18,10 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import XNote from '../../components/note.vue'; | ||||
| import XNote from '../components/note.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
| @@ -31,13 +31,6 @@ export default Vue.extend({ | ||||
| 		XNote | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		noteId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			note: null, | ||||
| @@ -45,11 +38,25 @@ export default Vue.extend({ | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.$root.api('notes/show', { noteId: this.noteId }).then(note => { | ||||
| 			this.note = note; | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			this.$root.api('notes/show', { | ||||
| 				noteId: this.$route.params.note | ||||
| 			}).then(note => { | ||||
| 				this.note = note; | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| @@ -38,10 +38,10 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import shouldMuteNote from '../../../../common/scripts/should-mute-note'; | ||||
| import i18n from '../../../i18n'; | ||||
| import shouldMuteNote from '../../../common/scripts/should-mute-note'; | ||||
| 
 | ||||
| import XNote from '../../components/note.vue'; | ||||
| import XNote from '../components/note.vue'; | ||||
| 
 | ||||
| const displayLimit = 20; | ||||
| 
 | ||||
| @@ -96,8 +96,8 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import getNoteSummary from '../../../../../../misc/get-note-summary'; | ||||
| import XNote from '../../components/note.vue'; | ||||
| import getNoteSummary from '../../../../../misc/get-note-summary'; | ||||
| import XNote from '../components/note.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| @@ -8,7 +8,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XNotifications from './deck.notifications.vue'; | ||||
| 
 | ||||
| @@ -25,7 +25,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XNotification from './deck.notification.vue'; | ||||
| 
 | ||||
| const displayLimit = 20; | ||||
							
								
								
									
										99
									
								
								src/client/app/desktop/views/deck/deck.search-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/client/app/desktop/views/deck/deck.search-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| <template> | ||||
| <x-column> | ||||
| 	<span slot="header"> | ||||
| 		<fa icon="search"/><span>{{ q }}</span> | ||||
| 	</span> | ||||
|  | ||||
| 	<div> | ||||
| 		<x-notes ref="timeline" :more="existMore ? more : null"/> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
|  | ||||
| const limit = 20; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XNotes | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			offset: 0, | ||||
| 			empty: false, | ||||
| 			notAvailable: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		q(): string { | ||||
| 			return this.$route.query.q; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			(this.$refs.timeline as any).init(() => new Promise((res, rej) => { | ||||
| 				this.$root.api('notes/search', { | ||||
| 					limit: limit + 1, | ||||
| 					offset: this.offset, | ||||
| 					query: this.q | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == 0) this.empty = true; | ||||
| 					if (notes.length == limit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes); | ||||
| 					this.fetching = false; | ||||
| 				}, (e: string) => { | ||||
| 					this.fetching = false; | ||||
| 					if (e === 'searching not available') this.notAvailable = true; | ||||
| 				}); | ||||
| 			})); | ||||
| 		}, | ||||
| 		more() { | ||||
| 			this.offset += limit; | ||||
|  | ||||
| 			const promise = this.$root.api('notes/search', { | ||||
| 				limit: limit + 1, | ||||
| 				offset: this.offset, | ||||
| 				query: this.q | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == limit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				for (const n of notes) { | ||||
| 					(this.$refs.timeline as any).append(n); | ||||
| 				} | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
|  | ||||
| 			return promise; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| @@ -38,7 +38,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XTl from './deck.tl.vue'; | ||||
| import XListTl from './deck.list-tl.vue'; | ||||
| @@ -1,14 +1,25 @@ | ||||
| <template> | ||||
| <x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/> | ||||
| <div class="iwaalbte" v-if="disabled"> | ||||
| 	<p> | ||||
| 		<fa :icon="faMinusCircle"/> | ||||
| 		{{ $t('disabled-timeline.title') }} | ||||
| 	</p> | ||||
| 	<p class="desc">{{ $t('disabled-timeline.description') }}</p> | ||||
| </div> | ||||
| <x-notes v-else ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import { faMinusCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import i18n from '../../../i18n'; | ||||
| 
 | ||||
| const fetchLimit = 10; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('deck'), | ||||
| 
 | ||||
| 	components: { | ||||
| 		XNotes | ||||
| 	}, | ||||
| @@ -36,7 +47,9 @@ export default Vue.extend({ | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			existMore: false, | ||||
| 			connection: null | ||||
| 			connection: null, | ||||
| 			disabled: false, | ||||
| 			faMinusCircle | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| @@ -75,6 +88,12 @@ export default Vue.extend({ | ||||
| 			this.connection.on('unfollow', this.onChangeFollowing); | ||||
| 		} | ||||
| 
 | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( | ||||
| 				meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) || | ||||
| 				meta.disableGlobalTimeline && ['global'].includes(this.src)); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| @@ -149,3 +168,16 @@ export default Vue.extend({ | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| .iwaalbte | ||||
| 	color var(--text) | ||||
| 	text-align center | ||||
| 
 | ||||
| 	> p | ||||
| 		margin 16px | ||||
| 
 | ||||
| 		&.desc | ||||
| 			font-size 14px | ||||
| 
 | ||||
| </style> | ||||
							
								
								
									
										244
									
								
								src/client/app/desktop/views/deck/deck.user-column.home.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								src/client/app/desktop/views/deck/deck.user-column.home.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,244 @@ | ||||
| <template> | ||||
| <div> | ||||
| 	<ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true"> | ||||
| 		<span slot="header"><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</span> | ||||
| 		<div> | ||||
| 			<x-note v-for="n in user.pinnedNotes" :key="n.id" :note="n" :mini="true"/> | ||||
| 		</div> | ||||
| 	</ui-container> | ||||
| 	<ui-container v-if="images.length > 0" :body-togglable="true"> | ||||
| 		<span slot="header"><fa :icon="['far', 'images']"/> {{ $t('images') }}</span> | ||||
| 		<div class="sainvnaq"> | ||||
| 			<router-link v-for="image in images" | ||||
| 				:style="`background-image: url(${image.thumbnailUrl})`" | ||||
| 				:key="`${image.id}:${image._note.id}`" | ||||
| 				:to="image._note | notePage" | ||||
| 				:title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`" | ||||
| 			></router-link> | ||||
| 		</div> | ||||
| 	</ui-container> | ||||
| 	<ui-container :body-togglable="true"> | ||||
| 		<span slot="header"><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</span> | ||||
| 		<div> | ||||
| 			<div ref="chart"></div> | ||||
| 		</div> | ||||
| 	</ui-container> | ||||
| 	<ui-container> | ||||
| 		<span slot="header"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</span> | ||||
| 		<div> | ||||
| 			<x-notes ref="timeline" :more="existMore ? fetchMoreNotes : null"/> | ||||
| 		</div> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import XNote from '../components/note.vue'; | ||||
| import { concat } from '../../../../../prelude/array'; | ||||
| import ApexCharts from 'apexcharts'; | ||||
|  | ||||
| const fetchLimit = 10; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('deck/deck.user-column.vue'), | ||||
| 	components: { | ||||
| 		XNotes, | ||||
| 		XNote | ||||
| 	}, | ||||
|  | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			existMore: false, | ||||
| 			moreFetching: false, | ||||
| 			withFiles: false, | ||||
| 			images: [], | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.$nextTick(() => { | ||||
| 				(this.$refs.timeline as any).init(() => this.initTl()); | ||||
| 			}); | ||||
|  | ||||
| 			const image = [ | ||||
| 				'image/jpeg', | ||||
| 				'image/png', | ||||
| 				'image/gif' | ||||
| 			]; | ||||
|  | ||||
| 			this.$root.api('users/notes', { | ||||
| 				userId: this.user.id, | ||||
| 				fileType: image, | ||||
| 				excludeNsfw: !this.$store.state.device.alwaysShowNsfw, | ||||
| 				limit: 9, | ||||
| 				untilDate: new Date().getTime() + 1000 * 86400 * 365 | ||||
| 			}).then(notes => { | ||||
| 				for (const note of notes) { | ||||
| 					for (const file of note.files) { | ||||
| 						file._note = note; | ||||
| 					} | ||||
| 				} | ||||
| 				const files = concat(notes.map((n: any): any[] => n.files)); | ||||
| 				this.images = files.filter(f => image.includes(f.type)).slice(0, 9); | ||||
| 			}); | ||||
|  | ||||
| 			this.$root.api('charts/user/notes', { | ||||
| 				userId: this.user.id, | ||||
| 				span: 'day', | ||||
| 				limit: 21 | ||||
| 			}).then(stats => { | ||||
| 				const normal = []; | ||||
| 				const reply = []; | ||||
| 				const renote = []; | ||||
|  | ||||
| 				const now = new Date(); | ||||
| 				const y = now.getFullYear(); | ||||
| 				const m = now.getMonth(); | ||||
| 				const d = now.getDate(); | ||||
|  | ||||
| 				for (let i = 0; i < 21; i++) { | ||||
| 					const x = new Date(y, m, d - i); | ||||
| 					normal.push([ | ||||
| 						x, | ||||
| 						stats.diffs.normal[i] | ||||
| 					]); | ||||
| 					reply.push([ | ||||
| 						x, | ||||
| 						stats.diffs.reply[i] | ||||
| 					]); | ||||
| 					renote.push([ | ||||
| 						x, | ||||
| 						stats.diffs.renote[i] | ||||
| 					]); | ||||
| 				} | ||||
|  | ||||
| 				const chart = new ApexCharts(this.$refs.chart, { | ||||
| 					chart: { | ||||
| 						type: 'bar', | ||||
| 						stacked: true, | ||||
| 						height: 100, | ||||
| 						sparkline: { | ||||
| 							enabled: true | ||||
| 						}, | ||||
| 					}, | ||||
| 					plotOptions: { | ||||
| 						bar: { | ||||
| 							columnWidth: '90%' | ||||
| 						} | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						clipMarkers: false, | ||||
| 						padding: { | ||||
| 							top: 16, | ||||
| 							right: 16, | ||||
| 							bottom: 16, | ||||
| 							left: 16 | ||||
| 						} | ||||
| 					}, | ||||
| 					tooltip: { | ||||
| 						shared: true, | ||||
| 						intersect: false | ||||
| 					}, | ||||
| 					series: [{ | ||||
| 						name: 'Normal', | ||||
| 						data: normal | ||||
| 					}, { | ||||
| 						name: 'Reply', | ||||
| 						data: reply | ||||
| 					}, { | ||||
| 						name: 'Renote', | ||||
| 						data: renote | ||||
| 					}], | ||||
| 					xaxis: { | ||||
| 						type: 'datetime', | ||||
| 						crosshairs: { | ||||
| 							width: 1, | ||||
| 							opacity: 1 | ||||
| 						} | ||||
| 					} | ||||
| 				}); | ||||
|  | ||||
| 				chart.render(); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		initTl() { | ||||
| 			return new Promise((res, rej) => { | ||||
| 				this.$root.api('users/notes', { | ||||
| 					userId: this.user.id, | ||||
| 					limit: fetchLimit + 1, | ||||
| 					untilDate: new Date().getTime() + 1000 * 86400 * 365, | ||||
| 					withFiles: this.withFiles, | ||||
| 					includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 					includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| 				}).then(notes => { | ||||
| 					if (notes.length == fetchLimit + 1) { | ||||
| 						notes.pop(); | ||||
| 						this.existMore = true; | ||||
| 					} | ||||
| 					res(notes); | ||||
| 				}, rej); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		fetchMoreNotes() { | ||||
| 			this.moreFetching = true; | ||||
|  | ||||
| 			const promise = this.$root.api('users/notes', { | ||||
| 				userId: this.user.id, | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime(), | ||||
| 				withFiles: this.withFiles, | ||||
| 				includeMyRenotes: this.$store.state.settings.showMyRenotes, | ||||
| 				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, | ||||
| 				includeLocalRenotes: this.$store.state.settings.showLocalRenotes | ||||
| 			}); | ||||
|  | ||||
| 			promise.then(notes => { | ||||
| 				if (notes.length == fetchLimit + 1) { | ||||
| 					notes.pop(); | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				for (const n of notes) (this.$refs.timeline as any).append(n); | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
|  | ||||
| 			return promise; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .sainvnaq | ||||
| 	display grid | ||||
| 	grid-template-columns 1fr 1fr 1fr | ||||
| 	gap 8px | ||||
| 	padding 16px | ||||
|  | ||||
| 	> * | ||||
| 		height 70px | ||||
| 		background-position center center | ||||
| 		background-size cover | ||||
| 		background-clip content-box | ||||
| 		border-radius 4px | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										262
									
								
								src/client/app/desktop/views/deck/deck.user-column.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								src/client/app/desktop/views/deck/deck.user-column.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,262 @@ | ||||
| <template> | ||||
| <x-column> | ||||
| 	<span slot="header"> | ||||
| 		<fa icon="user"/><mk-user-name :user="user" v-if="user"/> | ||||
| 	</span> | ||||
|  | ||||
| 	<div class="zubukjlciycdsyynicqrnlsmdwmymzqu" v-if="user"> | ||||
| 		<div class="is-remote" v-if="user.host != null"> | ||||
| 			<details> | ||||
| 				<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary> | ||||
| 				<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a> | ||||
| 			</details> | ||||
| 		</div> | ||||
| 		<header :style="bannerStyle"> | ||||
| 			<div> | ||||
| 				<button class="menu" @click="menu" ref="menu"><fa icon="ellipsis-h"/></button> | ||||
| 				<mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow" mini/> | ||||
| 				<mk-avatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 				<span class="name"> | ||||
| 					<mk-user-name :user="user"/> | ||||
| 				</span> | ||||
| 				<span class="acct">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span> | ||||
| 				<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		<div class="info"> | ||||
| 			<div class="description"> | ||||
| 				<mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> | ||||
| 			</div> | ||||
| 			<div class="fields" v-if="user.fields"> | ||||
| 				<dl class="field" v-for="(field, i) in user.fields" :key="i"> | ||||
| 					<dt class="name"> | ||||
| 						<mfm :text="field.name" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/> | ||||
| 					</dt> | ||||
| 					<dd class="value"> | ||||
| 						<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> | ||||
| 					</dd> | ||||
| 				</dl> | ||||
| 			</div> | ||||
| 			<div class="counts"> | ||||
| 				<div> | ||||
| 					<router-link :to="user | userPage()"> | ||||
| 						<b>{{ user.notesCount | number }}</b> | ||||
| 						<span>{{ $t('posts') }}</span> | ||||
| 					</router-link> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<router-link :to="user | userPage('following')"> | ||||
| 						<b>{{ user.followingCount | number }}</b> | ||||
| 						<span>{{ $t('following') }}</span> | ||||
| 					</router-link> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<router-link :to="user | userPage('followers')"> | ||||
| 						<b>{{ user.followersCount | number }}</b> | ||||
| 						<span>{{ $t('followers') }}</span> | ||||
| 					</router-link> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<router-view :user="user"></router-view> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import XUserMenu from '../../../common/views/components/user-menu.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('deck/deck.user-column.vue'), | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			user: null, | ||||
| 			fetching: true, | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		bannerStyle(): any { | ||||
| 			if (this.user == null) return {}; | ||||
| 			if (this.user.bannerUrl == null) return {}; | ||||
| 			return { | ||||
| 				backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, | ||||
| 				backgroundImage: `url(${ this.user.bannerUrl })` | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||
| 				this.user = user; | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		menu() { | ||||
| 			this.$root.new(XUserMenu, { | ||||
| 				source: this.$refs.menu, | ||||
| 				user: this.user | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .zubukjlciycdsyynicqrnlsmdwmymzqu | ||||
| 	background var(--deckColumnBg) | ||||
|  | ||||
| 	> .is-remote | ||||
| 		padding 8px 16px | ||||
| 		font-size 12px | ||||
|  | ||||
| 		&.is-remote | ||||
| 			color var(--remoteInfoFg) | ||||
| 			background var(--remoteInfoBg) | ||||
|  | ||||
| 		> a | ||||
| 			font-weight bold | ||||
|  | ||||
| 	> header | ||||
| 		overflow hidden | ||||
| 		background-size cover | ||||
| 		background-position center | ||||
|  | ||||
| 		> div | ||||
| 			padding 32px | ||||
| 			background rgba(#000, 0.5) | ||||
| 			color #fff | ||||
| 			text-align center | ||||
|  | ||||
| 			> .menu | ||||
| 				position absolute | ||||
| 				top 8px | ||||
| 				left 8px | ||||
| 				padding 8px | ||||
| 				font-size 16px | ||||
| 				text-shadow 0 0 8px #000 | ||||
|  | ||||
| 			> .follow | ||||
| 				position absolute | ||||
| 				top 16px | ||||
| 				right 16px | ||||
|  | ||||
| 			> .avatar | ||||
| 				display block | ||||
| 				width 64px | ||||
| 				height 64px | ||||
| 				margin 0 auto | ||||
|  | ||||
| 			> .name | ||||
| 				display block | ||||
| 				margin-top 8px | ||||
| 				font-weight bold | ||||
| 				text-shadow 0 0 8px #000 | ||||
|  | ||||
| 			> .acct | ||||
| 				display block | ||||
| 				font-size 14px | ||||
| 				opacity 0.7 | ||||
| 				text-shadow 0 0 8px #000 | ||||
|  | ||||
| 				> .locked | ||||
| 					opacity 0.8 | ||||
|  | ||||
| 			> .followed | ||||
| 				display inline-block | ||||
| 				font-size 12px | ||||
| 				background rgba(0, 0, 0, 0.5) | ||||
| 				opacity 0.7 | ||||
| 				margin-top: 2px | ||||
| 				padding 4px | ||||
| 				border-radius 4px | ||||
|  | ||||
| 	> .info | ||||
| 		padding 16px | ||||
| 		font-size 12px | ||||
| 		color var(--text) | ||||
| 		text-align center | ||||
| 		background var(--face) | ||||
|  | ||||
| 		&:before | ||||
| 			content "" | ||||
| 			display blcok | ||||
| 			position absolute | ||||
| 			top -32px | ||||
| 			left 0 | ||||
| 			right 0 | ||||
| 			width 0px | ||||
| 			margin 0 auto | ||||
| 			border-top solid 16px transparent | ||||
| 			border-left solid 16px transparent | ||||
| 			border-right solid 16px transparent | ||||
| 			border-bottom solid 16px var(--face) | ||||
|  | ||||
| 		> .fields | ||||
| 			margin-top 8px | ||||
|  | ||||
| 			> .field | ||||
| 				display flex | ||||
| 				padding 0 | ||||
| 				margin 0 | ||||
| 				align-items center | ||||
|  | ||||
| 				> .name | ||||
| 					padding 4px | ||||
| 					margin 4px | ||||
| 					width 30% | ||||
| 					overflow hidden | ||||
| 					white-space nowrap | ||||
| 					text-overflow ellipsis | ||||
| 					font-weight bold | ||||
|  | ||||
| 				> .value | ||||
| 					padding 4px | ||||
| 					margin 4px | ||||
| 					width 70% | ||||
| 					overflow hidden | ||||
| 					white-space nowrap | ||||
| 					text-overflow ellipsis | ||||
|  | ||||
| 		> .counts | ||||
| 			display grid | ||||
| 			grid-template-columns 2fr 2fr 2fr | ||||
| 			margin-top 8px | ||||
| 			border-top solid var(--lineWidth) var(--faceDivider) | ||||
|  | ||||
| 			> div | ||||
| 				padding 8px 8px 0 8px | ||||
| 				text-align center | ||||
|  | ||||
| 				> a | ||||
| 					color var(--text) | ||||
|  | ||||
| 					> b | ||||
| 						display block | ||||
| 						font-size 110% | ||||
|  | ||||
| 					> span | ||||
| 						display block | ||||
| 						font-size 80% | ||||
| 						opacity 0.7 | ||||
|  | ||||
| </style> | ||||
| @@ -9,11 +9,7 @@ | ||||
| 			</div> | ||||
| 			<x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])" @parentFocus="moveFocus(ids[0], $event)"/> | ||||
| 		</template> | ||||
| 		<template v-if="temporaryColumn"> | ||||
| 			<x-user-column v-if="temporaryColumn.type == 'user'" :acct="temporaryColumn.acct" :key="temporaryColumn.acct"/> | ||||
| 			<x-note-column v-else-if="temporaryColumn.type == 'note'" :note-id="temporaryColumn.noteId" :key="temporaryColumn.noteId"/> | ||||
| 			<x-hashtag-column v-else-if="temporaryColumn.type == 'tag'" :tag="temporaryColumn.tag" :key="temporaryColumn.tag"/> | ||||
| 		</template> | ||||
| 		<router-view></router-view> | ||||
| 		<button ref="add" @click="add" :title="$t('@deck.add-column')"><fa icon="plus"/></button> | ||||
| 	</div> | ||||
| </mk-ui> | ||||
| @@ -21,20 +17,17 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumnCore from './deck.column-core.vue'; | ||||
| import Menu from '../../../../common/views/components/menu.vue'; | ||||
| import MkUserListsWindow from '../../components/user-lists-window.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({ | ||||
| 	i18n: i18n('deck'), | ||||
| 	components: { | ||||
| 		XColumnCore, | ||||
| 		XUserColumn: () => import('./deck.user-column.vue').then(m => m.default), | ||||
| 		XNoteColumn: () => import('./deck.note-column.vue').then(m => m.default), | ||||
| 		XHashtagColumn: () => import('./deck.hashtag-column.vue').then(m => m.default) | ||||
| 		XColumnCore | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| @@ -55,10 +48,6 @@ export default Vue.extend({ | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		temporaryColumn(): any { | ||||
| 			return this.$store.state.device.deckTemporaryColumn; | ||||
| 		}, | ||||
| 
 | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				't': this.focus | ||||
| @@ -67,15 +56,14 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		temporaryColumn() { | ||||
| 			if (this.temporaryColumn != null) { | ||||
| 				this.$nextTick(() => { | ||||
| 					this.$refs.body.scrollTo({ | ||||
| 						left: this.$refs.body.scrollWidth - this.$refs.body.clientWidth, | ||||
| 						behavior: 'smooth' | ||||
| 					}); | ||||
| 		$route() { | ||||
| 			if (this.$route.name == 'index') return; | ||||
| 			this.$nextTick(() => { | ||||
| 				this.$refs.body.scrollTo({ | ||||
| 					left: this.$refs.body.scrollWidth - this.$refs.body.clientWidth, | ||||
| 					behavior: 'smooth' | ||||
| 				}); | ||||
| 			} | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| @@ -86,8 +74,6 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.$store.commit('navHook', this.onNav); | ||||
| 
 | ||||
| 		if (this.$store.state.settings.deck == null) { | ||||
| 			const deck = { | ||||
| 				columns: [/*{ | ||||
| @@ -133,8 +119,6 @@ export default Vue.extend({ | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		this.$store.commit('navHook', null); | ||||
| 
 | ||||
| 		document.documentElement.style.overflow = 'auto'; | ||||
| 	}, | ||||
| 
 | ||||
| @@ -143,39 +127,6 @@ export default Vue.extend({ | ||||
| 			return this.$refs[id][0]; | ||||
| 		}, | ||||
| 
 | ||||
| 		onNav(to) { | ||||
| 			if (!this.$store.state.settings.deckNav) return false; | ||||
| 
 | ||||
| 			if (to.name == 'user') { | ||||
| 				this.$store.commit('device/set', { | ||||
| 					key: 'deckTemporaryColumn', | ||||
| 					value: { | ||||
| 						type: 'user', | ||||
| 						acct: to.params.user | ||||
| 					} | ||||
| 				}); | ||||
| 				return true; | ||||
| 			} else if (to.name == 'note') { | ||||
| 				this.$store.commit('device/set', { | ||||
| 					key: 'deckTemporaryColumn', | ||||
| 					value: { | ||||
| 						type: 'note', | ||||
| 						noteId: to.params.note | ||||
| 					} | ||||
| 				}); | ||||
| 				return true; | ||||
| 			} else if (to.name == 'tag') { | ||||
| 				this.$store.commit('device/set', { | ||||
| 					key: 'deckTemporaryColumn', | ||||
| 					value: { | ||||
| 						type: 'tag', | ||||
| 						tag: to.params.tag | ||||
| 					} | ||||
| 				}); | ||||
| 				return true; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		add() { | ||||
| 			this.$root.new(Menu, { | ||||
| 				source: this.$refs.add, | ||||
| @@ -50,7 +50,7 @@ | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XColumn from './deck.column.vue'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import * as uuid from 'uuid'; | ||||
| @@ -1,16 +1,14 @@ | ||||
| <template> | ||||
| <mk-ui> | ||||
| 	<main v-if="!fetching"> | ||||
| 		<sequential-entrance animation="entranceFromTop" delay="25"> | ||||
| 			<template v-for="favorite in favorites"> | ||||
| 				<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/> | ||||
| 			</template> | ||||
| 		</sequential-entrance> | ||||
| 		<div class="more" v-if="existMore"> | ||||
| 			<ui-button inline @click="more">{{ $t('@.load-more') }}</ui-button> | ||||
| 		</div> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
| <div class="ecsvsegy" v-if="!fetching"> | ||||
| 	<sequential-entrance animation="entranceFromTop" delay="25"> | ||||
| 		<template v-for="favorite in favorites"> | ||||
| 			<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/> | ||||
| 		</template> | ||||
| 	</sequential-entrance> | ||||
| 	<div class="more" v-if="existMore"> | ||||
| 		<ui-button inline @click="more">{{ $t('@.load-more') }}</ui-button> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| @@ -72,10 +70,8 @@ export default Vue.extend({ | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| main | ||||
| .ecsvsegy | ||||
| 	margin 0 auto | ||||
| 	padding 16px | ||||
| 	max-width 700px | ||||
| 
 | ||||
| 	> * > .post | ||||
| 		margin-bottom 16px | ||||
							
								
								
									
										55
									
								
								src/client/app/desktop/views/home/featured.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/client/app/desktop/views/home/featured.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| <template> | ||||
| <div class="glowckho" v-if="!fetching"> | ||||
| 	<sequential-entrance animation="entranceFromTop" delay="25"> | ||||
| 		<template v-for="note in notes"> | ||||
| 			<mk-note-detail class="post" :note="note" :key="note.id"/> | ||||
| 		</template> | ||||
| 	</sequential-entrance> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import Progress from '../../../common/scripts/loading'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			notes: [], | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			Progress.start(); | ||||
| 			this.fetching = true; | ||||
|  | ||||
| 			this.$root.api('notes/featured', { | ||||
| 				limit: 20 | ||||
| 			}).then(notes => { | ||||
| 				notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); | ||||
| 				this.notes = notes; | ||||
| 				this.fetching = false; | ||||
|  | ||||
| 				Progress.done(); | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .glowckho | ||||
| 	margin 0 auto | ||||
|  | ||||
| 	> * > .post | ||||
| 		margin-bottom 16px | ||||
|  | ||||
| 	> .more | ||||
| 		margin 32px 16px 16px 16px | ||||
| 		text-align center | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										404
									
								
								src/client/app/desktop/views/home/home.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								src/client/app/desktop/views/home/home.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,404 @@ | ||||
| <template> | ||||
| <component :is="customize ? 'mk-dummy' : 'mk-ui'" v-hotkey.global="keymap" v-if="$store.getters.isSignedIn || $route.name != 'index'"> | ||||
| 	<div class="wqsofvpm" :data-customize="customize"> | ||||
| 		<div class="customize" v-if="customize"> | ||||
| 			<a @click="done()"><fa icon="check"/>{{ $t('done') }}</a> | ||||
| 			<div> | ||||
| 				<div class="adder"> | ||||
| 					<p>{{ $t('add-widget') }}</p> | ||||
| 					<select v-model="widgetAdderSelected"> | ||||
| 						<option value="profile">{{ $t('@.widgets.profile') }}</option> | ||||
| 						<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> | ||||
| 						<option value="calendar">{{ $t('@.widgets.calendar') }}</option> | ||||
| 						<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option> | ||||
| 						<option value="activity">{{ $t('@.widgets.activity') }}</option> | ||||
| 						<option value="rss">{{ $t('@.widgets.rss') }}</option> | ||||
| 						<option value="trends">{{ $t('@.widgets.trends') }}</option> | ||||
| 						<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> | ||||
| 						<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> | ||||
| 						<option value="version">{{ $t('@.widgets.version') }}</option> | ||||
| 						<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option> | ||||
| 						<option value="notifications">{{ $t('@.widgets.notifications') }}</option> | ||||
| 						<option value="users">{{ $t('@.widgets.users') }}</option> | ||||
| 						<option value="polls">{{ $t('@.widgets.polls') }}</option> | ||||
| 						<option value="post-form">{{ $t('@.widgets.post-form') }}</option> | ||||
| 						<option value="messaging">{{ $t('@.widgets.messaging') }}</option> | ||||
| 						<option value="memo">{{ $t('@.widgets.memo') }}</option> | ||||
| 						<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> | ||||
| 						<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> | ||||
| 						<option value="server">{{ $t('@.widgets.server') }}</option> | ||||
| 						<option value="nav">{{ $t('@.widgets.nav') }}</option> | ||||
| 						<option value="tips">{{ $t('@.widgets.tips') }}</option> | ||||
| 					</select> | ||||
| 					<button @click="addWidget">{{ $t('add') }}</button> | ||||
| 				</div> | ||||
| 				<div class="trash"> | ||||
| 					<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable> | ||||
| 					<p>{{ $t('@.trash') }}</p> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }"> | ||||
| 			<template v-if="customize"> | ||||
| 				<x-draggable v-for="place in ['left', 'right']" | ||||
| 					:list="widgets[place]" | ||||
| 					:class="place" | ||||
| 					:data-place="place" | ||||
| 					:options="{ group: 'x', animation: 150 }" | ||||
| 					@sort="onWidgetSort" | ||||
| 					:key="place" | ||||
| 				> | ||||
| 					<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)"> | ||||
| 						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/> | ||||
| 					</div> | ||||
| 				</x-draggable> | ||||
| 				<div class="main"> | ||||
| 					<a @click="hint">{{ $t('@.customization-tips.title') }}</a> | ||||
| 					<div> | ||||
| 						<x-timeline/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 			<template v-else> | ||||
| 				<div v-for="place in ['left', 'right']" :class="place"> | ||||
| 					<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/> | ||||
| 				</div> | ||||
| 				<div class="main"> | ||||
| 					<router-view ref="content"></router-view> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </component> | ||||
| <x-welcome v-else/> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import * as uuid from 'uuid'; | ||||
| import XWelcome from '../pages/welcome.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/home.vue'), | ||||
| 	components: { | ||||
| 		XDraggable, | ||||
| 		XWelcome | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			customize: window.location.search == '?customize', | ||||
| 			connection: null, | ||||
| 			widgetAdderSelected: null, | ||||
| 			trash: [], | ||||
| 			view: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	computed: { | ||||
| 		home(): any[] { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				return this.$store.state.settings.home || []; | ||||
| 			} else { | ||||
| 				return [{ | ||||
| 					name: 'instance', | ||||
| 					place: 'right' | ||||
| 				}, { | ||||
| 					name: 'broadcast', | ||||
| 					place: 'right', | ||||
| 					data: {} | ||||
| 				}]; | ||||
| 			} | ||||
| 		}, | ||||
| 		left(): any[] { | ||||
| 			return this.home.filter(w => w.place == 'left'); | ||||
| 		}, | ||||
| 		right(): any[] { | ||||
| 			return this.home.filter(w => w.place == 'right'); | ||||
| 		}, | ||||
| 		widgets(): any { | ||||
| 			return { | ||||
| 				left: this.left, | ||||
| 				right: this.right | ||||
| 			}; | ||||
| 		}, | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				't': this.focus | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			const defaultDesktopHomeWidgets = { | ||||
| 				left: [ | ||||
| 					'profile', | ||||
| 					'calendar', | ||||
| 					'activity', | ||||
| 					'rss', | ||||
| 					'hashtags', | ||||
| 					'photo-stream', | ||||
| 					'version' | ||||
| 				], | ||||
| 				right: [ | ||||
| 					'customize', | ||||
| 					'broadcast', | ||||
| 					'notifications', | ||||
| 					'users', | ||||
| 					'polls', | ||||
| 					'server', | ||||
| 					'nav', | ||||
| 					'tips' | ||||
| 				] | ||||
| 			}; | ||||
|  | ||||
| 			//#region Construct home data | ||||
| 			const _defaultDesktopHomeWidgets = []; | ||||
|  | ||||
| 			for (const widget of defaultDesktopHomeWidgets.left) { | ||||
| 				_defaultDesktopHomeWidgets.push({ | ||||
| 					name: widget, | ||||
| 					id: uuid(), | ||||
| 					place: 'left', | ||||
| 					data: {} | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			for (const widget of defaultDesktopHomeWidgets.right) { | ||||
| 				_defaultDesktopHomeWidgets.push({ | ||||
| 					name: widget, | ||||
| 					id: uuid(), | ||||
| 					place: 'right', | ||||
| 					data: {} | ||||
| 				}); | ||||
| 			} | ||||
| 			//#endregion | ||||
|  | ||||
| 			if (this.$store.state.settings.home == null) { | ||||
| 				this.$root.api('i/update_home', { | ||||
| 					home: _defaultDesktopHomeWidgets | ||||
| 				}).then(() => { | ||||
| 					this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets); | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 	}, | ||||
|  | ||||
| 	beforeDestroy() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		hint() { | ||||
| 			this.$root.dialog({ | ||||
| 				title: this.$t('@.customization-tips.title'), | ||||
| 				text: this.$t('@.customization-tips.paragraph') | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		onTlLoaded() { | ||||
| 			this.$emit('loaded'); | ||||
| 		}, | ||||
|  | ||||
| 		onWidgetContextmenu(widgetId) { | ||||
| 			const w = (this.$refs[widgetId] as any)[0]; | ||||
| 			if (w.func) w.func(); | ||||
| 		}, | ||||
|  | ||||
| 		onWidgetSort() { | ||||
| 			this.saveHome(); | ||||
| 		}, | ||||
|  | ||||
| 		onTrash(evt) { | ||||
| 			this.saveHome(); | ||||
| 		}, | ||||
|  | ||||
| 		addWidget() { | ||||
| 			this.$store.dispatch('settings/addHomeWidget', { | ||||
| 				name: this.widgetAdderSelected, | ||||
| 				id: uuid(), | ||||
| 				place: 'left', | ||||
| 				data: {} | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		saveHome() { | ||||
| 			const left = this.widgets.left; | ||||
| 			const right = this.widgets.right; | ||||
| 			this.$store.commit('settings/setHome', left.concat(right)); | ||||
| 			for (const w of left) w.place = 'left'; | ||||
| 			for (const w of right) w.place = 'right'; | ||||
| 			this.$root.api('i/update_home', { | ||||
| 				home: this.home | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		done() { | ||||
| 			location.href = '/'; | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.content as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .wqsofvpm | ||||
| 	display block | ||||
|  | ||||
| 	&[data-customize] | ||||
| 		padding-top 48px | ||||
| 		background-image url('/assets/desktop/grid.svg') | ||||
|  | ||||
| 		> .main > .main | ||||
| 			> a | ||||
| 				display block | ||||
| 				margin-bottom 8px | ||||
| 				text-align center | ||||
|  | ||||
| 			> div | ||||
| 				cursor not-allowed !important | ||||
|  | ||||
| 				> * | ||||
| 					pointer-events none | ||||
|  | ||||
| 	&:not([data-customize]) | ||||
| 		> .main > *:not(.main):empty | ||||
| 			display none | ||||
|  | ||||
| 	> .customize | ||||
| 		position fixed | ||||
| 		z-index 1000 | ||||
| 		top 0 | ||||
| 		left 0 | ||||
| 		width 100% | ||||
| 		height 48px | ||||
| 		color var(--text) | ||||
| 		background var(--desktopHeaderBg) | ||||
| 		box-shadow 0 1px 1px rgba(#000, 0.075) | ||||
|  | ||||
| 		> a | ||||
| 			display block | ||||
| 			position absolute | ||||
| 			z-index 1001 | ||||
| 			top 0 | ||||
| 			right 0 | ||||
| 			padding 0 16px | ||||
| 			line-height 48px | ||||
| 			text-decoration none | ||||
| 			color var(--primaryForeground) | ||||
| 			background var(--primary) | ||||
| 			transition background 0.1s ease | ||||
|  | ||||
| 			&:hover | ||||
| 				background var(--primaryLighten10) | ||||
|  | ||||
| 			&:active | ||||
| 				background var(--primaryDarken10) | ||||
| 				transition background 0s ease | ||||
|  | ||||
| 			> [data-icon] | ||||
| 				margin-right 8px | ||||
|  | ||||
| 		> div | ||||
| 			display flex | ||||
| 			margin 0 auto | ||||
| 			max-width 1220px - 32px | ||||
|  | ||||
| 			> div | ||||
| 				width 50% | ||||
|  | ||||
| 				&.adder | ||||
| 					> p | ||||
| 						display inline | ||||
| 						line-height 48px | ||||
|  | ||||
| 				&.trash | ||||
| 					border-left solid 1px var(--faceDivider) | ||||
|  | ||||
| 					> div | ||||
| 						width 100% | ||||
| 						height 100% | ||||
|  | ||||
| 					> p | ||||
| 						position absolute | ||||
| 						top 0 | ||||
| 						left 0 | ||||
| 						width 100% | ||||
| 						line-height 48px | ||||
| 						margin 0 | ||||
| 						text-align center | ||||
| 						pointer-events none | ||||
|  | ||||
| 	> .main | ||||
| 		display flex | ||||
| 		justify-content center | ||||
| 		margin 0 auto | ||||
| 		max-width 1240px | ||||
|  | ||||
| 		> * | ||||
| 			.customize-container | ||||
| 				cursor move | ||||
| 				border-radius 6px | ||||
|  | ||||
| 				&:hover | ||||
| 					box-shadow 0 0 8px rgba(64, 120, 200, 0.3) | ||||
|  | ||||
| 				> * | ||||
| 					pointer-events none | ||||
|  | ||||
| 		> .main | ||||
| 			padding 16px | ||||
| 			width calc(100% - 280px * 2) | ||||
| 			order 2 | ||||
|  | ||||
| 		&.side | ||||
| 			> .main | ||||
| 				width calc(100% - 280px) | ||||
| 				max-width 680px | ||||
|  | ||||
| 		> *:not(.main) | ||||
| 			width 280px | ||||
| 			padding 16px 0 16px 0 | ||||
|  | ||||
| 			> *:not(:last-child) | ||||
| 				margin-bottom 16px | ||||
|  | ||||
| 		> .left | ||||
| 			padding-left 16px | ||||
| 			order 1 | ||||
|  | ||||
| 		> .right | ||||
| 			padding-right 16px | ||||
| 			order 3 | ||||
|  | ||||
| 		&.side | ||||
| 			@media (max-width 1000px) | ||||
| 				> *:not(.main) | ||||
| 					display none | ||||
|  | ||||
| 				> .main | ||||
| 					width 100% | ||||
| 					max-width 700px | ||||
| 					margin 0 auto | ||||
|  | ||||
| 		&:not(.side) | ||||
| 			@media (max-width 1200px) | ||||
| 				> *:not(.main) | ||||
| 					display none | ||||
|  | ||||
| 				> .main | ||||
| 					width 100% | ||||
| 					max-width 700px | ||||
| 					margin 0 auto | ||||
|  | ||||
| </style> | ||||
| @@ -1,13 +1,11 @@ | ||||
| <template> | ||||
| <mk-ui> | ||||
| 	<main v-if="!fetching"> | ||||
| 		<mk-note-detail :note="note"/> | ||||
| 		<footer> | ||||
| 			<router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> {{ $t('next') }}</router-link> | ||||
| 			<router-link v-if="note.prev" :to="note.prev">{{ $t('prev') }} <fa icon="angle-right"/></router-link> | ||||
| 		</footer> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
| <div v-if="!fetching" class="kcthdwmv"> | ||||
| 	<mk-note-detail :note="note"/> | ||||
| 	<footer> | ||||
| 		<router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> {{ $t('next') }}</router-link> | ||||
| 		<router-link v-if="note.prev" :to="note.prev">{{ $t('prev') }} <fa icon="angle-right"/></router-link> | ||||
| 	</footer> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| @@ -48,8 +46,7 @@ export default Vue.extend({ | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| main | ||||
| 	padding 16px | ||||
| .kcthdwmv | ||||
| 	text-align center | ||||
| 
 | ||||
| 	> footer | ||||
| @@ -59,8 +56,4 @@ main | ||||
| 			display inline-block | ||||
| 			margin 0 16px | ||||
| 
 | ||||
| 	> .mk-note-detail | ||||
| 		margin 0 auto | ||||
| 		width 640px | ||||
| 
 | ||||
| </style> | ||||
| @@ -1,12 +1,14 @@ | ||||
| <template> | ||||
| <mk-ui> | ||||
| 	<header :class="$style.header"> | ||||
| 		<h1>{{ q }}</h1> | ||||
| 	</header> | ||||
| 	<p :class="$style.notAvailable" v-if="!fetching && notAvailable">{{ $t('not-available') }}</p> | ||||
| 	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('not-found', { q }) }}</p> | ||||
| 	<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/> | ||||
| </mk-ui> | ||||
| <div class="oxgbmvii"> | ||||
| 	<div class="notes"> | ||||
| 		<header> | ||||
| 			<span><fa icon="search"/> {{ q }}</span> | ||||
| 		</header> | ||||
| 		<p v-if="!fetching && notAvailable">{{ $t('not-available') }}</p> | ||||
| 		<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('not-found', { q }) }}</p> | ||||
| 		<mk-notes ref="timeline" :more="existMore ? more : null"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| @@ -106,45 +108,23 @@ export default Vue.extend({ | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" module> | ||||
| .header | ||||
| 	width 100% | ||||
| 	max-width 600px | ||||
| 	margin 0 auto | ||||
| 	color #555 | ||||
| <style lang="stylus" scoped> | ||||
| .oxgbmvii | ||||
| 	> .notes | ||||
| 		background var(--face) | ||||
| 		box-shadow var(--shadow) | ||||
| 		border-radius var(--round) | ||||
| 		overflow hidden | ||||
| 
 | ||||
| .notes | ||||
| 	max-width 600px | ||||
| 	margin 0 auto | ||||
| 	border solid 1px rgba(#000, 0.075) | ||||
| 	border-radius 6px | ||||
| 	overflow hidden | ||||
| 		> header | ||||
| 			padding 0 8px | ||||
| 			z-index 10 | ||||
| 			background var(--faceHeader) | ||||
| 			box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) | ||||
| 
 | ||||
| .empty | ||||
| 	display block | ||||
| 	margin 0 auto | ||||
| 	padding 32px | ||||
| 	max-width 400px | ||||
| 	text-align center | ||||
| 	color #999 | ||||
| 
 | ||||
| 	> [data-icon] | ||||
| 		display block | ||||
| 		margin-bottom 16px | ||||
| 		font-size 3em | ||||
| 		color #ccc | ||||
| 
 | ||||
| .notAvailable | ||||
| 	display block | ||||
| 	margin 0 auto | ||||
| 	padding 32px | ||||
| 	max-width 400px | ||||
| 	text-align center | ||||
| 	color #999 | ||||
| 
 | ||||
| 	> [data-icon] | ||||
| 		display block | ||||
| 		margin-bottom 16px | ||||
| 		font-size 3em | ||||
| 		color #ccc | ||||
| 			> span | ||||
| 				padding 0 8px | ||||
| 				font-size 0.9em | ||||
| 				line-height 42px | ||||
| 				color var(--text) | ||||
| </style> | ||||
| @@ -1,11 +1,8 @@ | ||||
| <template> | ||||
| <mk-ui> | ||||
| 	<header :class="$style.header"> | ||||
| 		<h1>#{{ $route.params.tag }}</h1> | ||||
| 	</header> | ||||
| <div> | ||||
| 	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p> | ||||
| 	<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/> | ||||
| </mk-ui> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| @@ -96,17 +93,10 @@ export default Vue.extend({ | ||||
| </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 | ||||
| 	background var(--face) | ||||
| 	box-shadow var(--shadow) | ||||
| 	border-radius var(--round) | ||||
| 	overflow hidden | ||||
| 
 | ||||
| .empty | ||||
							
								
								
									
										273
									
								
								src/client/app/desktop/views/home/timeline.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/client/app/desktop/views/home/timeline.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,273 @@ | ||||
| <template> | ||||
| <div class="mk-timeline"> | ||||
| 	<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/> | ||||
| 	<div class="main"> | ||||
| 		<header> | ||||
| 			<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> | ||||
| 			<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> | ||||
| 			<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> | ||||
| 			<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> | ||||
| 			<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> | ||||
| 			<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> | ||||
| 			<div class="buttons"> | ||||
| 				<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> | ||||
| 				<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> | ||||
| 				<button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button> | ||||
| 				<button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> | ||||
| 		<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> | ||||
| 		<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||
| 		<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||
| 		<x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||
| 		<x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> | ||||
| 		<x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> | ||||
| 		<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import XCore from './timeline.core.vue'; | ||||
| import Menu from '../../../common/views/components/menu.vue'; | ||||
| import MkSettingsWindow from '../components/settings-window.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/components/timeline.vue'), | ||||
| 	components: { | ||||
| 		XCore | ||||
| 	}, | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			src: 'home', | ||||
| 			list: null, | ||||
| 			tagTl: null, | ||||
| 			enableLocalTimeline: false, | ||||
| 			enableGlobalTimeline: false, | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	watch: { | ||||
| 		src() { | ||||
| 			this.saveSrc(); | ||||
| 		}, | ||||
|  | ||||
| 		list(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.tagTl = null; | ||||
| 		}, | ||||
|  | ||||
| 		tagTl(x) { | ||||
| 			this.saveSrc(); | ||||
| 			if (x != null) this.list = null; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	created() { | ||||
| 		this.$root.getMeta().then((meta: Record<string, any>) => { | ||||
| 			if (!( | ||||
| 				this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin | ||||
| 			) && this.src === 'global') this.src = 'local'; | ||||
| 			if (!( | ||||
| 				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin | ||||
| 			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; | ||||
| 		}); | ||||
|  | ||||
| 		if (this.$store.state.device.tl) { | ||||
| 			this.src = this.$store.state.device.tl.src; | ||||
| 			if (this.src == 'list') { | ||||
| 				this.list = this.$store.state.device.tl.arg; | ||||
| 			} else if (this.src == 'tag') { | ||||
| 				this.tagTl = this.$store.state.device.tl.arg; | ||||
| 			} | ||||
| 		} else if (this.$store.state.i.followingCount == 0) { | ||||
| 			this.src = 'hybrid'; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	mounted() { | ||||
| 		(this.$refs.tl as any).$once('loaded', () => { | ||||
| 			this.$emit('loaded'); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		saveSrc() { | ||||
| 			this.$store.commit('device/setTl', { | ||||
| 				src: this.src, | ||||
| 				arg: this.src == 'list' ? this.list : this.tagTl | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		}, | ||||
|  | ||||
| 		warp(date) { | ||||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		}, | ||||
|  | ||||
| 		async chooseList() { | ||||
| 			const lists = await this.$root.api('users/lists/list'); | ||||
|  | ||||
| 			let menu = [{ | ||||
| 				icon: 'plus', | ||||
| 				text: this.$t('add-list'), | ||||
| 				action: () => { | ||||
| 					this.$root.dialog({ | ||||
| 						title: this.$t('list-name'), | ||||
| 						input: true | ||||
| 					}).then(async ({ canceled, result: title }) => { | ||||
| 						if (canceled) return; | ||||
| 						const list = await this.$root.api('users/lists/create', { | ||||
| 							title | ||||
| 						}); | ||||
|  | ||||
| 						this.list = list; | ||||
| 						this.src = 'list'; | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
|  | ||||
| 			if (lists.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
|  | ||||
| 			menu = menu.concat(lists.map(list => ({ | ||||
| 				icon: 'list', | ||||
| 				text: list.title, | ||||
| 				action: () => { | ||||
| 					this.list = list; | ||||
| 					this.src = 'list'; | ||||
| 				} | ||||
| 			}))); | ||||
|  | ||||
| 			this.$root.new(Menu, { | ||||
| 				source: this.$refs.listButton, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		chooseTag() { | ||||
| 			let menu = [{ | ||||
| 				icon: 'plus', | ||||
| 				text: this.$t('add-tag-timeline'), | ||||
| 				action: () => { | ||||
| 					this.$root.new(MkSettingsWindow, { | ||||
| 						initialPage: 'hashtags' | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
|  | ||||
| 			if (this.$store.state.settings.tagTimelines.length > 0) { | ||||
| 				menu.push(null); | ||||
| 			} | ||||
|  | ||||
| 			menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({ | ||||
| 				icon: 'hashtag', | ||||
| 				text: t.title, | ||||
| 				action: () => { | ||||
| 					this.tagTl = t; | ||||
| 					this.src = 'tag'; | ||||
| 				} | ||||
| 			}))); | ||||
|  | ||||
| 			this.$root.new(Menu, { | ||||
| 				source: this.$refs.tagButton, | ||||
| 				items: menu | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .mk-timeline | ||||
| 	> .form | ||||
| 		margin-bottom 16px | ||||
| 		box-shadow var(--shadow) | ||||
| 		border-radius var(--round) | ||||
|  | ||||
| 	> .main | ||||
| 		background var(--face) | ||||
| 		box-shadow var(--shadow) | ||||
| 		border-radius var(--round) | ||||
| 		overflow hidden | ||||
|  | ||||
| 		> header | ||||
| 			padding 0 8px | ||||
| 			z-index 10 | ||||
| 			background var(--faceHeader) | ||||
| 			box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) | ||||
|  | ||||
| 			> .buttons | ||||
| 				position absolute | ||||
| 				z-index 2 | ||||
| 				top 0 | ||||
| 				right 0 | ||||
| 				padding-right 8px | ||||
|  | ||||
| 				> button | ||||
| 					padding 0 8px | ||||
| 					font-size 0.9em | ||||
| 					line-height 42px | ||||
| 					color var(--faceTextButton) | ||||
|  | ||||
| 					> .badge | ||||
| 						position absolute | ||||
| 						top -4px | ||||
| 						right 4px | ||||
| 						font-size 10px | ||||
| 						color var(--notificationIndicator) | ||||
|  | ||||
| 					&:hover | ||||
| 						color var(--faceTextButtonHover) | ||||
|  | ||||
| 					&[data-active] | ||||
| 						color var(--primary) | ||||
| 						cursor default | ||||
|  | ||||
| 						&:before | ||||
| 							content "" | ||||
| 							display block | ||||
| 							position absolute | ||||
| 							bottom 0 | ||||
| 							left 0 | ||||
| 							width 100% | ||||
| 							height 2px | ||||
| 							background var(--primary) | ||||
|  | ||||
| 			> span | ||||
| 				display inline-block | ||||
| 				padding 0 10px | ||||
| 				line-height 42px | ||||
| 				font-size 12px | ||||
| 				user-select none | ||||
|  | ||||
| 				&[data-active] | ||||
| 					color var(--primary) | ||||
| 					cursor default | ||||
| 					font-weight bold | ||||
|  | ||||
| 					&:before | ||||
| 						content "" | ||||
| 						display block | ||||
| 						position absolute | ||||
| 						bottom 0 | ||||
| 						left -8px | ||||
| 						width calc(100% + 16px) | ||||
| 						height 2px | ||||
| 						background var(--primary) | ||||
|  | ||||
| 				&:not([data-active]) | ||||
| 					color var(--desktopTimelineSrc) | ||||
| 					cursor pointer | ||||
|  | ||||
| 					&:hover | ||||
| 						color var(--desktopTimelineSrcHover) | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										82
									
								
								src/client/app/desktop/views/home/user/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/client/app/desktop/views/home/user/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| <template> | ||||
| <div class="omechnps" v-if="!fetching"> | ||||
| 	<div class="is-suspended" v-if="user.isSuspended"><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</div> | ||||
| 	<div class="is-remote" v-if="user.host != null"><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></div> | ||||
| 	<div class="main"> | ||||
| 		<x-header class="header" :user="user"/> | ||||
| 		<router-view :user="user"></router-view> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import parseAcct from '../../../../../../misc/acct/parse'; | ||||
| import Progress from '../../../../common/scripts/loading'; | ||||
| import XHeader from './user.header.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
| 	components: { | ||||
| 		XHeader | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			user: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 			Progress.start(); | ||||
| 			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||
| 				this.user = user; | ||||
| 				this.fetching = false; | ||||
| 				Progress.done(); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		warp(date) { | ||||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .omechnps | ||||
| 	width 100% | ||||
| 	margin 0 auto | ||||
|  | ||||
| 	> .is-suspended | ||||
| 	> .is-remote | ||||
| 		margin-bottom 16px | ||||
| 		padding 14px 16px | ||||
| 		font-size 14px | ||||
| 		box-shadow var(--shadow) | ||||
| 		border-radius var(--round) | ||||
|  | ||||
| 		&.is-suspended | ||||
| 			color var(--suspendedInfoFg) | ||||
| 			background var(--suspendedInfoBg) | ||||
|  | ||||
| 		&.is-remote | ||||
| 			color var(--remoteInfoFg) | ||||
| 			background var(--remoteInfoBg) | ||||
|  | ||||
| 		> a | ||||
| 			font-weight bold | ||||
|  | ||||
| 	> .main | ||||
| 		> .header | ||||
| 			margin-bottom 16px | ||||
|  | ||||
| </style> | ||||
| @@ -6,7 +6,7 @@ | ||||
| 		<div class="user" v-for="friend in users"> | ||||
| 			<mk-avatar class="avatar" :user="friend"/> | ||||
| 			<div class="body"> | ||||
| 				<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link> | ||||
| 				<router-link class="name" :to="friend | userPage" v-user-preview="friend.id"><mk-user-name :user="friend"/></router-link> | ||||
| 				<p class="username">@{{ friend | acct }}</p> | ||||
| 			</div> | ||||
| 		</div> | ||||
| @@ -9,12 +9,19 @@ | ||||
| 			</p> | ||||
| 			<div> | ||||
| 				<span class="username"><mk-acct :user="user" :detail="true" /></span> | ||||
| 				<span v-if="user.isBot" :title="$t('title')"><fa icon="robot"/></span> | ||||
| 				<span v-if="user.isBot" :title="$t('is-bot')"><fa icon="robot"/></span> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<mk-avatar class="avatar" :user="user" :disable-preview="true"/> | ||||
| 	<div class="body"> | ||||
| 		<div class="actions" v-if="$store.getters.isSignedIn"> | ||||
| 			<template v-if="$store.state.i.id != user.id"> | ||||
| 				<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span> | ||||
| 				<mk-follow-button :user="user" :inline="true" class="follow"/> | ||||
| 			</template> | ||||
| 			<ui-button @click="menu" ref="menu" :inline="true"><fa icon="ellipsis-h"/></ui-button> | ||||
| 		</div> | ||||
| 		<div class="description"> | ||||
| 			<mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> | ||||
| 		</div> | ||||
| @@ -33,7 +40,7 @@ | ||||
| 			<span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> | ||||
| 		</div> | ||||
| 		<div class="status"> | ||||
| 			<span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span> | ||||
| 			<router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link> | ||||
| 			<router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link> | ||||
| 			<router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link> | ||||
| 		</div> | ||||
| @@ -45,6 +52,7 @@ | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import * as age from 's-age'; | ||||
| import XUserMenu from '../../../../common/views/components/user-menu.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/pages/user/user.header.vue'), | ||||
| @@ -99,6 +107,13 @@ export default Vue.extend({ | ||||
| 			this.$updateBanner().then(i => { | ||||
| 				this.user.bannerUrl = i.bannerUrl; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		menu() { | ||||
| 			this.$root.new(XUserMenu, { | ||||
| 				source: this.$refs.menu.$el, | ||||
| 				user: this.user | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| @@ -187,6 +202,18 @@ export default Vue.extend({ | ||||
| 		padding 16px 16px 16px 154px | ||||
| 		color var(--text) | ||||
| 
 | ||||
| 		> .actions | ||||
| 			text-align right | ||||
| 			padding-bottom 16px | ||||
| 			margin-bottom 16px | ||||
| 			border-bottom solid 1px var(--faceDivider) | ||||
| 
 | ||||
| 			> * | ||||
| 				margin-left 8px | ||||
| 
 | ||||
| 			> .follow | ||||
| 				width 180px | ||||
| 
 | ||||
| 		> .fields | ||||
| 			margin-top 16px | ||||
| 
 | ||||
							
								
								
									
										57
									
								
								src/client/app/desktop/views/home/user/user.home.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/client/app/desktop/views/home/user/user.home.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| <template> | ||||
| <div class="lnctpgve"> | ||||
| 	<x-integrations :user="user" v-if="user.twitter || user.github || user.discord"/> | ||||
| 	<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> | ||||
| 	<!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>--> | ||||
| 	<div class="activity"> | ||||
| 		<ui-container :body-togglable="true"> | ||||
| 			<template slot="header"><fa icon="chart-bar"/>{{ $t('activity') }}</template> | ||||
| 			<x-activity :user="user" :limit="35" style="padding: 16px;"/> | ||||
| 		</ui-container> | ||||
| 	</div> | ||||
| 	<x-photos :user="user"/> | ||||
| 	<x-timeline class="timeline" ref="tl" :user="user"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import parseAcct from '../../../../../../misc/acct/parse'; | ||||
| import Progress from '../../../../common/scripts/loading'; | ||||
| import XTimeline from './user.timeline.vue'; | ||||
| import XPhotos from './user.photos.vue'; | ||||
| import XIntegrations from './user.integrations.vue'; | ||||
| import XActivity from '../../../../common/views/components/activity.vue'; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
| 	components: { | ||||
| 		XTimeline, | ||||
| 		XPhotos, | ||||
| 		XIntegrations, | ||||
| 		XActivity | ||||
| 	}, | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		warp(date) { | ||||
| 			(this.$refs.tl as any).warp(date); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .lnctpgve | ||||
| 	> * | ||||
| 		margin-bottom 16px | ||||
|  | ||||
| 	> .timeline | ||||
| 		box-shadow var(--shadow) | ||||
|  | ||||
| </style> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user