Compare commits
970 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc4f88b582 | |||
| bb69653349 | |||
| ec2859b350 | |||
| 40251d0ba1 | |||
| 742d10f7ea | |||
| 501b0ea537 | |||
| d2322b611a | |||
| f0184a1b5d | |||
| f1910c0ce9 | |||
| 2d85ee6a08 | |||
| e61228b031 | |||
| 64fdde52f6 | |||
| ca0b730605 | |||
| 4dcca7cf53 | |||
| cbf883ab1e | |||
| edf36d28ca | |||
| c554ced3a4 | |||
| 75a3ab343b | |||
| 617773d287 | |||
| 9ede66fcc0 | |||
| f969dd6ad2 | |||
| 468fe27565 | |||
| 251a1b13a3 | |||
| 94af72fd62 | |||
| e5ca36a68c | |||
| 4d27064db3 | |||
| d4f8934e8a | |||
| b354ad382c | |||
| 863d586664 | |||
| 92e0d885f0 | |||
| 327195dc55 | |||
| e47d3a6d13 | |||
| df82757376 | |||
| 59d2800051 | |||
| eab93c1ff6 | |||
| f89be95e65 | |||
| d69df0e906 | |||
| 5a99c54ebe | |||
| aa88e4cb0a | |||
| 6ba8fcb604 | |||
| 0e1286c454 | |||
| 3eab17330b | |||
| 2e92aaaaa3 | |||
| dfcc91c8c6 | |||
| 2712e47bc1 | |||
| 9421aa1d39 | |||
| d8708f020e | |||
| 5873ee4e6c | |||
| aff6caa4c0 | |||
| 2fe5699b8f | |||
| e43799e3b5 | |||
| 3a16e7401e | |||
| 6441ff45a9 | |||
| ef41494bad | |||
| 6380e6c4c2 | |||
| ff3421c7cb | |||
| 7f61beff7a | |||
| 3cc0b44cb6 | |||
| d8dd408de0 | |||
| 6f1bb99990 | |||
| b69801d33f | |||
| 2d36b2403b | |||
| 5bef62398a | |||
| 5f09326879 | |||
| 8d53d4f0a9 | |||
| 45d8747fdd | |||
| 974663c42d | |||
| f474763151 | |||
| 7c675f1ded | |||
| 751fd5917b | |||
| bdbd0b644e | |||
| 53acda2cc7 | |||
| 50f13701be | |||
| 1f047b3ce3 | |||
| c6a4485a20 | |||
| 7e9273aa91 | |||
| bccbdd4706 | |||
| bd38f8e263 | |||
| a023ee9893 | |||
| 38c9720c65 | |||
| 1189607f2e | |||
| a142a016d9 | |||
| 6ed6395c07 | |||
| eef99fd86b | |||
| 86bdbfe0c0 | |||
| 2258174a00 | |||
| 7cec2616b4 | |||
| 1bd60115ce | |||
| f7e2b48c72 | |||
| 93feda84dc | |||
| 5577fc9a32 | |||
| 9af2ebe485 | |||
| 7e0f13320a | |||
| 70e431f460 | |||
| 24487f9d77 | |||
| 584d3f4c4b | |||
| 61b90473ac | |||
| e7d2921828 | |||
| 68813cf4e3 | |||
| 6576042814 | |||
| 0421d07ca5 | |||
| 7e10674eb9 | |||
| 0df62261f5 | |||
| 28e59decab | |||
| 073f62e2ed | |||
| 86f6bfa04f | |||
| bd57be4b12 | |||
| 7bced9da57 | |||
| 206de567d4 | |||
| f3a6c04f41 | |||
| e0eb8d0342 | |||
| 74db389f6a | |||
| 40bd0b2262 | |||
| 4e2d63b15a | |||
| b37265f185 | |||
| c283924a20 | |||
| 554e3de73b | |||
| 176828a374 | |||
| 1be46da339 | |||
| a072226d92 | |||
| d0f07d47ce | |||
| 27ab013704 | |||
| 603d82dbe8 | |||
| 3b1464e29a | |||
| bd63808767 | |||
| 2d25fbf6a4 | |||
| b301b48571 | |||
| 16aacf55cb | |||
| c14dbe146c | |||
| 8361ea76d0 | |||
| 80cefc20fd | |||
| 2b99c62957 | |||
| ae16d37242 | |||
| 2fc2bc5590 | |||
| 62c5709cc5 | |||
| 22f21e7a83 | |||
| 155a734a52 | |||
| b996677c7e | |||
| bf42249200 | |||
| c161e54e6b | |||
| 2ae7a4712d | |||
| e90c23fde2 | |||
| 47fd319fed | |||
| 390231246f | |||
| 0c7f1475cb | |||
| 868dce8e3a | |||
| 66f45846a6 | |||
| df158da14a | |||
| 25e7b90926 | |||
| b686dec2b1 | |||
| 015f21a1c9 | |||
| 6f07fc7326 | |||
| 13df926362 | |||
| 78c3bb168f | |||
| f0ee4414f3 | |||
| b8f6b79682 | |||
| 7666a13445 | |||
| 858df3737d | |||
| a9e46b7c7f | |||
| 05ba991414 | |||
| 8ba5d3e739 | |||
| a4c7cf3b8a | |||
| f06ddec06b | |||
| 37ece31b94 | |||
| 650898fdc4 | |||
| 3572d5edba | |||
| 017197cd28 | |||
| 0c9de801f7 | |||
| d85d9c26b1 | |||
| 0746682763 | |||
| 1201418bec | |||
| 07adab74ec | |||
| 55a840ba9d | |||
| 0fdaca8cb2 | |||
| 86fc043e54 | |||
| a2541bb4e0 | |||
| 04bb4112c0 | |||
| c8b2bf96be | |||
| 4d2cf83452 | |||
| 12d7e4ea24 | |||
| 3d921c81bd | |||
| e48506844d | |||
| 18fc3a0ad5 | |||
| 75024d8501 | |||
| 7a0b83cdc9 | |||
| 3302ef08ae | |||
| a8c4aec721 | |||
| 9a82e8b097 | |||
| 4714d683d3 | |||
| 137bf5557c | |||
| 606e850aad | |||
| eb47bb8973 | |||
| a32cd0a98c | |||
| cfb55594a5 | |||
| 15b612bad6 | |||
| fab72940c4 | |||
| 2977edc04d | |||
| a256931596 | |||
| 29f3c8a40e | |||
| e2d064131b | |||
| ea6f6ec82c | |||
| 35146e3615 | |||
| bcd32de461 | |||
| 985a70c084 | |||
| 0dfde3e92c | |||
| 361662b6cf | |||
| 494d7f49c9 | |||
| 3ff141a122 | |||
| dba047ce34 | |||
| 931ed3d3c7 | |||
| d29bccb7d6 | |||
| d480429896 | |||
| 28d5a55352 | |||
| a8f9734400 | |||
| 4d84fd4cf7 | |||
| 8861835580 | |||
| 8c9821e444 | |||
| f15465f0d3 | |||
| 5e18f1ceeb | |||
| 5385797ff1 | |||
| 7d377b8e88 | |||
| 67bc9e5ba3 | |||
| 83a3b37f1f | |||
| 5f9e3709b3 | |||
| 3dac486688 | |||
| 88d04f0af7 | |||
| 617cd7d1f8 | |||
| 677e5325fa | |||
| 101f15a078 | |||
| 370f3b14fa | |||
| 6e641a99a2 | |||
| 716320de35 | |||
| 06343addef | |||
| eb69fc33f6 | |||
| 9e13e5d164 | |||
| 794906f01e | |||
| 263ef17816 | |||
| 7025cb8bb0 | |||
| 821460075a | |||
| b87637c611 | |||
| e10588430f | |||
| dda1a2f75c | |||
| cc35202911 | |||
| 9827c0e81e | |||
| f481cdd00b | |||
| f0e180693e | |||
| 9f9c5e01f1 | |||
| c71405ccad | |||
| 231a4af754 | |||
| 398b2624c8 | |||
| 4f0eabbd55 | |||
| fb2fc686b1 | |||
| 9923ff587b | |||
| 261a9da374 | |||
| ca8689fc88 | |||
| 15ec862da0 | |||
| ba4656312a | |||
| a6136f6cb2 | |||
| 9217ca8476 | |||
| ab7001ed20 | |||
| 023fcb506f | |||
| a66d40e0e8 | |||
| a982af771b | |||
| 92fac1cd9f | |||
| dea803468f | |||
| d1af4bdd51 | |||
| cf33b3295f | |||
| bacb6c8fb3 | |||
| 3d18dd7e47 | |||
| d2c82a04d9 | |||
| 6e8c7460a2 | |||
| e18e179a59 | |||
| d5d464a289 | |||
| c1a8dc34da | |||
| cea08d41ee | |||
| 3806f579b9 | |||
| 13048fce76 | |||
| a0ba907f6d | |||
| 35dfb09e6f | |||
| 8c9356b9fd | |||
| 52644ec5cb | |||
| a0305e90f0 | |||
| 1ad17fc2bf | |||
| adc6b86e6b | |||
| 36309ebe04 | |||
| 38d1074992 | |||
| 9f764a4217 | |||
| ef1c3d57b6 | |||
| 3019dd4801 | |||
| ed42c476fb | |||
| 0d85492d57 | |||
| dbf045c842 | |||
| f0b8468d45 | |||
| c768641f09 | |||
| 8f1aaf617a | |||
| a9f8ec5453 | |||
| c66a3e0206 | |||
| 58ca9b28fa | |||
| d8a52f9cc4 | |||
| f6cf509a04 | |||
| a731f046b3 | |||
| c65895b328 | |||
| 3a1d6a1970 | |||
| 170a406cad | |||
| 15229d27b4 | |||
| 9b5fe24ca4 | |||
| 0aa334515b | |||
| 1418054b53 | |||
| a63aeccbcc | |||
| 58b8217283 | |||
| 152302cbff | |||
| c3c120e91d | |||
| 1feff4dcc7 | |||
| 48c739027c | |||
| 68a2c5db98 | |||
| 9725443e60 | |||
| 3b6d72ef3b | |||
| 069cf3fa87 | |||
| 79ee77a6fe | |||
| a88abc7ee3 | |||
| 396349fdf2 | |||
| 62fcc16546 | |||
| dfc166c1ff | |||
| 48f97ebf40 | |||
| 3287ce9453 | |||
| d137d5800a | |||
| 29b084b4b8 | |||
| 6e0ceda968 | |||
| a35daceb5b | |||
| a4b6a97b1b | |||
| df487e3980 | |||
| 2c876f455a | |||
| c93adf2e9a | |||
| 38ea47df89 | |||
| c83657a072 | |||
| a45a05fbb2 | |||
| 8d93e9c13c | |||
| edbbbaad48 | |||
| ce8722b0e6 | |||
| 54a40bfa01 | |||
| cc1e3c6792 | |||
| fb70ea3b95 | |||
| 4a956fa18d | |||
| 45614b8804 | |||
| f57171d7b9 | |||
| 3633ea66d4 | |||
| 093c85d006 | |||
| 1d36ea5ec5 | |||
| d39de30221 | |||
| a5e861cd01 | |||
| 96a24ec625 | |||
| 91f833d1f0 | |||
| 4d720974c3 | |||
| 23c5627639 | |||
| 5775abb25c | |||
| e5be00dae1 | |||
| cc84485650 | |||
| 4e4c96dfaf | |||
| f0f56bf148 | |||
| 49db16318b | |||
| 079e289260 | |||
| a96367139c | |||
| 1860bb354f | |||
| deece57584 | |||
| 318c62c852 | |||
| 2a6f42fef3 | |||
| f2b75a075d | |||
| cd021bc6df | |||
| d4bd64a5f5 | |||
| ed80ecfdbd | |||
| 491449c418 | |||
| 33d92f9b16 | |||
| 7f513fe46f | |||
| 58a975e8df | |||
| 076c52788b | |||
| 80b1ccb267 | |||
| 92b04ba7df | |||
| e388dbc168 | |||
| 895eda3714 | |||
| be8098e8ec | |||
| 06f58f5349 | |||
| 85c515b395 | |||
| 631b8b93a4 | |||
| fb63e81ed1 | |||
| a4ea0a30bf | |||
| aef6d52951 | |||
| d87a1fda59 | |||
| 9ae8ff0098 | |||
| bb29218a6c | |||
| b25c0c6f02 | |||
| 28556f7c27 | |||
| 4b050c7fa5 | |||
| 3d37b9d8e1 | |||
| 1041a38f14 | |||
| 0bb69d7fe0 | |||
| d175e86901 | |||
| b37932fdf4 | |||
| 08ad2923a5 | |||
| e5eca8a554 | |||
| 0db756b4a5 | |||
| f4447eb3a0 | |||
| 2e10c1b0a3 | |||
| e744775ce8 | |||
| 647b8e5f6b | |||
| 4f617a7293 | |||
| df18a74adc | |||
| 2f1f1455aa | |||
| e5f0f95356 | |||
| 21f7cb39a8 | |||
| 5d203e93ae | |||
| fe8b5b62ae | |||
| f27226b55c | |||
| 666498e7b7 | |||
| aa07b219c9 | |||
| 3f65c8b1d6 | |||
| 62572c3204 | |||
| c2a4051d72 | |||
| 3f9d9dd6bd | |||
| 3e1b40ce29 | |||
| 169d13680a | |||
| 0f28c28760 | |||
| d9f8091ce6 | |||
| 5b664f464d | |||
| 7426417a52 | |||
| a035fa4e6c | |||
| 4c2301bc9f | |||
| 77505fa7c9 | |||
| 2df48b1643 | |||
| ce9fd04865 | |||
| 684650e14f | |||
| 0ff34f8a5a | |||
| 4750d9bb4c | |||
| 4d15cbcbbd | |||
| 3b02566e16 | |||
| 5b47856329 | |||
| 282e6812b3 | |||
| 6109fab14e | |||
| 87311cff09 | |||
| 6951fda0d6 | |||
| 2d99cbc640 | |||
| 22c70ae22a | |||
| c71cf72eea | |||
| 51c996bd7e | |||
| 4007ddbf32 | |||
| 6b67c095bf | |||
| b8b7ae8564 | |||
| b5ded67c06 | |||
| d3d219f15d | |||
| 8d623ab1ea | |||
| ab7490e49b | |||
| d8324dd80b | |||
| caacaf238c | |||
| e4b8aaece6 | |||
| bfc7b6af8f | |||
| fce9c5eeb2 | |||
| 6de87e8b65 | |||
| 041c72b07c | |||
| 741a59e0cc | |||
| 4ddb6189dc | |||
| 0e56ac1c2b | |||
| 853f5145be | |||
| b213d25711 | |||
| bd77f3a1a6 | |||
| 78817e37f7 | |||
| 2b0dd2cbae | |||
| 6a319154d9 | |||
| 4a068483ed | |||
| 9e5037c715 | |||
| 835feea163 | |||
| 52649bdabc | |||
| 0a606c2720 | |||
| cfa8edf2c0 | |||
| e7ac0e5d68 | |||
| 71863061c6 | |||
| 0e83ced25b | |||
| a613447105 | |||
| a3233e31d0 | |||
| 8a55f21a98 | |||
| c7d8cff7b3 | |||
| f71f101fce | |||
| afdc61b9b7 | |||
| 1d1ea7e703 | |||
| 052ad2fe3f | |||
| a0099ecb66 | |||
| 9940739f1f | |||
| afbe1a96ac | |||
| e029c2864f | |||
| dadf2f407f | |||
| 6751c22a23 | |||
| 1943991077 | |||
| 115170f35d | |||
| c3690b456e | |||
| 70593e71e6 | |||
| 1c60609547 | |||
| b84acfd7b7 | |||
| 97e072d93a | |||
| 2179054384 | |||
| a3f48fc3f4 | |||
| de2f968645 | |||
| e1483488c7 | |||
| a1822f073d | |||
| 218b15b5fd | |||
| bc029b0fa2 | |||
| 2d7f242713 | |||
| 0afc955ebd | |||
| e80e53d9d2 | |||
| afcfcce2e6 | |||
| 3171241c6f | |||
| 3e0bb91ff2 | |||
| 5bbc0e0bb5 | |||
| 3799983d4f | |||
| 3f04ebd89a | |||
| b93e5437bd | |||
| 64d56e6515 | |||
| d9d4d8954e | |||
| b132581b3a | |||
| f20ae34400 | |||
| dc8bef7928 | |||
| 650e69c336 | |||
| f21dc21a83 | |||
| 9afbb12f95 | |||
| 7b10e47b21 | |||
| 4b630c3c36 | |||
| bdd240a230 | |||
| c57af7e242 | |||
| f706234d77 | |||
| 805615d52b | |||
| 8424b772b0 | |||
| 115f38c422 | |||
| 48fd8a66ad | |||
| 53cde52027 | |||
| 75f6506bc7 | |||
| 1d77063a4b | |||
| ccd7378347 | |||
| 8e711e0587 | |||
| 87d420a92b | |||
| d6bbccdd71 | |||
| 913749739f | |||
| 0b7a8dca15 | |||
| dd4672dc9a | |||
| 9ac7046521 | |||
| e4c804fac0 | |||
| fd77c583bf | |||
| b319c0c72b | |||
| c5551e834b | |||
| e58422889b | |||
| b7755314b1 | |||
| 3fb647b34b | |||
| 0eb9c019e4 | |||
| aa0b2e0723 | |||
| 4539feed40 | |||
| 20c14a1d99 | |||
| 9793002070 | |||
| 01d8fa4e54 | |||
| 961ca3a71b | |||
| 08811e5a27 | |||
| a6fae395da | |||
| d690b88c1c | |||
| 322ec8681a | |||
| e1bfa6fbd3 | |||
| db0e1a2534 | |||
| d524e98348 | |||
| 7afa6c9f40 | |||
| 538903f9d8 | |||
| c5a6f40dff | |||
| b817e09ee8 | |||
| 6b5791fda6 | |||
| 3250e59266 | |||
| e5ae0671ce | |||
| 9d7a7e2019 | |||
| c2cf13fc00 | |||
| c3546ea856 | |||
| edb66ecade | |||
| c6a4a0a320 | |||
| 6a31962ca0 | |||
| fa99abf106 | |||
| 9be06d9f71 | |||
| 966919874e | |||
| a253c95170 | |||
| c6e0dcf08e | |||
| 0147226b3d | |||
| 1571053fba | |||
| 623aa3d20d | |||
| 636a55e72d | |||
| 304f871332 | |||
| 2cd8a3ec8d | |||
| 971cec024c | |||
| 7a17eb7fec | |||
| 4cb14c257c | |||
| 1187727b60 | |||
| 54fd7e2be6 | |||
| 0ef58696bf | |||
| 9478a462a7 | |||
| 4e2fd7baf9 | |||
| 66fb3987d6 | |||
| 1e60a491c4 | |||
| 12ce58a5a0 | |||
| 2f24f3312d | |||
| 1cc35b6df8 | |||
| 4f2be206df | |||
| 6074ad67ab | |||
| c5cb76ac3b | |||
| e4b010321d | |||
| 5e4ff5de7c | |||
| 6b14b645be | |||
| 79258f61be | |||
| 3ea442667f | |||
| 6c120cc576 | |||
| 89d6e624f2 | |||
| 060aa41f35 | |||
| 67f5cf3c03 | |||
| 5948d20f00 | |||
| b3ed29ff02 | |||
| 5d973df5bd | |||
| 26bfbdc2ad | |||
| 50ea6dd142 | |||
| 6f1d953642 | |||
| 86f1b88d79 | |||
| 4610b6e547 | |||
| be02516776 | |||
| 4e100b9ea2 | |||
| 8ae1f7e192 | |||
| fea0c91f74 | |||
| 051d51bcd9 | |||
| 5718c6491e | |||
| a97c07bfdf | |||
| d815f984fb | |||
| 4993dc37e2 | |||
| 0671aa0dd0 | |||
| b62653c202 | |||
| be4244acde | |||
| 8311d4deba | |||
| e51144809f | |||
| cd4ad2df11 | |||
| b632d740c1 | |||
| 123de8aad0 | |||
| 186078ae2a | |||
| 23c80794d5 | |||
| 3e1415ef89 | |||
| 2439f55342 | |||
| b0babee98a | |||
| 47bb08514b | |||
| 60eb164bdc | |||
| 3101456a85 | |||
| 5e851e643b | |||
| bfa62db274 | |||
| b349bcd427 | |||
| 7e6e6292f8 | |||
| 34592fa63a | |||
| 71c0b59866 | |||
| a97db1efd6 | |||
| e34d71fc1f | |||
| 89efb0d2f4 | |||
| 1b204012ea | |||
| b67a557a8c | |||
| 5fb302d0f1 | |||
| 7e21853cca | |||
| 7dd1a0dd30 | |||
| 90b066a744 | |||
| 1128cc463c | |||
| 22d8961c5e | |||
| 76727cd39c | |||
| 5829cd98af | |||
| 49fa9c47e9 | |||
| 495960c83a | |||
| 3b4afdf567 | |||
| a485ebc2bb | |||
| 6980e4ddf1 | |||
| cb19db1006 | |||
| ddf6c9cef0 | |||
| 9f3a983fef | |||
| 839627ffc4 | |||
| f3269cdc10 | |||
| 3502d374e3 | |||
| 0190a36070 | |||
| 23a6b86ef3 | |||
| 29cd8fbd3b | |||
| a36673a6a8 | |||
| 544db06294 | |||
| d9030b4fdd | |||
| a7570f5eb2 | |||
| f96e5882d1 | |||
| f16f35a4d4 | |||
| 51b14cc615 | |||
| 80bd6433aa | |||
| 50b3a30d1a | |||
| 5930b667a1 | |||
| 9b5ae4d4a3 | |||
| 22c8ad4583 | |||
| 726fdbea15 | |||
| 67c2b52682 | |||
| 238e557d2b | |||
| c8d87e7ef9 | |||
| a83b07a629 | |||
| 3bd2f3c36d | |||
| 465c5f194c | |||
| ec3df8e5f2 | |||
| c194568daf | |||
| 63039aea37 | |||
| fb803b2ff2 | |||
| fabac8524d | |||
| 35ad2a1957 | |||
| e4d40fbba4 | |||
| d4076e5df5 | |||
| 2ced27e734 | |||
| 04d4f5525c | |||
| 949f11ea1e | |||
| 00a5dfc36f | |||
| e29f33ed0c | |||
| 6946d09f55 | |||
| 450145dd6b | |||
| 551b8f3690 | |||
| 48178bdc53 | |||
| a511250b63 | |||
| 415a823af0 | |||
| 0e323ef248 | |||
| d157f67381 | |||
| fa20ea76b1 | |||
| e7f5033c7d | |||
| 9432fcec7d | |||
| 5768806d1b | |||
| f8c5cbcd0d | |||
| 20880cdf0b | |||
| cc5cff2038 | |||
| 2a510205c3 | |||
| 0db5a5a581 | |||
| ba858a894c | |||
| 654996fdbe | |||
| cebb4224ac | |||
| 9b27ac9aaf | |||
| 863255d52f | |||
| c4bd004cbc | |||
| 244174a32b | |||
| 22bdcda9c0 | |||
| ba8598858b | |||
| 17863f54fe | |||
| 26670e9003 | |||
| 10cd03c718 | |||
| f1db5e8f4b | |||
| d78c8e8ea4 | |||
| e560fbc935 | |||
| 4adffb4835 | |||
| 0e4a7c3d05 | |||
| 8780246ce5 | |||
| 61bb69c88f | |||
| bdf631c2c4 | |||
| 3addc36c96 | |||
| f851bc92c9 | |||
| 2e54cf12c7 | |||
| ff5f69b8fd | |||
| d7da9f80a7 | |||
| 6b990ba368 | |||
| cd3e6d0073 | |||
| 0aaef50ee5 | |||
| 05b2351e08 | |||
| d9a9f97751 | |||
| 8c0deb905e | |||
| d15d24c11c | |||
| 31c4300456 | |||
| 84a3cd92a3 | |||
| ace1f5067c | |||
| cd0f6a4f78 | |||
| 414ee55957 | |||
| 0f2fd8a352 | |||
| 0582f19e7c | |||
| 7e1e8ea429 | |||
| 7a8c975f61 | |||
| 1e41c2b99e | |||
| f0f1066bc8 | |||
| 2b7f12613e | |||
| fe0ed7e8f0 | |||
| 514d7d3f88 | |||
| 77b55a559b | |||
| 77bb0b5530 | |||
| 39909c8a85 | |||
| f4b36a9ebf | |||
| 67319d0e5b | |||
| cf58df17f6 | |||
| 8bb97fbfeb | |||
| 3a5ad18aca | |||
| e3d602fdcc | |||
| c07c0b2260 | |||
| f626da838a | |||
| 9623b0e140 | |||
| b1b9260a1d | |||
| 9c1814d122 | |||
| 0300db6c63 | |||
| c7690aecd0 | |||
| 1b32bb9c51 | |||
| 9bc7d99e16 | |||
| 965bc5573f | |||
| 2559d03d7e | |||
| 58d0f9678b | |||
| ddee8bb686 | |||
| 56616787ec | |||
| d3659b5597 | |||
| 373c30be69 | |||
| 7b60adb480 | |||
| 5864dc52f7 | |||
| a31ff20f50 | |||
| 2a97bdb39d | |||
| 86e3aefdab | |||
| a8d1987686 | |||
| 0d073d607c | |||
| 769a9a14fe | |||
| a61f6e1590 | |||
| ee86f56469 | |||
| 17d6f1b53b | |||
| befd4d5fc7 | |||
| 4c7edfc9a9 | |||
| d361a4d7dc | |||
| 571e73a346 | |||
| 39ecb33883 | |||
| 0f01931309 | |||
| a9830ff491 | |||
| 9a6363431d | |||
| ea6114f63c | |||
| bfb3a4364b | |||
| 6d3229b1a1 | |||
| 06042569f1 | |||
| 8ade11783a | |||
| a5e20a4eb2 | |||
| dd75e43c57 | |||
| 0e6af68a0f | |||
| f886135bb7 | |||
| c3f1765b21 | |||
| 7cc0d0763c | |||
| c8983d5606 | |||
| 0c60b31eee | |||
| aaf0b985ad | |||
| 9fde13c968 | |||
| 8896afd8d8 | |||
| 95007059d1 | |||
| 1d4b1b296e | |||
| 76547fe66d | |||
| 41991bcac2 | |||
| 1bc05e87e4 | |||
| f065fe98b2 | |||
| aecc59cdc8 | |||
| d1f02221cb | |||
| efd558f394 | |||
| 28ee5721bb | |||
| 94c37e8adf | |||
| e5f731c97d | |||
| 3cf7f9e3c8 | |||
| 56de3d2f52 | |||
| c551e3e697 | |||
| f7e2ac1c48 | |||
| 2f8aae371b | |||
| 9ea0f10abb | |||
| ecefbac4ec | |||
| 182fcca5da | |||
| 6509542dbd | |||
| 2ac78219ee | |||
| af2a408b78 | |||
| dfb376cbd4 | |||
| 1e39dac5ea | |||
| e55f1c58df | |||
| f150d0a105 | |||
| fd9f78a7a1 | |||
| 238ebcf1e4 | |||
| b141b04a53 | |||
| d2730d5bad | |||
| 1c75c74c91 | |||
| ef684dff61 | |||
| 241b4957e1 | |||
| 45eda03d1c | |||
| 1d0e4bada8 | |||
| 3c8ced53b9 | |||
| 80dd6b2500 | |||
| ad64b91d66 | |||
| 4988268f5f | |||
| 50adf0ddf2 | |||
| e5db1a7e55 | |||
| 49fe334186 | |||
| 14ec7d6a41 | |||
| 7cc19ef2ea | |||
| 0507eb6550 | |||
| 830a03a0d1 | |||
| 34d265467a | |||
| f5823a96e9 | |||
| 777f6c0162 | |||
| 299c00cf74 | |||
| 07c12ae162 | |||
| c8a7b6f433 | |||
| 5431d8fe55 | |||
| 6aa6f6f300 | |||
| 17d2eed06a | |||
| feb7090cde | |||
| 9cea1bc08c | |||
| d49434a799 | |||
| 214d85189c | |||
| b67db47c88 | |||
| dd3fe61cf3 | |||
| a2f21f4e13 | |||
| 628b99d117 | |||
| 5118eee19a | |||
| 4ba8d95a10 | |||
| 4016182b89 | |||
| 6b8b9c017f | |||
| 44b741e270 | |||
| 8bab8658e8 | |||
| bfe31e20ea | |||
| 5c2744b426 | |||
| f35c090caa | |||
| c1293c3afa | |||
| f15599e6e5 | |||
| 90345f158f | |||
| e654fead23 | |||
| 9c4957268d | |||
| 6689fed513 | |||
| 4ecbb58086 | |||
| 0a3ce9cc8b | |||
| dcfd178314 | |||
| 1b26c713ef | |||
| 9e9ab5cec9 | |||
| a96a62929d | |||
| a3c703bd37 | |||
| b87a9d6675 | |||
| 28ac9d83c1 | |||
| 6d47156297 | |||
| cda1819d81 | |||
| dd6fb78913 | |||
| 350d0b0888 | |||
| 8a9115b58e | |||
| b6e8c12dbc | |||
| e73cb423b6 | |||
| e47d5ba53b | |||
| 8d46fd78c7 | |||
| 95e74319e1 | |||
| 709b75198d | |||
| caed89f0ae | |||
| 72956e2343 | |||
| 4b18e0f36e | |||
| b479d80366 | |||
| 9e8513b312 | |||
| 52835cf8bf | |||
| 905b9771ec | |||
| fca885e665 | |||
| 6e687c0663 | |||
| 180da297f6 | |||
| b774472fff | |||
| 76a2e6befb | |||
| 1f5f612163 | |||
| 509ec99574 | |||
| ced9c0fa7e | |||
| 33777fab47 | |||
| b0789fd6fd | |||
| 433ea02a18 | |||
| 50d5ef7416 | |||
| c7c4e2e03e | |||
| d1bffd7659 | |||
| 7ed1fe70d1 | |||
| 87ae0eaa15 | |||
| edb5826072 | |||
| 7e1b1ec990 | |||
| 804e25084d | |||
| e21dd584a9 | |||
| 3f8845bdad | |||
| 098991d767 | |||
| d44d8f1989 | |||
| 763d2c0ce4 | |||
| 66c44b4260 | |||
| 6f621dd34c | |||
| 954d5c05df | |||
| 77bd79100b | |||
| f99240df69 | |||
| 78db835fe1 | |||
| b4ca079bdb | |||
| 10153e692e |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env", "@vue/babel-preset-jsx"],
|
"presets": ["@babel/preset-env"],
|
||||||
"plugins": ["@babel/plugin-transform-runtime", "lodash"],
|
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||||
"comments": false
|
"comments": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ module.exports = {
|
|||||||
'generator-star-spacing': 0,
|
'generator-star-spacing': 0,
|
||||||
// allow debugger during development
|
// allow debugger during development
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||||
'vue/require-prop-types': 0
|
'vue/require-prop-types': 0,
|
||||||
|
'vue/no-unused-vars': 0,
|
||||||
|
'no-tabs': 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ test/e2e/reports
|
|||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
.idea/
|
.idea/
|
||||||
config/local.json
|
config/local.json
|
||||||
|
config/local.*.json
|
||||||
|
docs/site/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# This file is a template, and might need editing before it works on your project.
|
# This file is a template, and might need editing before it works on your project.
|
||||||
# Official framework image. Look for the different tagged releases at:
|
# Official framework image. Look for the different tagged releases at:
|
||||||
# https://hub.docker.com/r/library/node/tags/
|
# https://hub.docker.com/r/library/node/tags/
|
||||||
image: node:10
|
image: node:12
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- lint
|
- lint
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
pipeline:
|
||||||
|
lint:
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- pull_request
|
||||||
|
image: node:16
|
||||||
|
commands:
|
||||||
|
- yarn
|
||||||
|
- yarn lint
|
||||||
|
- yarn stylelint
|
||||||
|
|
||||||
|
test:
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- pull_request
|
||||||
|
image: node:16
|
||||||
|
commands:
|
||||||
|
- apt update
|
||||||
|
- apt install firefox-esr -y --no-install-recommends
|
||||||
|
- yarn
|
||||||
|
- yarn unit
|
||||||
|
|
||||||
|
build:
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
branch:
|
||||||
|
- develop
|
||||||
|
- stable
|
||||||
|
image: node:16
|
||||||
|
commands:
|
||||||
|
- yarn
|
||||||
|
- yarn build
|
||||||
|
|
||||||
|
release:
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
branch:
|
||||||
|
- develop
|
||||||
|
- stable
|
||||||
|
image: node:16
|
||||||
|
secrets:
|
||||||
|
- SCW_ACCESS_KEY
|
||||||
|
- SCW_SECRET_KEY
|
||||||
|
- SCW_DEFAULT_ORGANIZATION_ID
|
||||||
|
commands:
|
||||||
|
- apt-get update && apt-get install -y rclone wget zip
|
||||||
|
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||||
|
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||||
|
- chmod +x scaleway-cli
|
||||||
|
- ./scaleway-cli object config install type=rclone
|
||||||
|
- zip akkoma-fe.zip -r dist
|
||||||
|
- rclone copyto akkoma-fe.zip scaleway:akkoma-updates/frontend/$CI_COMMIT_BRANCH/akkoma-fe.zip
|
||||||
|
|
||||||
|
docs:
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
branch:
|
||||||
|
- develop
|
||||||
|
- stable
|
||||||
|
environment:
|
||||||
|
CI: "true"
|
||||||
|
image: python:3.10-slim
|
||||||
|
secrets:
|
||||||
|
- SCW_ACCESS_KEY
|
||||||
|
- SCW_SECRET_KEY
|
||||||
|
- SCW_DEFAULT_ORGANIZATION_ID
|
||||||
|
commands:
|
||||||
|
- apt-get update && apt-get install -y rclone wget git zip
|
||||||
|
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||||
|
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||||
|
- chmod +x scaleway-cli
|
||||||
|
- ./scaleway-cli object config install type=rclone
|
||||||
|
- cd docs
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- mkdocs build
|
||||||
|
- zip -r docs.zip site/*
|
||||||
|
- cd site
|
||||||
|
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/
|
||||||
@@ -3,6 +3,43 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
### Fixed
|
||||||
|
- AdminFE button no longer scrolls page to top when clicked
|
||||||
|
- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough)
|
||||||
|
- Fixed many many bugs related to new mentions, including spacing and alignment issues
|
||||||
|
- Links in profile bios now properly open in new tabs
|
||||||
|
- Inline images now respect their intended width/height attributes
|
||||||
|
- Links with `&` in them work properly now
|
||||||
|
- Interaction list popovers now properly emojify names
|
||||||
|
- Completely hidden posts still had 1px border
|
||||||
|
- Attachments are ALWAYS in same order as user uploaded, no more "videos first"
|
||||||
|
- Attachment description is prefilled with backend-provided default when uploading
|
||||||
|
- Proper visual feedback that next image is loading when browsing
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
|
||||||
|
- User highlight background now also covers the `@`
|
||||||
|
- Reverted back to textual `@`, svg version is opt-in.
|
||||||
|
- Settings window has been throughly rearranged to make make more sense and make navication settings easier.
|
||||||
|
- Uploaded attachments are uniform with displayed attachments
|
||||||
|
- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
|
||||||
|
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Options to show domains in mentions
|
||||||
|
- Option to show user avatars in mention links (opt-in)
|
||||||
|
- Option to disable the tooltip for mentions
|
||||||
|
- Option to completely hide muted threads
|
||||||
|
- Ability to open videos in modal even if you disabled that feature, via an icon button
|
||||||
|
- New button on attachment that indicates that attachment has a description and shows a bar filled with description
|
||||||
|
- Attachments are truncated just like post contents
|
||||||
|
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
|
||||||
|
- Ability to rearrange order of attachments when uploading
|
||||||
|
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
||||||
|
- Added frontend ui for account migration
|
||||||
|
|
||||||
|
|
||||||
## [2.4.2] - 2022-01-09
|
## [2.4.2] - 2022-01-09
|
||||||
### Added
|
### Added
|
||||||
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
|
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
# Pleroma-FE
|
# Hynetsoc-FE
|
||||||
|
|
||||||
> A single column frontend designed for Pleroma.
|
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as:
|
||||||
|
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
|
||||||
|
- Custom emoji reactions
|
||||||
|
|
||||||

|
And specificly, this is a tweaked version with all the hyena.network themes and tweaks in place
|
||||||
|
|
||||||
# For Translators
|
# For Translators
|
||||||
|
|
||||||
To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Pleroma-FE.
|
||||||
|
|
||||||
|
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
|
||||||
|
|
||||||
|
Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||||
|
|
||||||
# FOR ADMINS
|
# FOR ADMINS
|
||||||
|
|
||||||
You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box.
|
To use Pleroma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Pleroma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
|
||||||
|
|
||||||
## Build Setup
|
## Build Setup
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ var utils = require('./utils')
|
|||||||
var projectRoot = path.resolve(__dirname, '../')
|
var projectRoot = path.resolve(__dirname, '../')
|
||||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||||
var CopyPlugin = require('copy-webpack-plugin');
|
var CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
var { VueLoaderPlugin } = require('vue-loader')
|
||||||
|
|
||||||
var env = process.env.NODE_ENV
|
var env = process.env.NODE_ENV
|
||||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||||
@@ -29,16 +30,16 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.vue'],
|
extensions: ['.js', '.jsx', '.vue', '.mjs'],
|
||||||
modules: [
|
modules: [
|
||||||
path.join(__dirname, '../node_modules')
|
path.join(__dirname, '../node_modules')
|
||||||
],
|
],
|
||||||
alias: {
|
alias: {
|
||||||
'vue$': 'vue/dist/vue.runtime.common',
|
|
||||||
'static': path.resolve(__dirname, '../static'),
|
'static': path.resolve(__dirname, '../static'),
|
||||||
'src': path.resolve(__dirname, '../src'),
|
'src': path.resolve(__dirname, '../src'),
|
||||||
'assets': path.resolve(__dirname, '../src/assets'),
|
'assets': path.resolve(__dirname, '../src/assets'),
|
||||||
'components': path.resolve(__dirname, '../src/components')
|
'components': path.resolve(__dirname, '../src/components'),
|
||||||
|
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@@ -58,9 +59,33 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
enforce: 'post',
|
||||||
|
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||||
|
type: 'javascript/auto',
|
||||||
|
loader: '@intlify/vue-i18n-loader',
|
||||||
|
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||||
|
path.resolve(__dirname, '../src/i18n')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.mjs$/,
|
||||||
|
include: /node_modules/,
|
||||||
|
type: "javascript/auto"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
use: 'vue-loader'
|
loader: 'vue-loader',
|
||||||
|
options: {
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement(tag) {
|
||||||
|
if (tag === 'pinch-zoom') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
@@ -95,6 +120,7 @@ module.exports = {
|
|||||||
entry: path.join(__dirname, '..', 'src/sw.js'),
|
entry: path.join(__dirname, '..', 'src/sw.js'),
|
||||||
filename: 'sw-pleroma.js'
|
filename: 'sw-pleroma.js'
|
||||||
}),
|
}),
|
||||||
|
new VueLoaderPlugin(),
|
||||||
// This copies Ruffle's WASM to a directory so that JS side can access it
|
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ module.exports = merge(baseWebpackConfig, {
|
|||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': config.dev.env,
|
'process.env': config.dev.env,
|
||||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
|
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
||||||
|
'__VUE_OPTIONS_API__': true,
|
||||||
|
'__VUE_PROD_DEVTOOLS__': false
|
||||||
}),
|
}),
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': env,
|
'process.env': env,
|
||||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||||
'DEV_OVERRIDES': JSON.stringify(undefined)
|
'DEV_OVERRIDES': JSON.stringify(undefined),
|
||||||
|
'__VUE_OPTIONS_API__': true,
|
||||||
|
'__VUE_PROD_DEVTOOLS__': false
|
||||||
}),
|
}),
|
||||||
// extract css into its own file
|
// extract css into its own file
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
|
|||||||
@@ -52,13 +52,26 @@ module.exports = {
|
|||||||
target,
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
cookieDomainRewrite: 'localhost',
|
cookieDomainRewrite: 'localhost',
|
||||||
ws: true
|
ws: true,
|
||||||
|
headers: {
|
||||||
|
'Origin': target
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'/oauth/revoke': {
|
'/oauth/revoke': {
|
||||||
target,
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
cookieDomainRewrite: 'localhost'
|
cookieDomainRewrite: 'localhost'
|
||||||
}
|
},
|
||||||
|
'/static/stickers.json': {
|
||||||
|
target,
|
||||||
|
changeOrigin: true,
|
||||||
|
cookieDomainRewrite: 'localhost'
|
||||||
|
},
|
||||||
|
'/static/stickers': {
|
||||||
|
target,
|
||||||
|
changeOrigin: true,
|
||||||
|
cookieDomainRewrite: 'localhost'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||||
// with this option, according to the CSS-Loader README
|
// with this option, according to the CSS-Loader README
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
all: install
|
||||||
|
pipenv run mkdocs build
|
||||||
|
|
||||||
|
install:
|
||||||
|
pipenv install
|
||||||
|
clean:
|
||||||
|
rm -rf site
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
mkdocs-material = "*"
|
||||||
|
markdown-include = "*"
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "926d34630c729228bb015cb958c04f8269c57f5ca1ffc2ceab1dfd1798884772"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
|
||||||
|
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==8.1.3"
|
||||||
|
},
|
||||||
|
"ghp-import": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619",
|
||||||
|
"sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"
|
||||||
|
],
|
||||||
|
"version": "==2.1.0"
|
||||||
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670",
|
||||||
|
"sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==4.12.0"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
|
||||||
|
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.1.2"
|
||||||
|
},
|
||||||
|
"markdown": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874",
|
||||||
|
"sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.3.7"
|
||||||
|
},
|
||||||
|
"markdown-include": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
|
||||||
|
"sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
|
||||||
|
"sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
|
||||||
|
"sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
|
||||||
|
"sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
|
||||||
|
"sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
|
||||||
|
"sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
|
||||||
|
"sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
|
||||||
|
"sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
|
||||||
|
"sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
|
||||||
|
"sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
|
||||||
|
"sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
|
||||||
|
"sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
|
||||||
|
"sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
|
||||||
|
"sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
|
||||||
|
"sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
|
||||||
|
"sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
|
||||||
|
"sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
|
||||||
|
"sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
|
||||||
|
"sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
|
||||||
|
"sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
|
||||||
|
"sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
|
||||||
|
"sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
|
||||||
|
"sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
|
||||||
|
"sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
|
||||||
|
"sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
|
||||||
|
"sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
|
||||||
|
"sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
|
||||||
|
"sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
|
||||||
|
"sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
|
||||||
|
"sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
|
||||||
|
"sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
|
||||||
|
"sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
|
||||||
|
"sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
|
||||||
|
"sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
|
||||||
|
"sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
|
||||||
|
"sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
|
||||||
|
"sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
|
||||||
|
"sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
|
||||||
|
"sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.1.1"
|
||||||
|
},
|
||||||
|
"mergedeep": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8",
|
||||||
|
"sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.3.4"
|
||||||
|
},
|
||||||
|
"mkdocs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde",
|
||||||
|
"sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.3.0"
|
||||||
|
},
|
||||||
|
"mkdocs-material": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:263f2721f3abe533b61f7c8bed435a0462620912742c919821ac2d698b4bfe67",
|
||||||
|
"sha256:dc82b667d2a83f0de581b46a6d0949732ab77e7638b87ea35b770b33bc02e75a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==8.3.9"
|
||||||
|
},
|
||||||
|
"mkdocs-material-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44",
|
||||||
|
"sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.0.3"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||||
|
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==21.3"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb",
|
||||||
|
"sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==2.12.0"
|
||||||
|
},
|
||||||
|
"pymdown-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0",
|
||||||
|
"sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==9.5"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
|
||||||
|
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.8'",
|
||||||
|
"version": "==3.0.9"
|
||||||
|
},
|
||||||
|
"python-dateutil": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
||||||
|
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.8.2"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
|
||||||
|
"sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
|
||||||
|
"sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
|
||||||
|
"sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
|
||||||
|
"sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
|
||||||
|
"sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
|
||||||
|
"sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
|
||||||
|
"sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
|
||||||
|
"sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
|
||||||
|
"sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
|
||||||
|
"sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
|
||||||
|
"sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
|
||||||
|
"sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
|
||||||
|
"sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
|
||||||
|
"sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
|
||||||
|
"sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
|
||||||
|
"sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
|
||||||
|
"sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
|
||||||
|
"sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
|
||||||
|
"sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
|
||||||
|
"sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
|
||||||
|
"sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
|
||||||
|
"sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
|
||||||
|
"sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
|
||||||
|
"sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
|
||||||
|
"sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
|
||||||
|
"sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
|
||||||
|
"sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
|
||||||
|
"sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
|
||||||
|
"sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
|
||||||
|
"sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
|
||||||
|
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
|
||||||
|
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==6.0"
|
||||||
|
},
|
||||||
|
"pyyaml-env-tag": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb",
|
||||||
|
"sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.1"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.16.0"
|
||||||
|
},
|
||||||
|
"watchdog": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412",
|
||||||
|
"sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654",
|
||||||
|
"sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306",
|
||||||
|
"sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33",
|
||||||
|
"sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd",
|
||||||
|
"sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7",
|
||||||
|
"sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892",
|
||||||
|
"sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609",
|
||||||
|
"sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6",
|
||||||
|
"sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1",
|
||||||
|
"sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591",
|
||||||
|
"sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d",
|
||||||
|
"sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d",
|
||||||
|
"sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c",
|
||||||
|
"sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3",
|
||||||
|
"sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39",
|
||||||
|
"sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213",
|
||||||
|
"sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330",
|
||||||
|
"sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428",
|
||||||
|
"sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1",
|
||||||
|
"sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846",
|
||||||
|
"sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153",
|
||||||
|
"sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3",
|
||||||
|
"sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9",
|
||||||
|
"sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==2.1.9"
|
||||||
|
},
|
||||||
|
"zipp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad",
|
||||||
|
"sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
||||||
|
|
||||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
|
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs.akkoma.dev/stable/configuration/cheatsheet.md#frontend_configurations)
|
||||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json).
|
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/static/config.json).
|
||||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) )
|
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/modules/instance.js) )
|
||||||
|
|
||||||
## Instance-defaults
|
## Instance-defaults
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ This could be a bit trickier, you basically need steps 1-4 from *develop build*
|
|||||||
|
|
||||||
### Replacing your instance's frontend with custom FE build
|
### Replacing your instance's frontend with custom FE build
|
||||||
|
|
||||||
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
|
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](https://docs.akkoma.dev/stable/configuration/static_dir/), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
|
||||||
|
|
||||||
### Running production build locally or on a separate server
|
### Running production build locally or on a separate server
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ server {
|
|||||||
|
|
||||||
### API, Data, Operations
|
### API, Data, Operations
|
||||||
|
|
||||||
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs.akkoma.dev/stable/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||||
|
|
||||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 491 B |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,74 @@
|
|||||||
|
p, a, li, pre {
|
||||||
|
font-family: "Tiresias PCFont", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
.codehilite pre {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root > * {
|
||||||
|
--md-primary-fg-color: #593196;
|
||||||
|
--md-accent-fg-color: #455a63;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app { background-repeat: repeat !important; }
|
||||||
|
@media all and (min-width: 800px) { .tab-icon { display: none !important; } }
|
||||||
|
.tab-switcher .tab-icon { color: #fff !important; }
|
||||||
|
.site-name { font-family: BLACKOUT; font-size: 3rem !important; line-height: 4rem;}
|
||||||
|
.site-name::before { content: "%"; }
|
||||||
|
.dropdown-item { color: #fff !important; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Tiresias PCFont';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Tiresias PCFont'), local('Tiresias PCFont'),
|
||||||
|
url('./fonts/Tiresias_PCfont.ttf') format('truetype')
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Tiresias Infofont';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Tiresias Infofont'), local('Tiresias Infofont'),
|
||||||
|
url('./fonts/Tiresias_Infofont.ttf') format('truetype')
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family:'blockzone';
|
||||||
|
src:url('./fonts/blockzone-webfont.woff2') format("woff2"),
|
||||||
|
url('./fonts/blockzone-webfont.woff') format("woff");
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:400
|
||||||
|
}
|
||||||
|
@font-face{
|
||||||
|
font-family:'PeanutMoney';
|
||||||
|
src:url('./fonts/PeanutMoney.ttf') format("opentype");
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:400
|
||||||
|
}
|
||||||
|
@font-face{
|
||||||
|
font-family:'Heading';
|
||||||
|
src:url('./fonts/heading.woff');
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:400
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family:'MonoMMM_5';
|
||||||
|
src:url('./fonts/monoMMM_5.woff');
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:400
|
||||||
|
}
|
||||||
|
@font-face{
|
||||||
|
font-family:'Tiny';
|
||||||
|
src:url('./fonts/tiny.woff');
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:400
|
||||||
|
}
|
||||||
|
@font-face{
|
||||||
|
font-family:'BLACKOUT';
|
||||||
|
src:url('./fonts/BLACKOUT.woff');
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:400
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 530 B |
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4485"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
sodipodi:docname="pleroma_logo_vector_nobg.svg"
|
||||||
|
inkscape:version="0.92.1 r15371">
|
||||||
|
<metadata
|
||||||
|
id="metadata4491">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs4489" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-height="997"
|
||||||
|
id="namedview4487"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.70710678"
|
||||||
|
inkscape:cx="26.131594"
|
||||||
|
inkscape:cy="235.37499"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="22"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4485" />
|
||||||
|
<g
|
||||||
|
id="g4612">
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cccccc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path4495"
|
||||||
|
d="M 235,89 V 423 H 152 V 115 l 26,-26 z"
|
||||||
|
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||||
|
<circle
|
||||||
|
r="26"
|
||||||
|
cx="178"
|
||||||
|
cy="115"
|
||||||
|
id="path4497"
|
||||||
|
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||||
|
<circle
|
||||||
|
r="26"
|
||||||
|
cx="335"
|
||||||
|
cy="230"
|
||||||
|
id="path4497-0"
|
||||||
|
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cccccc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path4516"
|
||||||
|
d="M 277,256 V 89 l 84,3e-6 L 361.00002,230 335,256 Z"
|
||||||
|
style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<circle
|
||||||
|
r="26"
|
||||||
|
cx="335"
|
||||||
|
cy="397"
|
||||||
|
id="path4497-0-6"
|
||||||
|
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cccccc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path4516-5"
|
||||||
|
d="m 277,423 v -83 h 84 l 2e-5,57 L 335,423 Z"
|
||||||
|
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,53 @@
|
|||||||
|
# Adding stickers
|
||||||
|
|
||||||
|
Pleroma-fe supports stickers, which are essentially little images stored server-side
|
||||||
|
which can be selected by a user to automatically attach them to a post.
|
||||||
|
|
||||||
|
There's no explicit setting for these, they just rely on the existence of certain files.
|
||||||
|
|
||||||
|
## Initialising the sticker config file
|
||||||
|
|
||||||
|
You're probably serving pleroma-fe from your instance's `instance/static/` directory -
|
||||||
|
this directy can also override files served at a given path.
|
||||||
|
|
||||||
|
The first thing we need to do is set up our `stickers.json` file. At `instance/static/static/stickers.json`,
|
||||||
|
put a file that looks like this
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"myPack": "/static/stickers/myPack"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is a mapping from name to pack directory location. It says "we have a pack called myPack, look for
|
||||||
|
it at `/static/stickers/myPack`". You can add as many packs as you like in this manner.
|
||||||
|
|
||||||
|
## Creating the pack
|
||||||
|
|
||||||
|
First, create your pack directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p instance/static/static/stickers/myPack
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you need to give it some config.
|
||||||
|
|
||||||
|
At `instance/static/static/stickers/myPack/pack.json`, put a file that looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "myPack",
|
||||||
|
"author": "me for i am very cool",
|
||||||
|
"tabIcon": "tab.png",
|
||||||
|
"stickers": [
|
||||||
|
"mySticker.png"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This should be relatively self-explanatory, it declares a pack with a title `myPack` which has only one sticker in it.
|
||||||
|
The `tabIcon` will appear on the sticker picker itself as a representative of the pack.
|
||||||
|
|
||||||
|
You can add as many stickers as you like. They should all be in the same directory as your `pack.json`.
|
||||||
|
|
||||||
|
Now you should find that there's a sticky note icon on the emoji picker on pleroma-fe that allows you to attach stickers.
|
||||||
@@ -65,6 +65,16 @@ If you set the input-method to Markdown, and post this, it will look something l
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Misskey markdown
|
||||||
|
|
||||||
|
The akkoma version of pleroma-fe includes support for writing and rendering
|
||||||
|
misskey markdown (MFM). To write this you will need to select "MFM" from
|
||||||
|
the content type dropdown (if supported), and then you can format text
|
||||||
|
[in MFM](https://akkoma.dev/sfr/marked-mfm/src/branch/master/docs/syntax.md).
|
||||||
|
|
||||||
|
To render MFM, you will need to turn on "Render Misskey Markdown" in
|
||||||
|
the settings modal.
|
||||||
|
|
||||||
## Other actions
|
## Other actions
|
||||||
|
|
||||||
In addition to posting you can also *favorite* posts also known as *liking* them and *repeat* posts (also known as *retweeting*, *boosting* and even *reprööting*). Favoriting a post increments a counter on it, notifies the post author of your affection towards that post and also adds that post to your "favorited" posts list (in your own profile, "Favorites" tab). Reprööting a post does all that and also repeats this post to your followers and your profile page with a note "*user* repeated post".
|
In addition to posting you can also *favorite* posts also known as *liking* them and *repeat* posts (also known as *retweeting*, *boosting* and even *reprööting*). Favoriting a post increments a counter on it, notifies the post author of your affection towards that post and also adds that post to your "favorited" posts list (in your own profile, "Favorites" tab). Reprööting a post does all that and also repeats this post to your followers and your profile page with a note "*user* repeated post".
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
site_name: Pleroma-FE Documentation
|
||||||
|
theme:
|
||||||
|
favicon: 'images/pleroma_logo_vector_bg_32.png'
|
||||||
|
name: 'material'
|
||||||
|
custom_dir: 'theme'
|
||||||
|
# Disable google fonts
|
||||||
|
font: false
|
||||||
|
logo: 'images/pleroma_logo_vector_nobg.svg'
|
||||||
|
features:
|
||||||
|
- tabs
|
||||||
|
palette:
|
||||||
|
primary: 'deep purple'
|
||||||
|
accent: 'blue grey'
|
||||||
|
|
||||||
|
extra_css:
|
||||||
|
- css/extra.css
|
||||||
|
repo_name: 'AkkomaGang/pleroma-fe'
|
||||||
|
repo_url: 'https://akkoma.dev/AkkomaGang/pleroma-fe'
|
||||||
|
|
||||||
|
extra:
|
||||||
|
repo_icon: gitea
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
# Note/warning blocks https://squidfunk.github.io/mkdocs-material/extensions/admonition/
|
||||||
|
- admonition
|
||||||
|
- codehilite:
|
||||||
|
guess_lang: false
|
||||||
|
# Make it possible to link to every header https://squidfunk.github.io/mkdocs-material/extensions/permalinks/
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.tabbed
|
||||||
|
- pymdownx.details
|
||||||
|
- markdown_include.include:
|
||||||
|
base_path: docs
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
click==8.1.3
|
||||||
|
ghp-import==2.1.0
|
||||||
|
importlib-metadata==4.12.0
|
||||||
|
Jinja2==3.1.2
|
||||||
|
Markdown==3.3.7
|
||||||
|
markdown-include==0.6.0
|
||||||
|
MarkupSafe==2.1.1
|
||||||
|
mergedeep==1.3.4
|
||||||
|
mkdocs==1.3.0
|
||||||
|
mkdocs-bootswatch==1.1
|
||||||
|
mkdocs-material==8.1.8
|
||||||
|
mkdocs-material-extensions==1.0.3
|
||||||
|
packaging==21.3
|
||||||
|
Pygments==2.11.2
|
||||||
|
pymdown-extensions==9.1
|
||||||
|
pyparsing==3.0.9
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
PyYAML==6.0
|
||||||
|
pyyaml_env_tag==0.1
|
||||||
|
six==1.16.0
|
||||||
|
watchdog==2.1.9
|
||||||
|
zipp==3.8.0
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) 2016-2019 Martin Donath <martin.donath@squidfunk.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to
|
||||||
|
deal in the Software without restriction, including without limitation the
|
||||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% import "partials/language.html" as lang with context %}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Check whether the repository is hosted on one of the supported code hosting
|
||||||
|
platforms (GitHub, GitLab or Bitbucket) to show icon.
|
||||||
|
-->
|
||||||
|
{% set platform = config.extra.repo_icon or config.repo_url %}
|
||||||
|
{% if "github" in platform %}
|
||||||
|
{% set repo_type = "github" %}
|
||||||
|
{% elif "gitlab" in platform %}
|
||||||
|
{% set repo_type = "gitlab" %}
|
||||||
|
{% elif "bitbucket" in platform %}
|
||||||
|
{% set repo_type = "bitbucket" %}
|
||||||
|
{% else %}
|
||||||
|
{% set repo_type = "" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page and page.url.startswith('backend') %}
|
||||||
|
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma" %}
|
||||||
|
{% set repo_name = "pleroma/pleroma" %}
|
||||||
|
{% elif page and page.url.startswith('frontend') %}
|
||||||
|
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma-fe" %}
|
||||||
|
{% set repo_name = "pleroma/pleroma-fe" %}
|
||||||
|
{% else %}
|
||||||
|
{% set repo_url = config.repo_url %}
|
||||||
|
{% set repo_name = config.repo_name %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Repository containing source -->
|
||||||
|
<a href="{{ repo_url }}" title="{{ lang.t('source.link.title') }}"
|
||||||
|
class="md-source" data-md-source="{{ repo_type }}">
|
||||||
|
{% if repo_type %}
|
||||||
|
<div class="md-source__icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<use xlink:href="#__{{ repo_type }}" width="24" height="24"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="md-source__repository">
|
||||||
|
{{ repo_name }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||||
|
<title>Pleroma</title>
|
||||||
|
<link rel="stylesheet" href="/static/font/css/fontello.css">
|
||||||
|
<link rel="stylesheet" href="/static/font/css/animation.css">
|
||||||
|
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||||
|
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||||
|
<link rel="stylesheet" href="/static/mfm.css">
|
||||||
<!--server-generated-meta-->
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -16,107 +16,114 @@
|
|||||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.7.6",
|
"@babel/runtime": "7.17.8",
|
||||||
"@chenfengyuan/vue-qrcode": "^1.0.0",
|
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.0",
|
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||||
"body-scroll-lock": "^2.6.4",
|
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||||
"chromatism": "^3.0.0",
|
"@vuelidate/core": "2.0.0-alpha.42",
|
||||||
"cropperjs": "^1.4.3",
|
"@vuelidate/validators": "2.0.0-alpha.30",
|
||||||
"diff": "^3.0.1",
|
"body-scroll-lock": "2.7.1",
|
||||||
"escape-html": "^1.0.3",
|
"chromatism": "3.0.0",
|
||||||
"localforage": "^1.5.0",
|
"click-outside-vue3": "4.0.1",
|
||||||
"parse-link-header": "^1.0.1",
|
"cropperjs": "1.5.12",
|
||||||
"phoenix": "^1.3.0",
|
"diff": "3.5.0",
|
||||||
"portal-vue": "^2.1.4",
|
"escape-html": "1.0.3",
|
||||||
"punycode.js": "^2.1.0",
|
"js-cookie": "^3.0.1",
|
||||||
"ruffle-mirror": "^2021.4.10",
|
"localforage": "1.10.0",
|
||||||
"v-click-outside": "^2.1.1",
|
"marked": "^4.0.17",
|
||||||
"vue": "^2.6.11",
|
"marked-mfm": "^0.5.0",
|
||||||
"vue-i18n": "^7.3.2",
|
"parse-link-header": "1.0.1",
|
||||||
"vue-router": "^3.0.1",
|
"phoenix": "1.6.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"punycode.js": "2.1.0",
|
||||||
"vuelidate": "^0.7.4",
|
"qrcode": "1",
|
||||||
"vuex": "^3.0.1"
|
"ruffle-mirror": "2021.12.31",
|
||||||
|
"vue": "^3.2.31",
|
||||||
|
"vue-i18n": "^9.2.0-beta.39",
|
||||||
|
"vue-router": "4.0.14",
|
||||||
|
"vue-template-compiler": "2.6.11",
|
||||||
|
"vuex": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.7.5",
|
"@babel/core": "7.17.8",
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
"@babel/plugin-transform-runtime": "7.17.0",
|
||||||
"@babel/preset-env": "^7.7.6",
|
"@babel/preset-env": "7.16.11",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "7.17.7",
|
||||||
"@ungap/event-target": "^0.1.0",
|
"@intlify/vue-i18n-loader": "^5.0.0",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
|
"@ungap/event-target": "0.2.3",
|
||||||
"@vue/babel-preset-jsx": "^1.2.4",
|
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
||||||
"@vue/test-utils": "^1.0.0-beta.26",
|
"@vue/babel-plugin-jsx": "1.1.1",
|
||||||
"autoprefixer": "^6.4.0",
|
"@vue/compiler-sfc": "^3.1.0",
|
||||||
"babel-eslint": "^7.0.0",
|
"@vue/test-utils": "^2.0.2",
|
||||||
"babel-loader": "^8.0.6",
|
"autoprefixer": "6.7.7",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-eslint": "7.2.3",
|
||||||
"chai": "^3.5.0",
|
"babel-loader": "8.2.4",
|
||||||
"chalk": "^1.1.3",
|
"babel-plugin-lodash": "3.3.4",
|
||||||
"chromedriver": "^87.0.1",
|
"chai": "3.5.0",
|
||||||
"connect-history-api-fallback": "^1.1.0",
|
"chalk": "1.1.3",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"chromedriver": "87.0.7",
|
||||||
"cross-spawn": "^4.0.2",
|
"connect-history-api-fallback": "1.6.0",
|
||||||
"css-loader": "^0.28.0",
|
"copy-webpack-plugin": "6.4.1",
|
||||||
"custom-event-polyfill": "^1.0.7",
|
"cross-spawn": "4.0.2",
|
||||||
"eslint": "^5.16.0",
|
"css-loader": "0.28.11",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"custom-event-polyfill": "1.0.7",
|
||||||
"eslint-friendly-formatter": "^2.0.5",
|
"eslint": "5.16.0",
|
||||||
"eslint-loader": "^2.1.0",
|
"eslint-config-standard": "12.0.0",
|
||||||
"eslint-plugin-import": "^2.13.0",
|
"eslint-friendly-formatter": "2.0.7",
|
||||||
"eslint-plugin-node": "^7.0.0",
|
"eslint-loader": "2.2.1",
|
||||||
"eslint-plugin-promise": "^4.0.0",
|
"eslint-plugin-import": "2.25.4",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-node": "7.0.1",
|
||||||
"eslint-plugin-vue": "^5.2.2",
|
"eslint-plugin-promise": "4.3.1",
|
||||||
"eventsource-polyfill": "^0.9.6",
|
"eslint-plugin-standard": "4.1.0",
|
||||||
"express": "^4.13.3",
|
"eslint-plugin-vue": "5.2.3",
|
||||||
"file-loader": "^3.0.1",
|
"eventsource-polyfill": "0.9.6",
|
||||||
"function-bind": "^1.0.2",
|
"express": "4.17.3",
|
||||||
"html-webpack-plugin": "^3.0.0",
|
"file-loader": "3.0.1",
|
||||||
"http-proxy-middleware": "^0.17.2",
|
"function-bind": "1.1.1",
|
||||||
"inject-loader": "^2.0.1",
|
"html-webpack-plugin": "3.2.0",
|
||||||
"iso-639-1": "^2.0.3",
|
"http-proxy-middleware": "0.21.0",
|
||||||
"isparta-loader": "^2.0.0",
|
"inject-loader": "2.0.1",
|
||||||
"json-loader": "^0.5.4",
|
"iso-639-1": "2.1.15",
|
||||||
"karma": "^3.0.0",
|
"isparta-loader": "2.0.0",
|
||||||
"karma-coverage": "^1.1.1",
|
"json-loader": "0.5.7",
|
||||||
"karma-firefox-launcher": "^1.1.0",
|
"karma": "6.3.17",
|
||||||
"karma-mocha": "^1.2.0",
|
"karma-coverage": "1.1.2",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-firefox-launcher": "1.3.0",
|
||||||
"karma-sinon-chai": "^2.0.2",
|
"karma-mocha": "2.0.1",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"karma-spec-reporter": "0.0.26",
|
"karma-sinon-chai": "2.0.2",
|
||||||
"karma-webpack": "^4.0.0-rc.3",
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
"lodash": "^4.16.4",
|
"karma-spec-reporter": "0.0.33",
|
||||||
"lolex": "^1.4.0",
|
"karma-webpack": "4.0.2",
|
||||||
"mini-css-extract-plugin": "^0.5.0",
|
"lodash": "4.17.21",
|
||||||
"mocha": "^3.1.0",
|
"lolex": "1.6.0",
|
||||||
"nightwatch": "^0.9.8",
|
"mini-css-extract-plugin": "0.12.0",
|
||||||
"opn": "^4.0.2",
|
"mocha": "3.5.3",
|
||||||
"ora": "^0.3.0",
|
"nightwatch": "0.9.21",
|
||||||
"postcss-loader": "^3.0.0",
|
"opn": "4.0.2",
|
||||||
"raw-loader": "^0.5.1",
|
"ora": "0.4.1",
|
||||||
"sass": "^1.17.3",
|
"postcss-loader": "3.0.0",
|
||||||
"sass-loader": "git://github.com/webpack-contrib/sass-loader",
|
"raw-loader": "0.5.1",
|
||||||
|
"sass": "1.53.0",
|
||||||
|
"sass-loader": "7.3.1",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "5.7.1",
|
||||||
"serviceworker-webpack-plugin": "^1.0.0",
|
"serviceworker-webpack-plugin": "1.0.1",
|
||||||
"shelljs": "^0.8.4",
|
"shelljs": "0.8.5",
|
||||||
"sinon": "^2.1.0",
|
"sinon": "2.4.1",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "2.14.0",
|
||||||
"stylelint": "^13.6.1",
|
"stylelint": "13.6.1",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "20.0.0",
|
||||||
"stylelint-rscss": "^0.4.0",
|
"stylelint-rscss": "0.4.0",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "1.1.2",
|
||||||
"vue-loader": "^14.0.0",
|
"vue-loader": "^16.0.0",
|
||||||
"vue-style-loader": "^4.0.0",
|
"vue-style-loader": "4.1.2",
|
||||||
"webpack": "^4.44.0",
|
"webpack": "4.46.0",
|
||||||
"webpack-dev-middleware": "^3.6.0",
|
"webpack-dev-middleware": "3.7.3",
|
||||||
"webpack-hot-middleware": "^2.12.2",
|
"webpack-hot-middleware": "2.25.1",
|
||||||
"webpack-merge": "^0.14.1"
|
"webpack-merge": "0.20.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4.0.0",
|
"node": ">= 4.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import UserPanel from './components/user_panel/user_panel.vue'
|
import UserPanel from './components/user_panel/user_panel.vue'
|
||||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||||
import Notifications from './components/notifications/notifications.vue'
|
|
||||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
|
||||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MediaModal from './components/media_modal/media_modal.vue'
|
||||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
@@ -16,17 +14,17 @@ import PostStatusModal from './components/post_status_modal/post_status_modal.vu
|
|||||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: {
|
components: {
|
||||||
UserPanel,
|
UserPanel,
|
||||||
NavPanel,
|
NavPanel,
|
||||||
Notifications,
|
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
ShoutPanel,
|
|
||||||
MediaModal,
|
MediaModal,
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
MobilePostStatusButton,
|
MobilePostStatusButton,
|
||||||
@@ -46,10 +44,20 @@ export default {
|
|||||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||||
window.addEventListener('resize', this.updateMobileState)
|
window.addEventListener('resize', this.updateMobileState)
|
||||||
},
|
},
|
||||||
destroyed () {
|
unmounted () {
|
||||||
window.removeEventListener('resize', this.updateMobileState)
|
window.removeEventListener('resize', this.updateMobileState)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
classes () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'-reverse': this.reverseLayout,
|
||||||
|
'-no-sticky-headers': this.noSticky,
|
||||||
|
'-has-new-post-button': this.newPostButtonShown
|
||||||
|
},
|
||||||
|
'-' + this.layoutType
|
||||||
|
]
|
||||||
|
},
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
userBackground () { return this.currentUser.background_image },
|
userBackground () { return this.currentUser.background_image },
|
||||||
instanceBackground () {
|
instanceBackground () {
|
||||||
@@ -65,38 +73,34 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shout () { return this.$store.state.shout.channel.state === 'joined' },
|
|
||||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
showInstanceSpecificPanel () {
|
showInstanceSpecificPanel () {
|
||||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||||
!this.$store.getters.mergedConfig.hideISP &&
|
!this.$store.getters.mergedConfig.hideISP &&
|
||||||
this.$store.state.instance.instanceSpecificPanelContent
|
this.$store.state.instance.instanceSpecificPanelContent
|
||||||
},
|
},
|
||||||
|
newPostButtonShown () {
|
||||||
|
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||||
|
},
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||||
shoutboxPosition () {
|
layoutType () { return this.$store.state.interface.layoutType },
|
||||||
return this.$store.getters.mergedConfig.showNewPostButton || false
|
|
||||||
},
|
|
||||||
hideShoutbox () {
|
|
||||||
return this.$store.getters.mergedConfig.hideShoutbox
|
|
||||||
},
|
|
||||||
isMobileLayout () { return this.$store.state.interface.mobileLayout },
|
|
||||||
privateMode () { return this.$store.state.instance.private },
|
privateMode () { return this.$store.state.instance.private },
|
||||||
sidebarAlign () {
|
reverseLayout () {
|
||||||
return {
|
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||||
'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
|
if (this.layoutType !== 'wide') {
|
||||||
|
return reverseSetting
|
||||||
|
} else {
|
||||||
|
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||||
|
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateMobileState () {
|
updateMobileState () {
|
||||||
const mobileLayout = windowWidth() <= 800
|
this.$store.dispatch('setLayoutWidth', windowWidth())
|
||||||
const layoutHeight = windowHeight()
|
this.$store.dispatch('setLayoutHeight', windowHeight())
|
||||||
const changed = mobileLayout !== this.isMobileLayout
|
|
||||||
if (changed) {
|
|
||||||
this.$store.dispatch('setMobileLayout', mobileLayout)
|
|
||||||
}
|
|
||||||
this.$store.dispatch('setLayoutHeight', layoutHeight)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="app"
|
id="app-loaded"
|
||||||
:style="bgStyle"
|
:style="bgStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="app_bg_wrapper"
|
id="app_bg_wrapper"
|
||||||
class="app-bg-wrapper"
|
class="app-bg-wrapper"
|
||||||
/>
|
/>
|
||||||
<MobileNav v-if="isMobileLayout" />
|
<MobileNav v-if="layoutType === 'mobile'" />
|
||||||
<DesktopNav v-else />
|
<DesktopNav v-else />
|
||||||
<div class="app-bg-wrapper app-container-wrapper" />
|
<Notifications v-if="currentUser" />
|
||||||
<div
|
<div
|
||||||
id="content"
|
id="content"
|
||||||
class="container underlay"
|
class="app-layout container"
|
||||||
|
:class="classes"
|
||||||
>
|
>
|
||||||
|
<div class="underlay" />
|
||||||
<div
|
<div
|
||||||
class="sidebar-flexer mobile-hidden"
|
id="sidebar"
|
||||||
:style="sidebarAlign"
|
class="column -scrollable"
|
||||||
|
:class="{ '-show-scrollbar': showScrollbars }"
|
||||||
>
|
>
|
||||||
<div class="sidebar-bounds">
|
<user-panel />
|
||||||
<div class="sidebar-scroller">
|
<template v-if="layoutType !== 'mobile'">
|
||||||
<div class="sidebar">
|
<nav-panel />
|
||||||
<user-panel />
|
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||||
<div v-if="!isMobileLayout">
|
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
||||||
<nav-panel />
|
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
|
||||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
<div id="notifs-sidebar" />
|
||||||
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
</template>
|
||||||
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
|
|
||||||
<notifications v-if="currentUser" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div
|
||||||
|
id="main-scroller"
|
||||||
|
class="column main"
|
||||||
|
:class="{ '-full-height': false }"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!currentUser"
|
v-if="!currentUser"
|
||||||
class="login-hint panel panel-default"
|
class="login-hint panel panel-default"
|
||||||
@@ -47,19 +48,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
<media-modal />
|
<div
|
||||||
|
id="notifs-column"
|
||||||
|
class="column -scrollable"
|
||||||
|
:class="{ '-show-scrollbar': showScrollbars }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<shout-panel
|
<media-modal />
|
||||||
v-if="currentUser && shout && !hideShoutbox"
|
|
||||||
:floating="true"
|
|
||||||
class="floating-shout mobile-hidden"
|
|
||||||
:class="{ 'left': shoutboxPosition }"
|
|
||||||
/>
|
|
||||||
<MobilePostStatusButton />
|
<MobilePostStatusButton />
|
||||||
<UserReportingModal />
|
<UserReportingModal />
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
<portal-target name="modal" />
|
<div id="modal" />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ $fallback--tooltipRadius: 5px;
|
|||||||
$fallback--avatarRadius: 4px;
|
$fallback--avatarRadius: 4px;
|
||||||
$fallback--avatarAltRadius: 10px;
|
$fallback--avatarAltRadius: 10px;
|
||||||
$fallback--attachmentRadius: 10px;
|
$fallback--attachmentRadius: 10px;
|
||||||
$fallback--chatMessageRadius: 10px;
|
|
||||||
|
|
||||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
|
||||||
|
$status-margin: 0.75em;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -1,8 +1,14 @@
|
|||||||
import Vue from 'vue'
|
import { createApp } from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import routes from './routes'
|
import vClickOutside from 'click-outside-vue3'
|
||||||
|
|
||||||
|
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
import App from '../App.vue'
|
import App from '../App.vue'
|
||||||
import { windowWidth } from '../services/window_utils/window_utils'
|
import routes from './routes'
|
||||||
|
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||||
|
|
||||||
|
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||||
@@ -115,8 +121,14 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||||||
copyInstanceOption('nsfwCensorImage')
|
copyInstanceOption('nsfwCensorImage')
|
||||||
copyInstanceOption('background')
|
copyInstanceOption('background')
|
||||||
copyInstanceOption('hidePostStats')
|
copyInstanceOption('hidePostStats')
|
||||||
|
copyInstanceOption('hideBotIndication')
|
||||||
copyInstanceOption('hideUserStats')
|
copyInstanceOption('hideUserStats')
|
||||||
copyInstanceOption('hideFilteredStatuses')
|
copyInstanceOption('hideFilteredStatuses')
|
||||||
|
copyInstanceOption('hideSiteName')
|
||||||
|
copyInstanceOption('hideSiteFavicon')
|
||||||
|
copyInstanceOption('showWiderShortcuts')
|
||||||
|
copyInstanceOption('showNavShortcuts')
|
||||||
|
copyInstanceOption('showPanelNavShortcuts')
|
||||||
copyInstanceOption('logo')
|
copyInstanceOption('logo')
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
@@ -147,6 +159,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||||||
copyInstanceOption('alwaysShowSubjectInput')
|
copyInstanceOption('alwaysShowSubjectInput')
|
||||||
copyInstanceOption('showFeaturesPanel')
|
copyInstanceOption('showFeaturesPanel')
|
||||||
copyInstanceOption('hideSitename')
|
copyInstanceOption('hideSitename')
|
||||||
|
copyInstanceOption('renderMisskeyMarkdown')
|
||||||
copyInstanceOption('sidebarRight')
|
copyInstanceOption('sidebarRight')
|
||||||
|
|
||||||
return store.dispatch('setTheme', config['theme'])
|
return store.dispatch('setTheme', config['theme'])
|
||||||
@@ -240,9 +253,6 @@ const getNodeInfo = async ({ store }) => {
|
|||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||||
@@ -281,6 +291,7 @@ const getNodeInfo = async ({ store }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
name: 'federating',
|
name: 'federating',
|
||||||
value: typeof federation.enabled === 'undefined'
|
value: typeof federation.enabled === 'undefined'
|
||||||
@@ -325,8 +336,8 @@ const checkOAuthToken = async ({ store }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const afterStoreSetup = async ({ store, i18n }) => {
|
const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
const width = windowWidth()
|
store.dispatch('setLayoutWidth', windowWidth())
|
||||||
store.dispatch('setMobileLayout', width <= 800)
|
store.dispatch('setLayoutHeight', windowHeight())
|
||||||
|
|
||||||
FaviconService.initFaviconService()
|
FaviconService.initFaviconService()
|
||||||
|
|
||||||
@@ -363,28 +374,37 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||||||
|
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
store.dispatch('fetchMutes')
|
||||||
|
store.dispatch('startFetchingAnnouncements')
|
||||||
getTOS({ store })
|
getTOS({ store })
|
||||||
getStickers({ store })
|
getStickers({ store })
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = createRouter({
|
||||||
mode: 'history',
|
history: createWebHistory(),
|
||||||
routes: routes(store),
|
routes: routes(store),
|
||||||
scrollBehavior: (to, _from, savedPosition) => {
|
scrollBehavior: (to, _from, savedPosition) => {
|
||||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||||
return false
|
return {}
|
||||||
}
|
}
|
||||||
return savedPosition || { x: 0, y: 0 }
|
|
||||||
|
return savedPosition || { left: 0, top: 0 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
const app = createApp(App)
|
||||||
return new Vue({
|
|
||||||
router,
|
app.use(router)
|
||||||
store,
|
app.use(store)
|
||||||
i18n,
|
app.use(i18n)
|
||||||
el: '#app',
|
|
||||||
render: h => h(App)
|
app.use(vClickOutside)
|
||||||
})
|
app.use(VBodyScrollLock)
|
||||||
|
|
||||||
|
app.component('FAIcon', FontAwesomeIcon)
|
||||||
|
app.component('FALayers', FontAwesomeLayers)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
export default afterStoreSetup
|
export default afterStoreSetup
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
|||||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||||
|
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
|
||||||
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||||
import Interactions from 'components/interactions/interactions.vue'
|
import Interactions from 'components/interactions/interactions.vue'
|
||||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||||
import ChatList from 'components/chat_list/chat_list.vue'
|
|
||||||
import Chat from 'components/chat/chat.vue'
|
|
||||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||||
import Search from 'components/search/search.vue'
|
import Search from 'components/search/search.vue'
|
||||||
import Registration from 'components/registration/registration.vue'
|
import Registration from 'components/registration/registration.vue'
|
||||||
@@ -16,10 +15,13 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
|||||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||||
import Notifications from 'components/notifications/notifications.vue'
|
import Notifications from 'components/notifications/notifications.vue'
|
||||||
import AuthForm from 'components/auth_form/auth_form.js'
|
import AuthForm from 'components/auth_form/auth_form.js'
|
||||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
|
||||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||||
import About from 'components/about/about.vue'
|
import About from 'components/about/about.vue'
|
||||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||||
|
import Lists from 'components/lists/lists.vue'
|
||||||
|
import ListTimeline from 'components/list_timeline/list_timeline.vue'
|
||||||
|
import ListEdit from 'components/list_edit/list_edit.vue'
|
||||||
|
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||||
|
|
||||||
export default (store) => {
|
export default (store) => {
|
||||||
const validateAuthenticatedRoute = (to, from, next) => {
|
const validateAuthenticatedRoute = (to, from, next) => {
|
||||||
@@ -41,12 +43,13 @@ export default (store) => {
|
|||||||
},
|
},
|
||||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||||
|
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
|
||||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||||
{ name: 'remote-user-profile-acct',
|
{ name: 'remote-user-profile-acct',
|
||||||
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
|
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||||
component: RemoteUserResolver,
|
component: RemoteUserResolver,
|
||||||
beforeEnter: validateAuthenticatedRoute
|
beforeEnter: validateAuthenticatedRoute
|
||||||
},
|
},
|
||||||
@@ -55,29 +58,25 @@ export default (store) => {
|
|||||||
component: RemoteUserResolver,
|
component: RemoteUserResolver,
|
||||||
beforeEnter: validateAuthenticatedRoute
|
beforeEnter: validateAuthenticatedRoute
|
||||||
},
|
},
|
||||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } },
|
||||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'registration', path: '/registration', component: Registration },
|
{ name: 'registration', path: '/registration', component: Registration },
|
||||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'login', path: '/login', component: AuthForm },
|
{ name: 'login', path: '/login', component: AuthForm },
|
||||||
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
|
|
||||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'about', path: '/about', component: About },
|
{ name: 'about', path: '/about', component: About },
|
||||||
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
{ name: 'lists', path: '/lists', component: Lists },
|
||||||
|
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
|
||||||
|
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
|
||||||
|
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||||
|
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } }
|
||||||
]
|
]
|
||||||
|
|
||||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
|
||||||
routes = routes.concat([
|
|
||||||
{ name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
|
|
||||||
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import FeaturesPanel from '../features_panel/features_panel.vue'
|
|||||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||||
|
import LocalBubblePanel from '../local_bubble_panel/local_bubble_panel.vue'
|
||||||
|
|
||||||
const About = {
|
const About = {
|
||||||
components: {
|
components: {
|
||||||
@@ -10,7 +11,8 @@ const About = {
|
|||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
TermsOfServicePanel,
|
TermsOfServicePanel,
|
||||||
StaffPanel,
|
StaffPanel,
|
||||||
MRFTransparencyPanel
|
MRFTransparencyPanel,
|
||||||
|
LocalBubblePanel
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sidebar">
|
<div class="column-inner">
|
||||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||||
<staff-panel />
|
<staff-panel />
|
||||||
<terms-of-service-panel />
|
<terms-of-service-panel />
|
||||||
|
<LocalBubblePanel />
|
||||||
<MRFTransparencyPanel />
|
<MRFTransparencyPanel />
|
||||||
<features-panel v-if="showFeaturesPanel" />
|
<features-panel v-if="showFeaturesPanel" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { mapState } from 'vuex'
|
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
@@ -36,18 +35,7 @@ const AccountActions = {
|
|||||||
},
|
},
|
||||||
reportUser () {
|
reportUser () {
|
||||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||||
},
|
|
||||||
openChat () {
|
|
||||||
this.$router.push({
|
|
||||||
name: 'chat',
|
|
||||||
params: { recipient_id: this.user.id }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,6 @@
|
|||||||
>
|
>
|
||||||
{{ $t('user_card.report') }}
|
{{ $t('user_card.report') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
v-if="pleromaChatMessagesAvailable"
|
|
||||||
class="btn button-default btn-block dropdown-item"
|
|
||||||
@click="openChat"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.message') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template v-slot:trigger>
|
||||||
@@ -74,10 +67,6 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
.AccountActions {
|
.AccountActions {
|
||||||
button.dropdown-item {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis-button {
|
.ellipsis-button {
|
||||||
width: 2.5em;
|
width: 2.5em;
|
||||||
margin: -0.5em 0;
|
margin: -0.5em 0;
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { mapState } from 'vuex'
|
||||||
|
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||||
|
import RichContent from '../rich_content/rich_content.jsx'
|
||||||
|
import localeService from '../../services/locale/locale.service.js'
|
||||||
|
|
||||||
|
const Announcement = {
|
||||||
|
components: {
|
||||||
|
AnnouncementEditor,
|
||||||
|
RichContent
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
editing: false,
|
||||||
|
editedAnnouncement: {
|
||||||
|
content: '',
|
||||||
|
startsAt: undefined,
|
||||||
|
endsAt: undefined,
|
||||||
|
allDay: undefined
|
||||||
|
},
|
||||||
|
editError: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
announcement: Object
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
}),
|
||||||
|
content () {
|
||||||
|
return this.announcement.content
|
||||||
|
},
|
||||||
|
isRead () {
|
||||||
|
return this.announcement.read
|
||||||
|
},
|
||||||
|
publishedAt () {
|
||||||
|
const time = this.announcement['published_at']
|
||||||
|
if (!time) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||||
|
},
|
||||||
|
startsAt () {
|
||||||
|
const time = this.announcement['starts_at']
|
||||||
|
if (!time) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||||
|
},
|
||||||
|
endsAt () {
|
||||||
|
const time = this.announcement['ends_at']
|
||||||
|
if (!time) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||||
|
},
|
||||||
|
inactive () {
|
||||||
|
return this.announcement.inactive
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
markAsRead () {
|
||||||
|
if (!this.isRead) {
|
||||||
|
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteAnnouncement () {
|
||||||
|
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||||
|
},
|
||||||
|
formatTimeOrDate (time, locale) {
|
||||||
|
const d = new Date(time)
|
||||||
|
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||||
|
},
|
||||||
|
enterEditMode () {
|
||||||
|
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
|
||||||
|
this.editedAnnouncement.startsAt = this.announcement['starts_at']
|
||||||
|
this.editedAnnouncement.endsAt = this.announcement['ends_at']
|
||||||
|
this.editedAnnouncement.allDay = this.announcement['all_day']
|
||||||
|
this.editing = true
|
||||||
|
},
|
||||||
|
submitEdit () {
|
||||||
|
this.$store.dispatch('editAnnouncement', {
|
||||||
|
id: this.announcement.id,
|
||||||
|
...this.editedAnnouncement
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.editing = false
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.editError = error.error
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cancelEdit () {
|
||||||
|
this.editing = false
|
||||||
|
},
|
||||||
|
clearError () {
|
||||||
|
this.editError = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Announcement
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div class="announcement">
|
||||||
|
<div class="heading">
|
||||||
|
<h4>{{ $t('announcements.title') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<rich-content
|
||||||
|
v-if="!editing"
|
||||||
|
:html="content"
|
||||||
|
:emoji="announcement.emojis"
|
||||||
|
:handle-links="true"
|
||||||
|
/>
|
||||||
|
<announcement-editor
|
||||||
|
v-else
|
||||||
|
:announcement="editedAnnouncement"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<div
|
||||||
|
v-if="!editing"
|
||||||
|
class="times"
|
||||||
|
>
|
||||||
|
<span v-if="publishedAt">
|
||||||
|
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="startsAt">
|
||||||
|
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="endsAt">
|
||||||
|
{{ $t('announcements.end_time_display', { time: endsAt }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!editing"
|
||||||
|
class="actions"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="currentUser"
|
||||||
|
class="btn button-default"
|
||||||
|
:class="{ toggled: isRead }"
|
||||||
|
:disabled="inactive"
|
||||||
|
:title="inactive ? $t('announcements.inactive_message') : ''"
|
||||||
|
@click="markAsRead"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.mark_as_read_action') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
|
class="btn button-default"
|
||||||
|
@click="enterEditMode"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.edit_action') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
|
class="btn button-default"
|
||||||
|
@click="deleteAnnouncement"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.delete_action') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="actions"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click="submitEdit"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.submit_edit_action') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click="cancelEdit"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.cancel_edit_action') }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="editing && editError"
|
||||||
|
class="alert error"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.edit_error', { error }) }}
|
||||||
|
<button
|
||||||
|
class="button-unstyled"
|
||||||
|
@click="clearError"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="times"
|
||||||
|
:title="$t('announcements.close_error')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./announcement.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../variables";
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
|
.heading, .body {
|
||||||
|
margin-bottom: var(--status-margin, $status-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.times {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer .actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
margin: 1em;
|
||||||
|
max-width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const AnnouncementEditor = {
|
||||||
|
components: {
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
announcement: Object,
|
||||||
|
disabled: Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnouncementEditor
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="announcement-editor">
|
||||||
|
<textarea
|
||||||
|
ref="textarea"
|
||||||
|
v-model="announcement.content"
|
||||||
|
class="post-textarea"
|
||||||
|
rows="1"
|
||||||
|
cols="1"
|
||||||
|
:placeholder="$t('announcements.post_placeholder')"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<span class="announcement-metadata">
|
||||||
|
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
|
||||||
|
<input
|
||||||
|
id="announcement-start-time"
|
||||||
|
v-model="announcement.startsAt"
|
||||||
|
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span class="announcement-metadata">
|
||||||
|
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
|
||||||
|
<input
|
||||||
|
id="announcement-end-time"
|
||||||
|
v-model="announcement.endsAt"
|
||||||
|
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span class="announcement-metadata">
|
||||||
|
<Checkbox
|
||||||
|
id="announcement-all-day"
|
||||||
|
v-model="announcement.allDay"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./announcement_editor.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.announcement-editor {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.announcement-metadata {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
height: 10em;
|
||||||
|
overflow: none;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { mapState } from 'vuex'
|
||||||
|
import Announcement from '../announcement/announcement.vue'
|
||||||
|
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||||
|
|
||||||
|
const AnnouncementsPage = {
|
||||||
|
components: {
|
||||||
|
Announcement,
|
||||||
|
AnnouncementEditor
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
newAnnouncement: {
|
||||||
|
content: '',
|
||||||
|
startsAt: undefined,
|
||||||
|
endsAt: undefined,
|
||||||
|
allDay: false
|
||||||
|
},
|
||||||
|
posting: false,
|
||||||
|
error: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$store.dispatch('fetchAnnouncements')
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
}),
|
||||||
|
announcements () {
|
||||||
|
return this.$store.state.announcements.announcements
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
postAnnouncement () {
|
||||||
|
this.posting = true
|
||||||
|
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||||
|
.then(() => {
|
||||||
|
this.newAnnouncement.content = ''
|
||||||
|
this.startsAt = undefined
|
||||||
|
this.endsAt = undefined
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.error = error.error
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.posting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearError () {
|
||||||
|
this.error = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnouncementsPage
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel panel-default announcements-page">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span>
|
||||||
|
{{ $t('announcements.page_header') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<section
|
||||||
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
|
>
|
||||||
|
<div class="post-form">
|
||||||
|
<div class="heading">
|
||||||
|
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<announcement-editor
|
||||||
|
:announcement="newAnnouncement"
|
||||||
|
:disabled="posting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button
|
||||||
|
class="btn button-default post-button"
|
||||||
|
:disabled="posting"
|
||||||
|
@click.prevent="postAnnouncement"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.post_action') }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="alert error"
|
||||||
|
>
|
||||||
|
{{ $t('announcements.post_error', { error }) }}
|
||||||
|
<button
|
||||||
|
class="button-unstyled"
|
||||||
|
@click="clearError"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="times"
|
||||||
|
:title="$t('announcements.close_error')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
v-for="announcement in announcements"
|
||||||
|
:key="announcement.id"
|
||||||
|
>
|
||||||
|
<announcement
|
||||||
|
:announcement="announcement"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./announcements_page.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../variables";
|
||||||
|
|
||||||
|
.announcements-page {
|
||||||
|
.post-form {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
|
.heading, .body {
|
||||||
|
margin-bottom: var(--status-margin, $status-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-button {
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
emits: ['resetAsyncComponent'],
|
||||||
methods: {
|
methods: {
|
||||||
retry () {
|
retry () {
|
||||||
this.$emit('resetAsyncComponent')
|
this.$emit('resetAsyncComponent')
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import {
|
|||||||
faImage,
|
faImage,
|
||||||
faVideo,
|
faVideo,
|
||||||
faPlayCircle,
|
faPlayCircle,
|
||||||
faTimes
|
faTimes,
|
||||||
|
faStop,
|
||||||
|
faSearchPlus,
|
||||||
|
faTrashAlt,
|
||||||
|
faPencilAlt,
|
||||||
|
faAlignRight
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
@@ -20,27 +25,39 @@ library.add(
|
|||||||
faImage,
|
faImage,
|
||||||
faVideo,
|
faVideo,
|
||||||
faPlayCircle,
|
faPlayCircle,
|
||||||
faTimes
|
faTimes,
|
||||||
|
faStop,
|
||||||
|
faSearchPlus,
|
||||||
|
faTrashAlt,
|
||||||
|
faPencilAlt,
|
||||||
|
faAlignRight
|
||||||
)
|
)
|
||||||
|
|
||||||
const Attachment = {
|
const Attachment = {
|
||||||
props: [
|
props: [
|
||||||
'attachment',
|
'attachment',
|
||||||
|
'description',
|
||||||
|
'hideDescription',
|
||||||
'nsfw',
|
'nsfw',
|
||||||
'size',
|
'size',
|
||||||
'allowPlay',
|
|
||||||
'setMedia',
|
'setMedia',
|
||||||
'naturalSizeLoad'
|
'remove',
|
||||||
|
'shiftUp',
|
||||||
|
'shiftDn',
|
||||||
|
'edit'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
localDescription: this.description || this.attachment.description,
|
||||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||||
loading: false,
|
loading: false,
|
||||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||||
modalOpen: false,
|
modalOpen: false,
|
||||||
showHidden: false
|
showHidden: false,
|
||||||
|
flashLoaded: false,
|
||||||
|
showDescription: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@@ -49,8 +66,23 @@ const Attachment = {
|
|||||||
VideoAttachment
|
VideoAttachment
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
classNames () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'-loading': this.loading,
|
||||||
|
'-nsfw-placeholder': this.hidden,
|
||||||
|
'-editable': this.edit !== undefined
|
||||||
|
},
|
||||||
|
'-type-' + this.type,
|
||||||
|
this.size && '-size-' + this.size,
|
||||||
|
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||||
|
]
|
||||||
|
},
|
||||||
usePlaceholder () {
|
usePlaceholder () {
|
||||||
return this.size === 'hide' || this.type === 'unknown'
|
return this.size === 'hide'
|
||||||
|
},
|
||||||
|
useContainFit () {
|
||||||
|
return this.$store.getters.mergedConfig.useContainFit
|
||||||
},
|
},
|
||||||
placeholderName () {
|
placeholderName () {
|
||||||
if (this.attachment.description === '' || !this.attachment.description) {
|
if (this.attachment.description === '' || !this.attachment.description) {
|
||||||
@@ -74,24 +106,36 @@ const Attachment = {
|
|||||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||||
},
|
},
|
||||||
isEmpty () {
|
isEmpty () {
|
||||||
return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
|
return (this.type === 'html' && !this.attachment.oembed)
|
||||||
},
|
|
||||||
isSmall () {
|
|
||||||
return this.size === 'small'
|
|
||||||
},
|
|
||||||
fullwidth () {
|
|
||||||
if (this.size === 'hide') return false
|
|
||||||
return this.type === 'html' || this.type === 'audio' || this.type === 'unknown'
|
|
||||||
},
|
},
|
||||||
useModal () {
|
useModal () {
|
||||||
const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
|
let modalTypes = []
|
||||||
: this.mergedConfig.playVideosInModal
|
switch (this.size) {
|
||||||
? ['image', 'video']
|
case 'hide':
|
||||||
: ['image']
|
case 'small':
|
||||||
|
modalTypes = ['image', 'video', 'audio', 'flash']
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
modalTypes = this.mergedConfig.playVideosInModal
|
||||||
|
? ['image', 'video', 'flash']
|
||||||
|
: ['image']
|
||||||
|
break
|
||||||
|
}
|
||||||
return modalTypes.includes(this.type)
|
return modalTypes.includes(this.type)
|
||||||
},
|
},
|
||||||
|
videoTag () {
|
||||||
|
return this.useModal ? 'button' : 'span'
|
||||||
|
},
|
||||||
|
statusForm () {
|
||||||
|
return this.$parent.$parent
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
localDescription (newVal) {
|
||||||
|
this.onEdit(newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
linkClicked ({ target }) {
|
linkClicked ({ target }) {
|
||||||
if (target.tagName === 'A') {
|
if (target.tagName === 'A') {
|
||||||
@@ -100,12 +144,37 @@ const Attachment = {
|
|||||||
},
|
},
|
||||||
openModal (event) {
|
openModal (event) {
|
||||||
if (this.useModal) {
|
if (this.useModal) {
|
||||||
event.stopPropagation()
|
this.$emit('setMedia')
|
||||||
event.preventDefault()
|
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||||
this.setMedia()
|
} else if (this.type === 'unknown') {
|
||||||
this.$store.dispatch('setCurrent', this.attachment)
|
window.open(this.attachment.url)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openModalForce (event) {
|
||||||
|
this.$emit('setMedia')
|
||||||
|
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||||
|
},
|
||||||
|
onEdit (event) {
|
||||||
|
this.edit && this.edit(this.attachment, event)
|
||||||
|
},
|
||||||
|
onRemove () {
|
||||||
|
this.remove && this.remove(this.attachment)
|
||||||
|
},
|
||||||
|
onShiftUp () {
|
||||||
|
this.shiftUp && this.shiftUp(this.attachment)
|
||||||
|
},
|
||||||
|
onShiftDn () {
|
||||||
|
this.shiftDn && this.shiftDn(this.attachment)
|
||||||
|
},
|
||||||
|
stopFlash () {
|
||||||
|
this.$refs.flash.closePlayer()
|
||||||
|
},
|
||||||
|
setFlashLoaded (event) {
|
||||||
|
this.flashLoaded = event
|
||||||
|
},
|
||||||
|
toggleDescription () {
|
||||||
|
this.showDescription = !this.showDescription
|
||||||
|
},
|
||||||
toggleHidden (event) {
|
toggleHidden (event) {
|
||||||
if (
|
if (
|
||||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||||
@@ -132,7 +201,30 @@ const Attachment = {
|
|||||||
onImageLoad (image) {
|
onImageLoad (image) {
|
||||||
const width = image.naturalWidth
|
const width = image.naturalWidth
|
||||||
const height = image.naturalHeight
|
const height = image.naturalHeight
|
||||||
this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
|
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||||
|
},
|
||||||
|
resize (e) {
|
||||||
|
const target = e.target || e
|
||||||
|
if (!(target instanceof window.Element)) { return }
|
||||||
|
|
||||||
|
// Reset to default height for empty form, nothing else to do here.
|
||||||
|
if (target.value === '') {
|
||||||
|
target.style.height = null
|
||||||
|
this.$emit('resize')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddingString = getComputedStyle(target)['padding']
|
||||||
|
// remove -px suffix
|
||||||
|
const padding = Number(paddingString.substring(0, paddingString.length - 2))
|
||||||
|
|
||||||
|
target.style.height = 'auto'
|
||||||
|
const newHeight = Math.floor(target.scrollHeight - padding * 2)
|
||||||
|
target.style.height = `${newHeight}px`
|
||||||
|
this.$emit('resize', newHeight)
|
||||||
|
},
|
||||||
|
postStatus (event) {
|
||||||
|
console.log(this.statusForm.postStatus(event, this.statusForm.newStatus))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.Attachment {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
align-self: flex-start;
|
||||||
|
line-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: $fallback--attachmentRadius;
|
||||||
|
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
||||||
|
border-color: $fallback--border;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
|
||||||
|
.attachment-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 200px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-container {
|
||||||
|
flex: 0 1 0;
|
||||||
|
display: flex;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
p {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-line;
|
||||||
|
word-break: break-word;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-static {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
background: var(--popover);
|
||||||
|
box-shadow: var(--popupShadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
box-sizing: content-box;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: min-height 200ms 100ms;
|
||||||
|
|
||||||
|
padding-bottom: var(--_padding);
|
||||||
|
height: calc(var(--post-line-height) * 1em);
|
||||||
|
min-height: calc(var(--post-line-height) * 1em);
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&.scrollable-form {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .placeholder-container,
|
||||||
|
& .image-container,
|
||||||
|
& .audio-container,
|
||||||
|
& .video-container,
|
||||||
|
& .flash-container,
|
||||||
|
& .oembed-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .flash-container,
|
||||||
|
& .video-container {
|
||||||
|
& .flash,
|
||||||
|
& video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
audio {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.play-icon {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 64px;
|
||||||
|
top: calc(50% - 32px);
|
||||||
|
left: calc(50% - 32px);
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-buttons {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.attachment-button {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: $fallback--tooltipRadius;
|
||||||
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
|
text-align: center;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
// TODO: theming? hard to theme with unknown background image color
|
||||||
|
background: rgba(230, 230, 230, 0.7);
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .svg-inline--fa {
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.oembed-container {
|
||||||
|
line-height: 1.2em;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 15px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
flex: 1;
|
||||||
|
img {
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 2;
|
||||||
|
margin: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-size-small {
|
||||||
|
.play-icon {
|
||||||
|
zoom: 0.5;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-buttons {
|
||||||
|
zoom: 0.7;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-editable {
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
& .description-container,
|
||||||
|
& .attachment-buttons {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-placeholder {
|
||||||
|
display: inline-block;
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--postLink, $fallback--link);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: auto;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&:not(.-editable) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-editable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
& .description-container,
|
||||||
|
& .attachment-buttons {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-container {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-buttons {
|
||||||
|
order: 99;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-loading {
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-contain-fit {
|
||||||
|
img,
|
||||||
|
canvas {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-cover-fit {
|
||||||
|
img,
|
||||||
|
canvas {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<button
|
||||||
v-if="usePlaceholder"
|
v-if="usePlaceholder"
|
||||||
:class="{ 'fullwidth': fullwidth }"
|
class="Attachment -placeholder button-unstyled"
|
||||||
|
:class="classNames"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -11,318 +12,259 @@
|
|||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
:alt="attachment.description"
|
:alt="attachment.description"
|
||||||
:title="attachment.description"
|
:title="attachment.description"
|
||||||
|
@click.prevent
|
||||||
>
|
>
|
||||||
<FAIcon :icon="placeholderIconClass" />
|
<FAIcon :icon="placeholderIconClass" />
|
||||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
|
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<div
|
||||||
|
v-if="edit || remove"
|
||||||
|
class="attachment-buttons"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="remove"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
@click.prevent="onRemove"
|
||||||
|
>
|
||||||
|
<FAIcon icon="trash-alt" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||||
|
class="description-container"
|
||||||
|
:class="{ '-static': !edit }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="edit"
|
||||||
|
v-model="localDescription"
|
||||||
|
type="text"
|
||||||
|
class="description-field"
|
||||||
|
:placeholder="$t('post_status.media_description')"
|
||||||
|
@keydown.enter.prevent=""
|
||||||
|
>
|
||||||
|
<p v-else>
|
||||||
|
{{ localDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
v-show="!isEmpty"
|
class="Attachment"
|
||||||
class="attachment"
|
:class="classNames"
|
||||||
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
|
|
||||||
>
|
>
|
||||||
<a
|
|
||||||
v-if="hidden"
|
|
||||||
class="image-attachment"
|
|
||||||
:href="attachment.url"
|
|
||||||
:alt="attachment.description"
|
|
||||||
:title="attachment.description"
|
|
||||||
@click.prevent.stop="toggleHidden"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:key="nsfwImage"
|
|
||||||
class="nsfw"
|
|
||||||
:src="nsfwImage"
|
|
||||||
:class="{'small': isSmall}"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
v-if="type === 'video'"
|
|
||||||
class="play-icon"
|
|
||||||
icon="play-circle"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
v-if="nsfw && hideNsfwLocal && !hidden"
|
|
||||||
class="button-unstyled hider"
|
|
||||||
@click.prevent="toggleHidden"
|
|
||||||
>
|
|
||||||
<FAIcon icon="times" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a
|
|
||||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
|
||||||
class="image-attachment"
|
|
||||||
:class="{'hidden': hidden && preloadImage }"
|
|
||||||
:href="attachment.url"
|
|
||||||
target="_blank"
|
|
||||||
@click="openModal"
|
|
||||||
>
|
|
||||||
<StillImage
|
|
||||||
class="image"
|
|
||||||
:referrerpolicy="referrerpolicy"
|
|
||||||
:mimetype="attachment.mimetype"
|
|
||||||
:src="attachment.large_thumb_url || attachment.url"
|
|
||||||
:image-load-handler="onImageLoad"
|
|
||||||
:alt="attachment.description"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
v-if="type === 'video' && !hidden"
|
|
||||||
class="video-container"
|
|
||||||
:class="{'small': isSmall}"
|
|
||||||
:href="allowPlay ? undefined : attachment.url"
|
|
||||||
@click="openModal"
|
|
||||||
>
|
|
||||||
<VideoAttachment
|
|
||||||
class="video"
|
|
||||||
:attachment="attachment"
|
|
||||||
:controls="allowPlay"
|
|
||||||
@play="$emit('play')"
|
|
||||||
@pause="$emit('pause')"
|
|
||||||
/>
|
|
||||||
<FAIcon
|
|
||||||
v-if="!allowPlay"
|
|
||||||
class="play-icon"
|
|
||||||
icon="play-circle"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<audio
|
|
||||||
v-if="type === 'audio'"
|
|
||||||
:src="attachment.url"
|
|
||||||
:alt="attachment.description"
|
|
||||||
:title="attachment.description"
|
|
||||||
controls
|
|
||||||
@play="$emit('play')"
|
|
||||||
@pause="$emit('pause')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="type === 'html' && attachment.oembed"
|
v-show="!isEmpty"
|
||||||
class="oembed"
|
class="attachment-wrapper"
|
||||||
@click.prevent="linkClicked"
|
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
v-if="attachment.thumb_url"
|
v-if="hidden"
|
||||||
class="image"
|
class="image-container"
|
||||||
|
:href="attachment.url"
|
||||||
|
:alt="attachment.description"
|
||||||
|
:title="attachment.description"
|
||||||
|
@click.prevent.stop="toggleHidden"
|
||||||
>
|
>
|
||||||
<img :src="attachment.thumb_url">
|
<img
|
||||||
|
:key="nsfwImage"
|
||||||
|
class="nsfw"
|
||||||
|
:src="nsfwImage"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
v-if="type === 'video'"
|
||||||
|
class="play-icon"
|
||||||
|
icon="play-circle"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-if="!hidden"
|
||||||
|
class="attachment-buttons"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="type === 'flash' && flashLoaded"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.attachment_stop_flash')"
|
||||||
|
@click.prevent="stopFlash"
|
||||||
|
>
|
||||||
|
<FAIcon icon="stop" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.show_attachment_description')"
|
||||||
|
@click.prevent="toggleDescription"
|
||||||
|
>
|
||||||
|
<FAIcon icon="align-right" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!useModal && type !== 'unknown'"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.show_attachment_in_modal')"
|
||||||
|
@click.prevent="openModalForce"
|
||||||
|
>
|
||||||
|
<FAIcon icon="search-plus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="nsfw && hideNsfwLocal"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.hide_attachment')"
|
||||||
|
@click.prevent="toggleHidden"
|
||||||
|
>
|
||||||
|
<FAIcon icon="times" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="shiftUp"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.move_up')"
|
||||||
|
@click.prevent="onShiftUp"
|
||||||
|
>
|
||||||
|
<FAIcon icon="chevron-left" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="shiftDn"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.move_down')"
|
||||||
|
@click.prevent="onShiftDn"
|
||||||
|
>
|
||||||
|
<FAIcon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="remove"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.remove_attachment')"
|
||||||
|
@click.prevent="onRemove"
|
||||||
|
>
|
||||||
|
<FAIcon icon="trash-alt" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
|
||||||
<div v-html="attachment.oembed.oembedHTML" />
|
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Flash
|
<a
|
||||||
v-if="type === 'flash'"
|
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||||
:src="attachment.large_thumb_url || attachment.url"
|
class="image-container"
|
||||||
/>
|
:class="{'-hidden': hidden && preloadImage }"
|
||||||
|
:href="attachment.url"
|
||||||
|
target="_blank"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<StillImage
|
||||||
|
class="image"
|
||||||
|
:referrerpolicy="referrerpolicy"
|
||||||
|
:mimetype="attachment.mimetype"
|
||||||
|
:src="attachment.large_thumb_url || attachment.url"
|
||||||
|
:image-load-handler="onImageLoad"
|
||||||
|
:alt="attachment.description"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="type === 'unknown' && !hidden"
|
||||||
|
class="placeholder-container"
|
||||||
|
:href="attachment.url"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
size="5x"
|
||||||
|
:icon="placeholderIconClass"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
{{ localDescription }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="videoTag"
|
||||||
|
v-if="type === 'video' && !hidden"
|
||||||
|
class="video-container"
|
||||||
|
:class="{ 'button-unstyled': 'isModal' }"
|
||||||
|
:href="attachment.url"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<VideoAttachment
|
||||||
|
class="video"
|
||||||
|
:attachment="attachment"
|
||||||
|
:controls="!useModal"
|
||||||
|
@play="$emit('play')"
|
||||||
|
@pause="$emit('pause')"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="useModal"
|
||||||
|
class="play-icon"
|
||||||
|
icon="play-circle"
|
||||||
|
/>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="type === 'audio' && !hidden"
|
||||||
|
class="audio-container"
|
||||||
|
:href="attachment.url"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<audio
|
||||||
|
v-if="type === 'audio'"
|
||||||
|
:src="attachment.url"
|
||||||
|
:alt="attachment.description"
|
||||||
|
:title="attachment.description"
|
||||||
|
controls
|
||||||
|
@play="$emit('play')"
|
||||||
|
@pause="$emit('pause')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="type === 'html' && attachment.oembed"
|
||||||
|
class="oembed-container"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="attachment.thumb_url"
|
||||||
|
class="image"
|
||||||
|
>
|
||||||
|
<img :src="attachment.thumb_url">
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||||
|
<div v-html="attachment.oembed.oembedHTML" />
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="type === 'flash' && !hidden"
|
||||||
|
class="flash-container"
|
||||||
|
:href="attachment.url"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<Flash
|
||||||
|
ref="flash"
|
||||||
|
class="flash"
|
||||||
|
:src="attachment.large_thumb_url || attachment.url"
|
||||||
|
@playerOpened="setFlashLoaded(true)"
|
||||||
|
@playerClosed="setFlashLoaded(false)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||||
|
class="description-container"
|
||||||
|
:class="{ '-static': !edit }"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-if="edit"
|
||||||
|
v-model="localDescription"
|
||||||
|
class="description-field"
|
||||||
|
:placeholder="$t('post_status.media_description')"
|
||||||
|
rows="1"
|
||||||
|
cols="1"
|
||||||
|
@keydown.ctrl.enter="postStatus()"
|
||||||
|
@input="resize"
|
||||||
|
/>
|
||||||
|
<p v-else>
|
||||||
|
{{ localDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./attachment.js"></script>
|
<script src="./attachment.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style src="./attachment.scss" lang="scss"></style>
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.attachments {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.non-gallery {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.3em 1em 0.3em 0;
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--postLink, $fallback--link);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nsfw-placeholder {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
cursor: progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
align-self: flex-start;
|
|
||||||
line-height: 0;
|
|
||||||
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: $fallback--attachmentRadius;
|
|
||||||
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
|
||||||
border-color: $fallback--border;
|
|
||||||
border-color: var(--border, $fallback--border);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.non-gallery.attachment {
|
|
||||||
&.flash,
|
|
||||||
&.video {
|
|
||||||
flex: 1 0 40%;
|
|
||||||
}
|
|
||||||
.nsfw {
|
|
||||||
height: 260px;
|
|
||||||
}
|
|
||||||
.small {
|
|
||||||
height: 120px;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
.video {
|
|
||||||
height: 260px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
video {
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullwidth {
|
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
// fixes small gap below video
|
|
||||||
&.video {
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container {
|
|
||||||
display: flex;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon {
|
|
||||||
position: absolute;
|
|
||||||
font-size: 64px;
|
|
||||||
top: calc(50% - 32px);
|
|
||||||
left: calc(50% - 32px);
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon::before {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.html {
|
|
||||||
flex-basis: 90%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hider {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
margin: 10px;
|
|
||||||
padding: 0;
|
|
||||||
z-index: 4;
|
|
||||||
border-radius: $fallback--tooltipRadius;
|
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
|
||||||
text-align: center;
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
font-size: 1.25em;
|
|
||||||
// TODO: theming? hard to theme with unknown background image color
|
|
||||||
background: rgba(230, 230, 230, 0.7);
|
|
||||||
.svg-inline--fa {
|
|
||||||
color: rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
|
||||||
&:hover .svg-inline--fa {
|
|
||||||
color: rgba(0, 0, 0, 0.9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.media-upload {
|
|
||||||
line-height: 0;
|
|
||||||
max-height: 200px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oembed {
|
|
||||||
line-height: 1.2em;
|
|
||||||
flex: 1 0 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin-right: 15px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
flex: 1;
|
|
||||||
img {
|
|
||||||
border: 0px;
|
|
||||||
border-radius: 5px;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex: 2;
|
|
||||||
margin: 8px;
|
|
||||||
word-break: break-all;
|
|
||||||
h1 {
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-attachment {
|
|
||||||
&,
|
|
||||||
& .image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nsfw {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
image-orientation: from-image; // NOTE: only FF supports this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { h, resolveComponent } from 'vue'
|
||||||
import LoginForm from '../login_form/login_form.vue'
|
import LoginForm from '../login_form/login_form.vue'
|
||||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||||
@@ -5,8 +6,8 @@ import { mapGetters } from 'vuex'
|
|||||||
|
|
||||||
const AuthForm = {
|
const AuthForm = {
|
||||||
name: 'AuthForm',
|
name: 'AuthForm',
|
||||||
render (createElement) {
|
render () {
|
||||||
return createElement('component', { is: this.authForm })
|
return h(resolveComponent(this.authForm))
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
authForm () {
|
authForm () {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<UserAvatar
|
<UserAvatar
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:user="user"
|
:user="user"
|
||||||
@click.prevent.native="toggleUserExpanded"
|
@click.prevent="toggleUserExpanded"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Bookmarks = {
|
|||||||
components: {
|
components: {
|
||||||
Timeline
|
Timeline
|
||||||
},
|
},
|
||||||
destroyed () {
|
unmounted () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import Timeline from '../timeline/timeline.vue'
|
||||||
|
const PublicTimeline = {
|
||||||
|
components: {
|
||||||
|
Timeline
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||||
|
},
|
||||||
|
unmounted () {
|
||||||
|
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublicTimeline
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<Timeline
|
||||||
|
:title="$t('nav.bubble_timeline')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'bubble'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./bubble_timeline.js"></script>
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
import _ from 'lodash'
|
|
||||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
|
||||||
import { mapGetters, mapState } from 'vuex'
|
|
||||||
import ChatMessage from '../chat_message/chat_message.vue'
|
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
|
||||||
import ChatTitle from '../chat_title/chat_title.vue'
|
|
||||||
import chatService from '../../services/chat_service/chat_service.js'
|
|
||||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
|
||||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {
|
|
||||||
faChevronDown,
|
|
||||||
faChevronLeft
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faChevronDown,
|
|
||||||
faChevronLeft
|
|
||||||
)
|
|
||||||
|
|
||||||
const BOTTOMED_OUT_OFFSET = 10
|
|
||||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
|
||||||
const SAFE_RESIZE_TIME_OFFSET = 100
|
|
||||||
const MARK_AS_READ_DELAY = 1500
|
|
||||||
const MAX_RETRIES = 10
|
|
||||||
|
|
||||||
const Chat = {
|
|
||||||
components: {
|
|
||||||
ChatMessage,
|
|
||||||
ChatTitle,
|
|
||||||
PostStatusForm
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
jumpToBottomButtonVisible: false,
|
|
||||||
hoveredMessageChainId: undefined,
|
|
||||||
lastScrollPosition: {},
|
|
||||||
scrollableContainerHeight: '100%',
|
|
||||||
errorLoadingChat: false,
|
|
||||||
messageRetriers: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.startFetching()
|
|
||||||
window.addEventListener('resize', this.handleLayoutChange)
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
window.addEventListener('scroll', this.handleScroll)
|
|
||||||
if (typeof document.hidden !== 'undefined') {
|
|
||||||
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
this.handleResize()
|
|
||||||
})
|
|
||||||
this.setChatLayout()
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
window.removeEventListener('scroll', this.handleScroll)
|
|
||||||
window.removeEventListener('resize', this.handleLayoutChange)
|
|
||||||
this.unsetChatLayout()
|
|
||||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
|
||||||
this.$store.dispatch('clearCurrentChat')
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
recipient () {
|
|
||||||
return this.currentChat && this.currentChat.account
|
|
||||||
},
|
|
||||||
recipientId () {
|
|
||||||
return this.$route.params.recipient_id
|
|
||||||
},
|
|
||||||
formPlaceholder () {
|
|
||||||
if (this.recipient) {
|
|
||||||
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
|
|
||||||
} else {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
chatViewItems () {
|
|
||||||
return chatService.getView(this.currentChatMessageService)
|
|
||||||
},
|
|
||||||
newMessageCount () {
|
|
||||||
return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
|
|
||||||
},
|
|
||||||
streamingEnabled () {
|
|
||||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
|
||||||
},
|
|
||||||
...mapGetters([
|
|
||||||
'currentChat',
|
|
||||||
'currentChatMessageService',
|
|
||||||
'findOpenedChatByRecipientId',
|
|
||||||
'mergedConfig'
|
|
||||||
]),
|
|
||||||
...mapState({
|
|
||||||
backendInteractor: state => state.api.backendInteractor,
|
|
||||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
|
|
||||||
mobileLayout: state => state.interface.mobileLayout,
|
|
||||||
layoutHeight: state => state.interface.layoutHeight,
|
|
||||||
currentUser: state => state.users.currentUser
|
|
||||||
})
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
chatViewItems () {
|
|
||||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
|
||||||
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
|
||||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (bottomedOutBeforeUpdate) {
|
|
||||||
this.scrollDown()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
'$route': function () {
|
|
||||||
this.startFetching()
|
|
||||||
},
|
|
||||||
layoutHeight () {
|
|
||||||
this.handleResize({ expand: true })
|
|
||||||
},
|
|
||||||
mastoUserSocketStatus (newValue) {
|
|
||||||
if (newValue === WSConnectionStatus.JOINED) {
|
|
||||||
this.fetchChat({ isFirstFetch: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
|
|
||||||
onMessageHover ({ isHovered, messageChainId }) {
|
|
||||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
|
||||||
},
|
|
||||||
onFilesDropped () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.handleResize()
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleVisibilityChange () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
|
||||||
this.scrollDown({ forceRead: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setChatLayout () {
|
|
||||||
// This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
|
|
||||||
// This layout prevents empty spaces from being visible at the bottom
|
|
||||||
// of the chat on iOS Safari (`safe-area-inset`) when
|
|
||||||
// - the on-screen keyboard appears and the user starts typing
|
|
||||||
// - the user selects the text inside the input area
|
|
||||||
// - the user selects and deletes the text that is multiple lines long
|
|
||||||
// TODO: unify the chat layout with the global layout.
|
|
||||||
let html = document.querySelector('html')
|
|
||||||
if (html) {
|
|
||||||
html.classList.add('chat-layout')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
unsetChatLayout () {
|
|
||||||
let html = document.querySelector('html')
|
|
||||||
if (html) {
|
|
||||||
html.classList.remove('chat-layout')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleLayoutChange () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
this.scrollDown()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
|
|
||||||
updateScrollableContainerHeight () {
|
|
||||||
const header = this.$refs.header
|
|
||||||
const footer = this.$refs.footer
|
|
||||||
const inner = this.mobileLayout ? window.document.body : this.$refs.inner
|
|
||||||
this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
|
|
||||||
},
|
|
||||||
// Preserves the scroll position when OSK appears or the posting form changes its height.
|
|
||||||
handleResize (opts = {}) {
|
|
||||||
const { expand = false, delayed = false } = opts
|
|
||||||
|
|
||||||
if (delayed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.handleResize({ ...opts, delayed: false })
|
|
||||||
}, SAFE_RESIZE_TIME_OFFSET)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
|
|
||||||
const { offsetHeight = undefined } = this.lastScrollPosition
|
|
||||||
this.lastScrollPosition = getScrollPosition(this.$refs.scrollable)
|
|
||||||
|
|
||||||
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
|
||||||
if (diff < 0 || (!this.bottomedOut() && expand)) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
this.$refs.scrollable.scrollTo({
|
|
||||||
top: this.$refs.scrollable.scrollTop - diff,
|
|
||||||
left: 0
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
scrollDown (options = {}) {
|
|
||||||
const { behavior = 'auto', forceRead = false } = options
|
|
||||||
const scrollable = this.$refs.scrollable
|
|
||||||
if (!scrollable) { return }
|
|
||||||
this.$nextTick(() => {
|
|
||||||
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
|
|
||||||
})
|
|
||||||
if (forceRead) {
|
|
||||||
this.readChat()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
readChat () {
|
|
||||||
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
|
||||||
if (document.hidden) { return }
|
|
||||||
const lastReadId = this.currentChatMessageService.maxId
|
|
||||||
this.$store.dispatch('readChat', {
|
|
||||||
id: this.currentChat.id,
|
|
||||||
lastReadId
|
|
||||||
})
|
|
||||||
},
|
|
||||||
bottomedOut (offset) {
|
|
||||||
return isBottomedOut(this.$refs.scrollable, offset)
|
|
||||||
},
|
|
||||||
reachedTop () {
|
|
||||||
const scrollable = this.$refs.scrollable
|
|
||||||
return scrollable && scrollable.scrollTop <= 0
|
|
||||||
},
|
|
||||||
cullOlderCheck () {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
|
||||||
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
},
|
|
||||||
handleScroll: _.throttle(function () {
|
|
||||||
if (!this.currentChat) { return }
|
|
||||||
|
|
||||||
if (this.reachedTop()) {
|
|
||||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
|
||||||
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
|
||||||
this.jumpToBottomButtonVisible = false
|
|
||||||
this.cullOlderCheck()
|
|
||||||
if (this.newMessageCount > 0) {
|
|
||||||
// Use a delay before marking as read to prevent situation where new messages
|
|
||||||
// arrive just as you're leaving the view and messages that you didn't actually
|
|
||||||
// get to see get marked as read.
|
|
||||||
window.setTimeout(() => {
|
|
||||||
// Don't mark as read if the element doesn't exist, user has left chat view
|
|
||||||
if (this.$el) this.readChat()
|
|
||||||
}, MARK_AS_READ_DELAY)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.jumpToBottomButtonVisible = true
|
|
||||||
}
|
|
||||||
}, 200),
|
|
||||||
handleScrollUp (positionBeforeLoading) {
|
|
||||||
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
|
|
||||||
this.$refs.scrollable.scrollTo({
|
|
||||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
|
||||||
left: 0
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
|
||||||
const chatMessageService = this.currentChatMessageService
|
|
||||||
if (!chatMessageService) { return }
|
|
||||||
if (fetchLatest && this.streamingEnabled) { return }
|
|
||||||
|
|
||||||
const chatId = chatMessageService.chatId
|
|
||||||
const fetchOlderMessages = !!maxId
|
|
||||||
const sinceId = fetchLatest && chatMessageService.maxId
|
|
||||||
|
|
||||||
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
|
||||||
.then((messages) => {
|
|
||||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
|
||||||
if (isFirstFetch) {
|
|
||||||
chatService.clear(chatMessageService)
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable)
|
|
||||||
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (fetchOlderMessages) {
|
|
||||||
this.handleScrollUp(positionBeforeUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstFetch) {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
// In vertical screens, the first batch of fetched messages may not always take the
|
|
||||||
// full height of the scrollable container.
|
|
||||||
// If this is the case, we want to fetch the messages until the scrollable container
|
|
||||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
|
||||||
if (!isScrollable(this.$refs.scrollable) && messages.length > 0) {
|
|
||||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async startFetching () {
|
|
||||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
|
||||||
if (!chat) {
|
|
||||||
try {
|
|
||||||
chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error creating or getting a chat', e)
|
|
||||||
this.errorLoadingChat = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (chat) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollDown({ forceRead: true })
|
|
||||||
})
|
|
||||||
this.$store.dispatch('addOpenedChat', { chat })
|
|
||||||
this.doStartFetching()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doStartFetching () {
|
|
||||||
this.$store.dispatch('startFetchingCurrentChat', {
|
|
||||||
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
|
||||||
})
|
|
||||||
this.fetchChat({ isFirstFetch: true })
|
|
||||||
},
|
|
||||||
handleAttachmentPosting () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.handleResize()
|
|
||||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
|
||||||
// to account for the potential delay in the DOM update.
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
}, SAFE_RESIZE_TIME_OFFSET)
|
|
||||||
this.scrollDown({ forceRead: true })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
sendMessage ({ status, media, idempotencyKey }) {
|
|
||||||
const params = {
|
|
||||||
id: this.currentChat.id,
|
|
||||||
content: status,
|
|
||||||
idempotencyKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media[0]) {
|
|
||||||
params.mediaId = media[0].id
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeMessage = buildFakeMessage({
|
|
||||||
attachments: media,
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
content: status,
|
|
||||||
userId: this.currentUser.id,
|
|
||||||
idempotencyKey
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$store.dispatch('addChatMessages', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
messages: [fakeMessage]
|
|
||||||
}).then(() => {
|
|
||||||
this.handleAttachmentPosting()
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
|
|
||||||
},
|
|
||||||
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
|
||||||
if (retriesLeft <= 0) return
|
|
||||||
|
|
||||||
this.backendInteractor.sendChatMessage(params)
|
|
||||||
.then(data => {
|
|
||||||
this.$store.dispatch('addChatMessages', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
updateMaxId: false,
|
|
||||||
messages: [{ ...data, fakeId: fakeMessage.id }]
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error sending message', error)
|
|
||||||
this.$store.dispatch('handleMessageError', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
fakeId: fakeMessage.id,
|
|
||||||
isRetry: retriesLeft !== MAX_RETRIES
|
|
||||||
})
|
|
||||||
if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
|
|
||||||
this.messageRetriers[fakeMessage.id] = setTimeout(() => {
|
|
||||||
this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
|
|
||||||
}, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Promise.resolve(fakeMessage)
|
|
||||||
},
|
|
||||||
goBack () {
|
|
||||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Chat
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
.chat-view {
|
|
||||||
display: flex;
|
|
||||||
height: calc(100vh - 60px);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.chat-title {
|
|
||||||
// prevents chat header jumping on when the user avatar loads
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-view-inner {
|
|
||||||
height: auto;
|
|
||||||
width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
display: flex;
|
|
||||||
margin: 0.5em 0.5em 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-view-body {
|
|
||||||
background-color: var(--chatBg, $fallback--bg);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
min-height: 100%;
|
|
||||||
margin: 0 0 0 0;
|
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable-message-list {
|
|
||||||
padding: 0 0.8em;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-view-heading {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
top: 50px;
|
|
||||||
display: flex;
|
|
||||||
z-index: 2;
|
|
||||||
position: sticky;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-back-button {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 28px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.6em;
|
|
||||||
margin: -0.6em 0.6em -0.6em -0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jump-to-bottom-button {
|
|
||||||
width: 2.5em;
|
|
||||||
height: 2.5em;
|
|
||||||
border-radius: 100%;
|
|
||||||
position: absolute;
|
|
||||||
right: 1.3em;
|
|
||||||
top: -3.2em;
|
|
||||||
background-color: $fallback--fg;
|
|
||||||
background-color: var(--btn, $fallback--fg);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
z-index: 10;
|
|
||||||
transition: 0.35s all;
|
|
||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.visible {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 1em;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--text, $fallback--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.unread-message-count {
|
|
||||||
font-size: 0.8em;
|
|
||||||
left: 50%;
|
|
||||||
margin-top: -1rem;
|
|
||||||
padding: 0.1em;
|
|
||||||
border-radius: 50px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-loading-error {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.error {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 800px) {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.chat-view-inner {
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-view-body {
|
|
||||||
display: flex;
|
|
||||||
min-height: auto;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-view-heading {
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: static;
|
|
||||||
z-index: 9999;
|
|
||||||
top: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
/* This practically overlays the panel heading color over panel background
|
|
||||||
* color. This is needed because we allow transparent panel background and
|
|
||||||
* it doesn't work well in this "disjointed panel header" case
|
|
||||||
*/
|
|
||||||
background:
|
|
||||||
linear-gradient(to top, var(--panel), var(--panel)),
|
|
||||||
linear-gradient(to top, var(--bg), var(--bg));
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable-message-list {
|
|
||||||
display: unset;
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
position: sticky;
|
|
||||||
bottom: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="chat-view">
|
|
||||||
<div class="chat-view-inner">
|
|
||||||
<div
|
|
||||||
id="nav"
|
|
||||||
ref="inner"
|
|
||||||
class="panel-default panel chat-view-body"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="header"
|
|
||||||
class="panel-heading chat-view-heading mobile-hidden"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="go-back-button"
|
|
||||||
@click="goBack"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
size="lg"
|
|
||||||
icon="chevron-left"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<div class="title text-center">
|
|
||||||
<ChatTitle
|
|
||||||
:user="recipient"
|
|
||||||
:with-avatar="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="scrollable"
|
|
||||||
class="scrollable-message-list"
|
|
||||||
:style="{ height: scrollableContainerHeight }"
|
|
||||||
@scroll="handleScroll"
|
|
||||||
>
|
|
||||||
<template v-if="!errorLoadingChat">
|
|
||||||
<ChatMessage
|
|
||||||
v-for="chatViewItem in chatViewItems"
|
|
||||||
:key="chatViewItem.id"
|
|
||||||
:author="recipient"
|
|
||||||
:chat-view-item="chatViewItem"
|
|
||||||
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
|
|
||||||
@hover="onMessageHover"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="chat-loading-error"
|
|
||||||
>
|
|
||||||
<div class="alert error">
|
|
||||||
{{ $t('chats.error_loading_chat') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref="footer"
|
|
||||||
class="panel-body footer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="jump-to-bottom-button"
|
|
||||||
:class="{ 'visible': jumpToBottomButtonVisible }"
|
|
||||||
@click="scrollDown({ behavior: 'smooth' })"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<FAIcon icon="chevron-down" />
|
|
||||||
<div
|
|
||||||
v-if="newMessageCount"
|
|
||||||
class="badge badge-notification unread-chat-count unread-message-count"
|
|
||||||
>
|
|
||||||
{{ newMessageCount }}
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<PostStatusForm
|
|
||||||
:disable-subject="true"
|
|
||||||
:disable-scope-selector="true"
|
|
||||||
:disable-notice="true"
|
|
||||||
:disable-lock-warning="true"
|
|
||||||
:disable-polls="true"
|
|
||||||
:disable-sensitivity-checkbox="true"
|
|
||||||
:disable-submit="errorLoadingChat || !currentChat"
|
|
||||||
:disable-preview="true"
|
|
||||||
:optimistic-posting="true"
|
|
||||||
:post-handler="sendMessage"
|
|
||||||
:submit-on-enter="!mobileLayout"
|
|
||||||
:preserve-focus="!mobileLayout"
|
|
||||||
:auto-focus="!mobileLayout"
|
|
||||||
:placeholder="formPlaceholder"
|
|
||||||
:file-limit="1"
|
|
||||||
max-height="160"
|
|
||||||
emoji-picker-placement="top"
|
|
||||||
@resize="handleResize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat.js"></script>
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
@import './chat.scss';
|
|
||||||
</style>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Captures a scroll position
|
|
||||||
export const getScrollPosition = (el) => {
|
|
||||||
return {
|
|
||||||
scrollTop: el.scrollTop,
|
|
||||||
scrollHeight: el.scrollHeight,
|
|
||||||
offsetHeight: el.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
|
|
||||||
// Takes two scroll positions, before and after the update.
|
|
||||||
export const getNewTopPosition = (previousPosition, newPosition) => {
|
|
||||||
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isBottomedOut = (el, offset = 0) => {
|
|
||||||
if (!el) { return }
|
|
||||||
const scrollHeight = el.scrollTop + offset
|
|
||||||
const totalHeight = el.scrollHeight - el.offsetHeight
|
|
||||||
return totalHeight <= scrollHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form.
|
|
||||||
export const scrollableContainerHeight = (inner, header, footer) => {
|
|
||||||
return inner.offsetHeight - header.clientHeight - footer.clientHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns whether or not the scrollbar is visible.
|
|
||||||
export const isScrollable = (el) => {
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
return el.scrollHeight > el.clientHeight
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { mapState, mapGetters } from 'vuex'
|
|
||||||
import ChatListItem from '../chat_list_item/chat_list_item.vue'
|
|
||||||
import ChatNew from '../chat_new/chat_new.vue'
|
|
||||||
import List from '../list/list.vue'
|
|
||||||
|
|
||||||
const ChatList = {
|
|
||||||
components: {
|
|
||||||
ChatListItem,
|
|
||||||
List,
|
|
||||||
ChatNew
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
currentUser: state => state.users.currentUser
|
|
||||||
}),
|
|
||||||
...mapGetters(['sortedChatList'])
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isNew: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.$store.dispatch('fetchChats', { latest: true })
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
cancelNewChat () {
|
|
||||||
this.isNew = false
|
|
||||||
this.$store.dispatch('fetchChats', { latest: true })
|
|
||||||
},
|
|
||||||
newChat () {
|
|
||||||
this.isNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChatList
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="isNew">
|
|
||||||
<ChatNew @cancel="cancelNewChat" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="chat-list panel panel-default"
|
|
||||||
>
|
|
||||||
<div class="panel-heading">
|
|
||||||
<span class="title">
|
|
||||||
{{ $t("chats.chats") }}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="button-default"
|
|
||||||
@click="newChat"
|
|
||||||
>
|
|
||||||
{{ $t("chats.new") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div
|
|
||||||
v-if="sortedChatList.length > 0"
|
|
||||||
class="timeline"
|
|
||||||
>
|
|
||||||
<List :items="sortedChatList">
|
|
||||||
<template v-slot:item="{item}">
|
|
||||||
<ChatListItem
|
|
||||||
:key="item.id"
|
|
||||||
:compact="false"
|
|
||||||
:chat="item"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="emtpy-chat-list-alert"
|
|
||||||
>
|
|
||||||
<span>{{ $t('chats.empty_chat_list_placeholder') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_list.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.chat-list {
|
|
||||||
min-height: 25em;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emtpy-chat-list-alert {
|
|
||||||
padding: 3em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--faint, $fallback--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { mapState } from 'vuex'
|
|
||||||
import StatusBody from '../status_content/status_content.vue'
|
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
|
||||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
|
||||||
import Timeago from '../timeago/timeago.vue'
|
|
||||||
import ChatTitle from '../chat_title/chat_title.vue'
|
|
||||||
|
|
||||||
const ChatListItem = {
|
|
||||||
name: 'ChatListItem',
|
|
||||||
props: [
|
|
||||||
'chat'
|
|
||||||
],
|
|
||||||
components: {
|
|
||||||
UserAvatar,
|
|
||||||
AvatarList,
|
|
||||||
Timeago,
|
|
||||||
ChatTitle,
|
|
||||||
StatusBody
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
currentUser: state => state.users.currentUser
|
|
||||||
}),
|
|
||||||
attachmentInfo () {
|
|
||||||
if (this.chat.lastMessage.attachments.length === 0) { return }
|
|
||||||
|
|
||||||
const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
|
|
||||||
if (types.includes('video')) {
|
|
||||||
return this.$t('file_type.video')
|
|
||||||
} else if (types.includes('audio')) {
|
|
||||||
return this.$t('file_type.audio')
|
|
||||||
} else if (types.includes('image')) {
|
|
||||||
return this.$t('file_type.image')
|
|
||||||
} else {
|
|
||||||
return this.$t('file_type.file')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
messageForStatusContent () {
|
|
||||||
const message = this.chat.lastMessage
|
|
||||||
const messageEmojis = message ? message.emojis : []
|
|
||||||
const isYou = message && message.account_id === this.currentUser.id
|
|
||||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
|
||||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
|
||||||
return {
|
|
||||||
summary: '',
|
|
||||||
emojis: messageEmojis,
|
|
||||||
raw_html: messagePreview,
|
|
||||||
text: messagePreview,
|
|
||||||
attachments: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openChat (_e) {
|
|
||||||
if (this.chat.id) {
|
|
||||||
this.$router.push({
|
|
||||||
name: 'chat',
|
|
||||||
params: {
|
|
||||||
username: this.currentUser.screen_name,
|
|
||||||
recipient_id: this.chat.account.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChatListItem
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
.chat-list-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 0.75em;
|
|
||||||
height: 5em;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--selectedPost, $fallback--lightBg);
|
|
||||||
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-list-item-left {
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-list-item-center {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading {
|
|
||||||
width: 100%;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading-right {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-and-account-name {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 1;
|
|
||||||
line-height: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-preview {
|
|
||||||
display: inline-flex;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
margin: 0.35em 0;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--faint, $fallback--text);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--faintLink, $fallback--link);
|
|
||||||
text-decoration: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .animated.avatar {
|
|
||||||
canvas {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Avatar {
|
|
||||||
border-radius: $fallback--avatarAltRadius;
|
|
||||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-preview-body {
|
|
||||||
--emoji-size: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-wrapper {
|
|
||||||
line-height: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-preview-body {
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="chat-list-item"
|
|
||||||
@click.capture.prevent="openChat"
|
|
||||||
>
|
|
||||||
<div class="chat-list-item-left">
|
|
||||||
<UserAvatar
|
|
||||||
:user="chat.account"
|
|
||||||
height="48px"
|
|
||||||
width="48px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="chat-list-item-center">
|
|
||||||
<div class="heading">
|
|
||||||
<span
|
|
||||||
v-if="chat.account"
|
|
||||||
class="name-and-account-name"
|
|
||||||
>
|
|
||||||
<ChatTitle
|
|
||||||
:user="chat.account"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="heading-right" />
|
|
||||||
<div class="time-wrapper">
|
|
||||||
<Timeago
|
|
||||||
:time="chat.updated_at"
|
|
||||||
:auto-update="60"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-preview">
|
|
||||||
<StatusBody
|
|
||||||
class="chat-preview-body"
|
|
||||||
:status="messageForStatusContent"
|
|
||||||
:single-line="true"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="chat.unread > 0"
|
|
||||||
class="badge badge-notification unread-chat-count"
|
|
||||||
>
|
|
||||||
{{ chat.unread }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_list_item.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
@import './chat_list_item.scss';
|
|
||||||
</style>
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { mapState, mapGetters } from 'vuex'
|
|
||||||
import Popover from '../popover/popover.vue'
|
|
||||||
import Attachment from '../attachment/attachment.vue'
|
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
|
||||||
import Gallery from '../gallery/gallery.vue'
|
|
||||||
import LinkPreview from '../link-preview/link-preview.vue'
|
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
|
||||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {
|
|
||||||
faTimes,
|
|
||||||
faEllipsisH
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faTimes,
|
|
||||||
faEllipsisH
|
|
||||||
)
|
|
||||||
|
|
||||||
const ChatMessage = {
|
|
||||||
name: 'ChatMessage',
|
|
||||||
props: [
|
|
||||||
'author',
|
|
||||||
'edited',
|
|
||||||
'noHeading',
|
|
||||||
'chatViewItem',
|
|
||||||
'hoveredMessageChain'
|
|
||||||
],
|
|
||||||
components: {
|
|
||||||
Popover,
|
|
||||||
Attachment,
|
|
||||||
StatusContent,
|
|
||||||
UserAvatar,
|
|
||||||
Gallery,
|
|
||||||
LinkPreview,
|
|
||||||
ChatMessageDate
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
// Returns HH:MM (hours and minutes) in local time.
|
|
||||||
createdAt () {
|
|
||||||
const time = this.chatViewItem.data.created_at
|
|
||||||
return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
|
|
||||||
},
|
|
||||||
isCurrentUser () {
|
|
||||||
return this.message.account_id === this.currentUser.id
|
|
||||||
},
|
|
||||||
message () {
|
|
||||||
return this.chatViewItem.data
|
|
||||||
},
|
|
||||||
userProfileLink () {
|
|
||||||
return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
|
|
||||||
},
|
|
||||||
isMessage () {
|
|
||||||
return this.chatViewItem.type === 'message'
|
|
||||||
},
|
|
||||||
messageForStatusContent () {
|
|
||||||
return {
|
|
||||||
summary: '',
|
|
||||||
emojis: this.message.emojis,
|
|
||||||
raw_html: this.message.content || '',
|
|
||||||
text: this.message.content || '',
|
|
||||||
attachments: this.message.attachments
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hasAttachment () {
|
|
||||||
return this.message.attachments.length > 0
|
|
||||||
},
|
|
||||||
...mapState({
|
|
||||||
betterShadow: state => state.interface.browserSupport.cssFilter,
|
|
||||||
currentUser: state => state.users.currentUser,
|
|
||||||
restrictedNicknames: state => state.instance.restrictedNicknames
|
|
||||||
}),
|
|
||||||
popoverMarginStyle () {
|
|
||||||
if (this.isCurrentUser) {
|
|
||||||
return {}
|
|
||||||
} else {
|
|
||||||
return { left: 50 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...mapGetters(['mergedConfig', 'findUser'])
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
hovered: false,
|
|
||||||
menuOpened: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onHover (bool) {
|
|
||||||
this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId })
|
|
||||||
},
|
|
||||||
async deleteMessage () {
|
|
||||||
const confirmed = window.confirm(this.$t('chats.delete_confirm'))
|
|
||||||
if (confirmed) {
|
|
||||||
await this.$store.dispatch('deleteChatMessage', {
|
|
||||||
messageId: this.chatViewItem.data.id,
|
|
||||||
chatId: this.chatViewItem.data.chat_id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.hovered = false
|
|
||||||
this.menuOpened = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChatMessage
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.chat-message-wrapper {
|
|
||||||
&.hovered-message-chain {
|
|
||||||
.animated.Avatar {
|
|
||||||
canvas {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-menu {
|
|
||||||
transition: opacity 0.1s;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: -0.8em;
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding-top: 0.2em;
|
|
||||||
padding-bottom: 0.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover, .extra-button-popover.open & {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--text, $fallback--text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover {
|
|
||||||
width: 12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message {
|
|
||||||
display: flex;
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-wrapper {
|
|
||||||
margin-right: 0.72em;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-preview, .attachments {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
max-width: 80%;
|
|
||||||
min-width: 10em;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&.with-media {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.gallery-row {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
border-radius: $fallback--chatMessageRadius;
|
|
||||||
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
|
||||||
display: flex;
|
|
||||||
padding: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.created-at {
|
|
||||||
position: relative;
|
|
||||||
float: right;
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin: -1em 0 -0.5em 0;
|
|
||||||
font-style: italic;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.without-attachment {
|
|
||||||
.message-content {
|
|
||||||
// TODO figure out how to do it properly
|
|
||||||
.RichContent::after {
|
|
||||||
margin-right: 5.4em;
|
|
||||||
content: " ";
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending {
|
|
||||||
.status-content.media-body, .created-at {
|
|
||||||
color: var(--faint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
.status-content.media-body, .created-at {
|
|
||||||
color: $fallback--cRed;
|
|
||||||
color: var(--badgeNotification, $fallback--cRed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.incoming {
|
|
||||||
a {
|
|
||||||
color: var(--chatMessageIncomingLink, $fallback--link);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
color: var(--chatMessageIncomingText, $fallback--text);
|
|
||||||
background-color: var(--chatMessageIncomingBg, $fallback--bg);
|
|
||||||
border: 1px solid var(--chatMessageIncomingBorder, --border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.created-at {
|
|
||||||
a {
|
|
||||||
color: var(--chatMessageIncomingText, $fallback--text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-menu {
|
|
||||||
left: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.outgoing {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-content: end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--chatMessageOutgoingLink, $fallback--link);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
color: var(--chatMessageOutgoingText, $fallback--text);
|
|
||||||
background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);
|
|
||||||
border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-inner {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-menu {
|
|
||||||
right: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-date-separator {
|
|
||||||
text-align: center;
|
|
||||||
margin: 1.4em 0;
|
|
||||||
font-size: 0.9em;
|
|
||||||
user-select: none;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--faintedText, $fallback--text);
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="isMessage"
|
|
||||||
class="chat-message-wrapper"
|
|
||||||
:class="{ 'hovered-message-chain': hoveredMessageChain }"
|
|
||||||
@mouseover="onHover(true)"
|
|
||||||
@mouseleave="onHover(false)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="chat-message"
|
|
||||||
:class="[{ 'outgoing': isCurrentUser, 'incoming': !isCurrentUser }]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!isCurrentUser"
|
|
||||||
class="avatar-wrapper"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-if="chatViewItem.isHead"
|
|
||||||
:to="userProfileLink"
|
|
||||||
>
|
|
||||||
<UserAvatar
|
|
||||||
:compact="true"
|
|
||||||
:better-shadow="betterShadow"
|
|
||||||
:user="author"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="chat-message-inner">
|
|
||||||
<div
|
|
||||||
class="status-body"
|
|
||||||
:style="{ 'min-width': message.attachment ? '80%' : '' }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="media status"
|
|
||||||
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
|
|
||||||
style="position: relative"
|
|
||||||
@mouseenter="hovered = true"
|
|
||||||
@mouseleave="hovered = false"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="chat-message-menu"
|
|
||||||
:class="{ 'visible': hovered || menuOpened }"
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
trigger="click"
|
|
||||||
placement="top"
|
|
||||||
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
|
|
||||||
:bound-to="{ x: 'container' }"
|
|
||||||
:margin="popoverMarginStyle"
|
|
||||||
@show="menuOpened = true"
|
|
||||||
@close="menuOpened = false"
|
|
||||||
>
|
|
||||||
<template v-slot:content>
|
|
||||||
<div class="dropdown-menu">
|
|
||||||
<button
|
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
|
||||||
@click="deleteMessage"
|
|
||||||
>
|
|
||||||
<FAIcon icon="times" /> {{ $t("chats.delete") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-slot:trigger>
|
|
||||||
<button
|
|
||||||
class="button-default menu-icon"
|
|
||||||
:title="$t('chats.more')"
|
|
||||||
>
|
|
||||||
<FAIcon icon="ellipsis-h" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<StatusContent
|
|
||||||
class="message-content"
|
|
||||||
:status="messageForStatusContent"
|
|
||||||
:full-content="true"
|
|
||||||
>
|
|
||||||
<template v-slot:footer>
|
|
||||||
<span
|
|
||||||
class="created-at"
|
|
||||||
>
|
|
||||||
{{ createdAt }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</StatusContent>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="chat-message-date-separator"
|
|
||||||
>
|
|
||||||
<ChatMessageDate :date="chatViewItem.date" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_message.js" ></script>
|
|
||||||
<style lang="scss">
|
|
||||||
@import './chat_message.scss';
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { mapState, mapGetters } from 'vuex'
|
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {
|
|
||||||
faSearch,
|
|
||||||
faChevronLeft
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faSearch,
|
|
||||||
faChevronLeft
|
|
||||||
)
|
|
||||||
|
|
||||||
const chatNew = {
|
|
||||||
components: {
|
|
||||||
BasicUserCard,
|
|
||||||
UserAvatar
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
suggestions: [],
|
|
||||||
userIds: [],
|
|
||||||
loading: false,
|
|
||||||
query: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created () {
|
|
||||||
const { chats } = await this.backendInteractor.chats()
|
|
||||||
chats.forEach(chat => this.suggestions.push(chat.account))
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
users () {
|
|
||||||
return this.userIds.map(userId => this.findUser(userId))
|
|
||||||
},
|
|
||||||
availableUsers () {
|
|
||||||
if (this.query.length !== 0) {
|
|
||||||
return this.users
|
|
||||||
} else {
|
|
||||||
return this.suggestions
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...mapState({
|
|
||||||
currentUser: state => state.users.currentUser,
|
|
||||||
backendInteractor: state => state.api.backendInteractor
|
|
||||||
}),
|
|
||||||
...mapGetters(['findUser'])
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
goBack () {
|
|
||||||
this.$emit('cancel')
|
|
||||||
},
|
|
||||||
goToChat (user) {
|
|
||||||
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
|
|
||||||
},
|
|
||||||
onInput () {
|
|
||||||
this.search(this.query)
|
|
||||||
},
|
|
||||||
addUser (user) {
|
|
||||||
this.selectedUserIds.push(user.id)
|
|
||||||
this.query = ''
|
|
||||||
},
|
|
||||||
removeUser (userId) {
|
|
||||||
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
|
||||||
},
|
|
||||||
search (query) {
|
|
||||||
if (!query) {
|
|
||||||
this.loading = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true
|
|
||||||
this.userIds = []
|
|
||||||
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
|
|
||||||
.then(data => {
|
|
||||||
this.loading = false
|
|
||||||
this.userIds = data.accounts.map(a => a.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default chatNew
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
.chat-new {
|
|
||||||
.input-wrap {
|
|
||||||
display: flex;
|
|
||||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
margin-right: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-list {
|
|
||||||
padding-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.basic-user-card:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--selectedPost, $fallback--lightBg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-back-button {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 28px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.6em;
|
|
||||||
margin: -0.6em 0.6em -0.6em -0.6em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
id="nav"
|
|
||||||
class="panel-default panel chat-new"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="header"
|
|
||||||
class="panel-heading"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="go-back-button"
|
|
||||||
@click="goBack"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
size="lg"
|
|
||||||
icon="chevron-left"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="input-wrap">
|
|
||||||
<div class="input-search">
|
|
||||||
<FAIcon
|
|
||||||
class="search-icon fa-scale-110 fa-old-padding"
|
|
||||||
icon="search"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref="search"
|
|
||||||
v-model="query"
|
|
||||||
placeholder="Search people"
|
|
||||||
@input="onInput"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="member-list">
|
|
||||||
<div
|
|
||||||
v-for="user in availableUsers"
|
|
||||||
:key="user.id"
|
|
||||||
class="member"
|
|
||||||
>
|
|
||||||
<div @click.capture.prevent="goToChat(user)">
|
|
||||||
<BasicUserCard :user="user" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_new.js"></script>
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
@import './chat_new.scss';
|
|
||||||
</style>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
|
||||||
|
|
||||||
export default Vue.component('chat-title', {
|
|
||||||
name: 'ChatTitle',
|
|
||||||
components: {
|
|
||||||
UserAvatar
|
|
||||||
},
|
|
||||||
props: [
|
|
||||||
'user', 'withAvatar'
|
|
||||||
],
|
|
||||||
computed: {
|
|
||||||
title () {
|
|
||||||
return this.user ? this.user.screen_name_ui : ''
|
|
||||||
},
|
|
||||||
htmlTitle () {
|
|
||||||
return this.user ? this.user.name_html : ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getUserProfileLink (user) {
|
|
||||||
return generateProfileLink(user.id, user.screen_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<div
|
|
||||||
class="chat-title"
|
|
||||||
:title="title"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-if="withAvatar && user"
|
|
||||||
:to="getUserProfileLink(user)"
|
|
||||||
>
|
|
||||||
<UserAvatar
|
|
||||||
:user="user"
|
|
||||||
width="23px"
|
|
||||||
height="23px"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
<span
|
|
||||||
class="username"
|
|
||||||
v-html="htmlTitle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_title.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.chat-title {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.username {
|
|
||||||
max-width: 100%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: inline;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
.emoji {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
vertical-align: middle;
|
|
||||||
object-fit: contain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Avatar {
|
|
||||||
width: 23px;
|
|
||||||
height: 23px;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
|
|
||||||
border-radius: $fallback--avatarAltRadius;
|
|
||||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
|
||||||
|
|
||||||
&.animated::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:checked="checked"
|
:checked="modelValue"
|
||||||
:indeterminate.prop="indeterminate"
|
:indeterminate="indeterminate"
|
||||||
@change="$emit('change', $event.target.checked)"
|
@change="$emit('update:modelValue', $event.target.checked)"
|
||||||
>
|
>
|
||||||
<i class="checkbox-indicator" />
|
<i class="checkbox-indicator" />
|
||||||
<span
|
<span
|
||||||
@@ -22,12 +22,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
model: {
|
emits: ['update:modelValue'],
|
||||||
prop: 'checked',
|
|
||||||
event: 'change'
|
|
||||||
},
|
|
||||||
props: [
|
props: [
|
||||||
'checked',
|
'modelValue',
|
||||||
'indeterminate',
|
'indeterminate',
|
||||||
'disabled'
|
'disabled'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
flex: 0 0 2em;
|
flex: 0 0 2em;
|
||||||
min-width: 2em;
|
min-width: 2em;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
height: 100%;
|
height: 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.computedIndicator,
|
.computedIndicator,
|
||||||
|
|||||||
@@ -11,28 +11,28 @@
|
|||||||
</label>
|
</label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
||||||
:checked="present"
|
:model-value="present"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="opt"
|
class="opt"
|
||||||
@change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||||
/>
|
/>
|
||||||
<div class="input color-input-field">
|
<div class="input color-input-field">
|
||||||
<input
|
<input
|
||||||
:id="name + '-t'"
|
:id="name + '-t'"
|
||||||
class="textColor unstyled"
|
class="textColor unstyled"
|
||||||
type="text"
|
type="text"
|
||||||
:value="value || fallback"
|
:value="modelValue || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="validColor"
|
v-if="validColor"
|
||||||
:id="name"
|
:id="name"
|
||||||
class="nativeColor unstyled"
|
class="nativeColor unstyled"
|
||||||
type="color"
|
type="color"
|
||||||
:value="value || fallback"
|
:value="modelValue || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="transparentColor"
|
v-if="transparentColor"
|
||||||
@@ -67,7 +67,7 @@ export default {
|
|||||||
},
|
},
|
||||||
// Color value, should be required but vue cannot tell the difference
|
// Color value, should be required but vue cannot tell the difference
|
||||||
// between "property missing" and "property set to undefined"
|
// between "property missing" and "property set to undefined"
|
||||||
value: {
|
modelValue: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined
|
default: undefined
|
||||||
@@ -91,18 +91,19 @@ export default {
|
|||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
computed: {
|
computed: {
|
||||||
present () {
|
present () {
|
||||||
return typeof this.value !== 'undefined'
|
return typeof this.modelValue !== 'undefined'
|
||||||
},
|
},
|
||||||
validColor () {
|
validColor () {
|
||||||
return hex2rgb(this.value || this.fallback)
|
return hex2rgb(this.modelValue || this.fallback)
|
||||||
},
|
},
|
||||||
transparentColor () {
|
transparentColor () {
|
||||||
return this.value === 'transparent'
|
return this.modelValue === 'transparent'
|
||||||
},
|
},
|
||||||
computedColor () {
|
computedColor () {
|
||||||
return this.value && this.value.startsWith('--')
|
return this.modelValue && this.modelValue.startsWith('--')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
|
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
const sortById = (a, b) => {
|
||||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||||
@@ -35,7 +49,10 @@ const conversation = {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
expanded: false
|
expanded: false,
|
||||||
|
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||||
|
statusContentPropertiesObject: {},
|
||||||
|
inlineDivePosition: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
@@ -53,12 +70,50 @@ const conversation = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hideStatus () {
|
maxDepthToShowByDefault () {
|
||||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
// maxDepthInThread = max number of depths that is *visible*
|
||||||
return this.virtualHidden && this.$refs.statusComponent[0].suspendable
|
// since our depth starts with 0 and "showing" means "showing children"
|
||||||
} else {
|
// there is a -2 here
|
||||||
return this.virtualHidden
|
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||||
|
return maxDepth >= 1 ? maxDepth : 1
|
||||||
|
},
|
||||||
|
displayStyle () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationDisplay
|
||||||
|
},
|
||||||
|
isTreeView () {
|
||||||
|
return !this.isLinearView
|
||||||
|
},
|
||||||
|
treeViewIsSimple () {
|
||||||
|
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||||
|
},
|
||||||
|
isLinearView () {
|
||||||
|
return this.displayStyle === 'linear'
|
||||||
|
},
|
||||||
|
shouldFadeAncestors () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||||
|
},
|
||||||
|
otherRepliesButtonPosition () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonBelowStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'below'
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonInsideStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
|
},
|
||||||
|
suspendable () {
|
||||||
|
if (this.isTreeView) {
|
||||||
|
return Object.entries(this.statusContentProperties)
|
||||||
|
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
||||||
}
|
}
|
||||||
|
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||||
|
return this.$refs.statusComponent.every(s => s.suspendable)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideStatus () {
|
||||||
|
return this.virtualHidden && this.suspendable
|
||||||
},
|
},
|
||||||
status () {
|
status () {
|
||||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||||
@@ -90,6 +145,121 @@ const conversation = {
|
|||||||
|
|
||||||
return sortAndFilterConversation(conversation, this.status)
|
return sortAndFilterConversation(conversation, this.status)
|
||||||
},
|
},
|
||||||
|
statusMap () {
|
||||||
|
return this.conversation.reduce((res, s) => {
|
||||||
|
res[s.id] = s
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
threadTree () {
|
||||||
|
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
||||||
|
table[status.id] = index
|
||||||
|
return table
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const threads = this.conversation.reduce((a, cur) => {
|
||||||
|
const id = cur.id
|
||||||
|
a.forest[id] = this.getReplies(id)
|
||||||
|
.map(s => s.id)
|
||||||
|
|
||||||
|
return a
|
||||||
|
}, {
|
||||||
|
forest: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||||
|
if (processed[id]) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
processed[id] = true
|
||||||
|
return [{
|
||||||
|
status: this.conversation[reverseLookupTable[id]],
|
||||||
|
id,
|
||||||
|
depth
|
||||||
|
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
||||||
|
}).reduce((a, b) => a.concat(b), [])
|
||||||
|
|
||||||
|
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
|
||||||
|
|
||||||
|
return linearized
|
||||||
|
},
|
||||||
|
replyIds () {
|
||||||
|
return this.conversation.map(k => k.id)
|
||||||
|
.reduce((res, id) => {
|
||||||
|
res[id] = (this.replies[id] || []).map(k => k.id)
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
totalReplyCount () {
|
||||||
|
const sizes = {}
|
||||||
|
const subTreeSizeFor = (id) => {
|
||||||
|
if (sizes[id]) {
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
this.conversation.map(k => k.id).map(subTreeSizeFor)
|
||||||
|
return Object.keys(sizes).reduce((res, id) => {
|
||||||
|
res[id] = sizes[id] - 1 // exclude itself
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
totalReplyDepth () {
|
||||||
|
const depths = {}
|
||||||
|
const subTreeDepthFor = (id) => {
|
||||||
|
if (depths[id]) {
|
||||||
|
return depths[id]
|
||||||
|
}
|
||||||
|
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
|
||||||
|
return depths[id]
|
||||||
|
}
|
||||||
|
this.conversation.map(k => k.id).map(subTreeDepthFor)
|
||||||
|
return Object.keys(depths).reduce((res, id) => {
|
||||||
|
res[id] = depths[id] - 1 // exclude itself
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
depths () {
|
||||||
|
return this.threadTree.reduce((a, k) => {
|
||||||
|
a[k.id] = k.depth
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
topLevel () {
|
||||||
|
const topLevel = this.conversation.reduce((tl, cur) =>
|
||||||
|
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
|
||||||
|
return topLevel
|
||||||
|
},
|
||||||
|
otherTopLevelCount () {
|
||||||
|
return this.topLevel.length - 1
|
||||||
|
},
|
||||||
|
showingTopLevel () {
|
||||||
|
if (this.canDive && this.diveRoot) {
|
||||||
|
return [this.statusMap[this.diveRoot]]
|
||||||
|
}
|
||||||
|
return this.topLevel
|
||||||
|
},
|
||||||
|
diveRoot () {
|
||||||
|
const statusId = this.inlineDivePosition || this.statusId
|
||||||
|
const isTopLevel = !this.parentOf(statusId)
|
||||||
|
return isTopLevel ? null : statusId
|
||||||
|
},
|
||||||
|
diveDepth () {
|
||||||
|
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||||
|
},
|
||||||
|
diveMode () {
|
||||||
|
return this.canDive && !!this.diveRoot
|
||||||
|
},
|
||||||
|
shouldShowAllConversationButton () {
|
||||||
|
// The "show all conversation" button tells the user that there exist
|
||||||
|
// other toplevel statuses, so do not show it if there is only a single root
|
||||||
|
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
|
||||||
|
},
|
||||||
|
shouldShowAncestors () {
|
||||||
|
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
||||||
|
},
|
||||||
replies () {
|
replies () {
|
||||||
let i = 1
|
let i = 1
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
@@ -109,15 +279,71 @@ const conversation = {
|
|||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
isExpanded () {
|
isExpanded () {
|
||||||
return this.expanded || this.isPage
|
return !!(this.expanded || this.isPage)
|
||||||
},
|
},
|
||||||
hiddenStyle () {
|
hiddenStyle () {
|
||||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||||
return this.virtualHidden ? { height } : {}
|
return this.virtualHidden ? { height } : {}
|
||||||
|
},
|
||||||
|
threadDisplayStatus () {
|
||||||
|
return this.conversation.reduce((a, k) => {
|
||||||
|
const id = k.id
|
||||||
|
const depth = this.depths[id]
|
||||||
|
const status = (() => {
|
||||||
|
if (this.threadDisplayStatusObject[id]) {
|
||||||
|
return this.threadDisplayStatusObject[id]
|
||||||
|
}
|
||||||
|
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||||
|
return 'showing'
|
||||||
|
} else {
|
||||||
|
return 'hidden'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = status
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
statusContentProperties () {
|
||||||
|
return this.conversation.reduce((a, k) => {
|
||||||
|
const id = k.id
|
||||||
|
const props = (() => {
|
||||||
|
const def = {
|
||||||
|
showingTall: false,
|
||||||
|
expandingSubject: false,
|
||||||
|
showingLongSubject: false,
|
||||||
|
isReplying: false,
|
||||||
|
mediaPlaying: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusContentPropertiesObject[id]) {
|
||||||
|
return {
|
||||||
|
...def,
|
||||||
|
...this.statusContentPropertiesObject[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = props
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
canDive () {
|
||||||
|
return this.isTreeView && this.isExpanded
|
||||||
|
},
|
||||||
|
focused () {
|
||||||
|
return (id) => {
|
||||||
|
return (this.isExpanded) && id === this.highlight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeHighlight () {
|
||||||
|
return this.isExpanded ? this.highlight : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status
|
Status,
|
||||||
|
ThreadTree
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
statusId (newVal, oldVal) {
|
statusId (newVal, oldVal) {
|
||||||
@@ -132,6 +358,8 @@ const conversation = {
|
|||||||
expanded (value) {
|
expanded (value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
|
} else {
|
||||||
|
this.resetDisplayState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
virtualHidden (value) {
|
virtualHidden (value) {
|
||||||
@@ -161,8 +389,8 @@ const conversation = {
|
|||||||
getReplies (id) {
|
getReplies (id) {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
getHighlight () {
|
||||||
return (this.isExpanded) && id === this.statusId
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -170,15 +398,139 @@ const conversation = {
|
|||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
getHighlight () {
|
|
||||||
return this.isExpanded ? this.highlight : null
|
|
||||||
},
|
|
||||||
toggleExpanded () {
|
toggleExpanded () {
|
||||||
this.expanded = !this.expanded
|
this.expanded = !this.expanded
|
||||||
},
|
},
|
||||||
getConversationId (statusId) {
|
getConversationId (statusId) {
|
||||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
||||||
|
},
|
||||||
|
setThreadDisplay (id, nextStatus) {
|
||||||
|
this.threadDisplayStatusObject = {
|
||||||
|
...this.threadDisplayStatusObject,
|
||||||
|
[id]: nextStatus
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleThreadDisplay (id) {
|
||||||
|
const curStatus = this.threadDisplayStatus[id]
|
||||||
|
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||||
|
this.setThreadDisplay(id, nextStatus)
|
||||||
|
},
|
||||||
|
setThreadDisplayRecursively (id, nextStatus) {
|
||||||
|
this.setThreadDisplay(id, nextStatus)
|
||||||
|
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
|
||||||
|
},
|
||||||
|
showThreadRecursively (id) {
|
||||||
|
this.setThreadDisplayRecursively(id, 'showing')
|
||||||
|
},
|
||||||
|
setStatusContentProperty (id, name, value) {
|
||||||
|
this.statusContentPropertiesObject = {
|
||||||
|
...this.statusContentPropertiesObject,
|
||||||
|
[id]: {
|
||||||
|
...this.statusContentPropertiesObject[id],
|
||||||
|
[name]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleStatusContentProperty (id, name) {
|
||||||
|
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||||
|
},
|
||||||
|
leastVisibleAncestor (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(cur)
|
||||||
|
while (cur) {
|
||||||
|
// if the parent is showing it means cur is visible
|
||||||
|
if (this.threadDisplayStatus[parent] === 'showing') {
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
// nothing found, fall back to toplevel
|
||||||
|
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||||
|
},
|
||||||
|
diveIntoStatus (id, preventScroll) {
|
||||||
|
this.tryScrollTo(id)
|
||||||
|
},
|
||||||
|
diveToTopLevel () {
|
||||||
|
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
// only used when we are not on a page
|
||||||
|
undive () {
|
||||||
|
this.inlineDivePosition = null
|
||||||
|
this.setHighlight(this.statusId)
|
||||||
|
},
|
||||||
|
tryScrollTo (id) {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isPage) {
|
||||||
|
// set statusId
|
||||||
|
this.$router.push({ name: 'conversation', params: { id } })
|
||||||
|
} else {
|
||||||
|
this.inlineDivePosition = id
|
||||||
|
}
|
||||||
|
// Because the conversation can be unmounted when out of sight
|
||||||
|
// and mounted again when it comes into sight,
|
||||||
|
// the `mounted` or `created` function in `status` should not
|
||||||
|
// contain scrolling calls, as we do not want the page to jump
|
||||||
|
// when we scroll with an expanded conversation.
|
||||||
|
//
|
||||||
|
// Now the method is to rely solely on the `highlight` watcher
|
||||||
|
// in `status` components.
|
||||||
|
// In linear views, all statuses are rendered at all times, but
|
||||||
|
// in tree views, it is possible that a change in active status
|
||||||
|
// removes and adds status components (e.g. an originally child
|
||||||
|
// status becomes an ancestor status, and thus they will be
|
||||||
|
// different).
|
||||||
|
// Here, let the components be rendered first, in order to trigger
|
||||||
|
// the `highlight` watcher.
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setHighlight(id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goToCurrent () {
|
||||||
|
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
statusById (id) {
|
||||||
|
return this.statusMap[id]
|
||||||
|
},
|
||||||
|
parentOf (id) {
|
||||||
|
const status = this.statusById(id)
|
||||||
|
if (!status) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const { in_reply_to_status_id: parentId } = status
|
||||||
|
if (!this.statusMap[parentId]) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return parentId
|
||||||
|
},
|
||||||
|
parentOrSelf (id) {
|
||||||
|
return this.parentOf(id) || id
|
||||||
|
},
|
||||||
|
// Ancestors of some status, from top to bottom
|
||||||
|
ancestorsOf (id) {
|
||||||
|
const ancestors = []
|
||||||
|
let cur = this.parentOf(id)
|
||||||
|
while (cur) {
|
||||||
|
ancestors.unshift(this.statusMap[cur])
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
return ancestors
|
||||||
|
},
|
||||||
|
topLevelAncestorOrSelfId (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(id)
|
||||||
|
while (parent) {
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
}
|
||||||
|
return cur
|
||||||
|
},
|
||||||
|
resetDisplayState () {
|
||||||
|
this.undive()
|
||||||
|
this.threadDisplayStatusObject = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isExpanded"
|
v-if="isExpanded"
|
||||||
class="panel-heading conversation-heading"
|
class="panel-heading conversation-heading -sticky"
|
||||||
>
|
>
|
||||||
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
||||||
<button
|
<button
|
||||||
@@ -18,24 +18,176 @@
|
|||||||
{{ $t('timeline.collapse') }}
|
{{ $t('timeline.collapse') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<status
|
<div class="conversation-body panel-body">
|
||||||
v-for="status in conversation"
|
<div
|
||||||
:key="status.id"
|
v-if="isTreeView"
|
||||||
ref="statusComponent"
|
class="thread-body"
|
||||||
:inline-expanded="collapsable && isExpanded"
|
>
|
||||||
:statusoid="status"
|
<div
|
||||||
:expandable="!isExpanded"
|
v-if="shouldShowAllConversationButton"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
class="conversation-dive-to-top-level-box"
|
||||||
:focused="focused(status.id)"
|
>
|
||||||
:in-conversation="isExpanded"
|
<i18n-t
|
||||||
:highlight="getHighlight()"
|
keypath="status.show_all_conversation_with_icon"
|
||||||
:replies="getReplies(status.id)"
|
tag="button"
|
||||||
:in-profile="inProfile"
|
class="button-unstyled -link"
|
||||||
:profile-user-id="profileUserId"
|
scope="global"
|
||||||
class="conversation-status status-fadein panel-body"
|
@click.prevent="diveToTopLevel"
|
||||||
@goto="setHighlight"
|
>
|
||||||
@toggleExpanded="toggleExpanded"
|
<template #icon>
|
||||||
/>
|
<FAIcon
|
||||||
|
icon="angle-double-left"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #text>
|
||||||
|
<span>
|
||||||
|
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowAncestors"
|
||||||
|
class="thread-ancestors"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="status in ancestorsOf(diveRoot)"
|
||||||
|
:key="status.id"
|
||||||
|
class="thread-ancestor"
|
||||||
|
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||||
|
>
|
||||||
|
<status
|
||||||
|
ref="statusComponent"
|
||||||
|
:inline-expanded="collapsable && isExpanded"
|
||||||
|
:statusoid="status"
|
||||||
|
:expandable="!isExpanded"
|
||||||
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
|
:focused="focused(status.id)"
|
||||||
|
:in-conversation="isExpanded"
|
||||||
|
:highlight="getHighlight()"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
|
:simple-tree="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
|
:dive="() => diveIntoStatus(status.id)"
|
||||||
|
|
||||||
|
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||||
|
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||||
|
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||||
|
:controlled-replying="statusContentProperties[status.id].replying"
|
||||||
|
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||||
|
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||||
|
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||||
|
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||||
|
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||||
|
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||||
|
class="thread-ancestor-dive-box"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="thread-ancestor-dive-box-inner"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
tag="button"
|
||||||
|
scope="global"
|
||||||
|
keypath="status.ancestor_follow_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="diveIntoStatus(status.id)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<FAIcon
|
||||||
|
icon="angle-double-right"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #text>
|
||||||
|
<span>
|
||||||
|
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<thread-tree
|
||||||
|
v-for="status in showingTopLevel"
|
||||||
|
:key="status.id"
|
||||||
|
ref="statusComponent"
|
||||||
|
:depth="0"
|
||||||
|
|
||||||
|
:status="status"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:conversation="conversation"
|
||||||
|
:collapsable="collapsable"
|
||||||
|
:is-expanded="isExpanded"
|
||||||
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
|
:focused="focused"
|
||||||
|
:get-replies="getReplies"
|
||||||
|
:highlight="maybeHighlight"
|
||||||
|
:set-highlight="setHighlight"
|
||||||
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
|
:simple="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
:dive="canDive ? diveIntoStatus : undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLinearView"
|
||||||
|
class="thread-body"
|
||||||
|
>
|
||||||
|
<status
|
||||||
|
v-for="status in conversation"
|
||||||
|
:key="status.id"
|
||||||
|
ref="statusComponent"
|
||||||
|
:inline-expanded="collapsable && isExpanded"
|
||||||
|
:statusoid="status"
|
||||||
|
:expandable="!isExpanded"
|
||||||
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
|
:focused="focused(status.id)"
|
||||||
|
:in-conversation="isExpanded"
|
||||||
|
:highlight="getHighlight()"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -49,6 +201,48 @@
|
|||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.Conversation {
|
.Conversation {
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.conversation-dive-to-top-level-box {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestors {
|
||||||
|
margin-left: var(--status-margin, $status-margin);
|
||||||
|
border-left: 2px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestor.-faded .StatusContent {
|
||||||
|
--link: var(--faintLink);
|
||||||
|
--text: var(--faint);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestor-dive-box {
|
||||||
|
padding-left: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
&, &-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.thread-ancestor-dive-box-inner {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-status {
|
.conversation-status {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
@@ -56,12 +250,33 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-expanded {
|
.thread-ancestor-has-other-replies .conversation-status,
|
||||||
.conversation-status:last-child {
|
.thread-ancestor:last-child .conversation-status,
|
||||||
border-bottom: none;
|
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
&:last-child .conversation-status,
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
&.-expanded .thread-tree .conversation-status {
|
||||||
}
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestors + .thread-tree > .conversation-status {
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* expanded conversation in timeline */
|
||||||
|
&.status-fadein.-expanded .thread-body {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: $fallback--cRed;
|
||||||
|
border-left-color: var(--cRed, $fallback--cRed);
|
||||||
|
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||||
|
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||||
|
border-bottom: 1px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-expanded.status-fadein {
|
||||||
|
margin: calc(var(--status-margin, $status-margin) / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import {
|
|||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
faHome,
|
faHome,
|
||||||
faComments,
|
faComments,
|
||||||
faBell,
|
|
||||||
faUserPlus,
|
faUserPlus,
|
||||||
faBullhorn,
|
faBullhorn,
|
||||||
faSearch,
|
faSearch,
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faCog,
|
faCog,
|
||||||
|
faGlobe,
|
||||||
|
faBolt,
|
||||||
|
faUsers,
|
||||||
|
faCommentMedical,
|
||||||
|
faBookmark,
|
||||||
faInfoCircle
|
faInfoCircle
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
@@ -19,12 +23,16 @@ library.add(
|
|||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
faHome,
|
faHome,
|
||||||
faComments,
|
faComments,
|
||||||
faBell,
|
|
||||||
faUserPlus,
|
faUserPlus,
|
||||||
faBullhorn,
|
faBullhorn,
|
||||||
faSearch,
|
faSearch,
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faCog,
|
faCog,
|
||||||
|
faGlobe,
|
||||||
|
faBolt,
|
||||||
|
faUsers,
|
||||||
|
faCommentMedical,
|
||||||
|
faBookmark,
|
||||||
faInfoCircle
|
faInfoCircle
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,7 +73,22 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
logo () { return this.$store.state.instance.logo },
|
logo () { return this.$store.state.instance.logo },
|
||||||
|
mergedConfig () {
|
||||||
|
return this.$store.getters.mergedConfig
|
||||||
|
},
|
||||||
sitename () { return this.$store.state.instance.name },
|
sitename () { return this.$store.state.instance.name },
|
||||||
|
showNavShortcuts () {
|
||||||
|
return this.mergedConfig.showNavShortcuts
|
||||||
|
},
|
||||||
|
showWiderShortcuts () {
|
||||||
|
return this.mergedConfig.showWiderShortcuts
|
||||||
|
},
|
||||||
|
hideSiteFavicon () {
|
||||||
|
return this.mergedConfig.hideSiteFavicon
|
||||||
|
},
|
||||||
|
hideSiteName () {
|
||||||
|
return this.mergedConfig.hideSiteName
|
||||||
|
},
|
||||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||||
logoLeft () { return this.$store.state.instance.logoLeft },
|
logoLeft () { return this.$store.state.instance.logoLeft },
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
@@ -75,10 +98,6 @@ export default {
|
|||||||
scrollToTop () {
|
scrollToTop () {
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
},
|
},
|
||||||
logout () {
|
|
||||||
this.$router.replace('/main/public')
|
|
||||||
this.$store.dispatch('logout')
|
|
||||||
},
|
|
||||||
onSearchBarToggled (hidden) {
|
onSearchBarToggled (hidden) {
|
||||||
this.searchBarHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.DesktopNav {
|
.DesktopNav {
|
||||||
height: 50px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: fixed;
|
|
||||||
|
input {
|
||||||
|
color: var(--inputTopbarText, var(--inputText));
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--topBarLink, $fallback--link);
|
color: var(--topBarLink, $fallback--link);
|
||||||
@@ -11,18 +13,18 @@
|
|||||||
|
|
||||||
.inner-nav {
|
.inner-nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 50px;
|
grid-template-rows: var(--navbar-height);
|
||||||
grid-template-columns: 2fr auto 2fr;
|
grid-template-columns: 2fr auto 2fr;
|
||||||
grid-template-areas: "sitename logo actions";
|
grid-template-areas: "nav-left logo actions";
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0 1.2em;
|
padding: 0 1.2em;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 980px;
|
max-width: 1110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-logoLeft {
|
&.-logoLeft .inner-nav {
|
||||||
grid-template-columns: auto 2fr 2fr;
|
grid-template-columns: auto 2fr 2fr;
|
||||||
grid-template-areas: "logo sitename actions";
|
grid-template-areas: "logo nav-left actions";
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-default {
|
.button-default {
|
||||||
@@ -77,7 +79,7 @@
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 50px;
|
height: var(--navbar-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +89,43 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
&.router-link-active {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-top: 0.05em;
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
font-weight: bolder;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--selectedMenuText, $fallback--text);
|
||||||
|
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.svg-inline--fa {
|
.svg-inline--fa {
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
color: var(--topBarLink, $fallback--link);
|
color: var(--topBarLink, $fallback--link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sitename {
|
.-wide {
|
||||||
grid-area: sitename;
|
.nav-icon {
|
||||||
|
margin-left: 0.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
padding-left: 5px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-left-wrapper {
|
||||||
|
grid-area: nav-left;
|
||||||
|
|
||||||
|
.favicon {
|
||||||
|
height: 28px;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@@ -103,8 +134,8 @@
|
|||||||
|
|
||||||
.item {
|
.item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
line-height: 50px;
|
line-height: var(--navbar-height);
|
||||||
height: 50px;
|
height: var(--navbar-height);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -5,16 +5,79 @@
|
|||||||
:class="{ '-logoLeft': logoLeft }"
|
:class="{ '-logoLeft': logoLeft }"
|
||||||
@click="scrollToTop()"
|
@click="scrollToTop()"
|
||||||
>
|
>
|
||||||
<div class="inner-nav">
|
<div
|
||||||
<div class="item sitename">
|
class="inner-nav"
|
||||||
|
:class="{ '-wide': showWiderShortcuts }"
|
||||||
|
>
|
||||||
|
<div class="item nav-left-wrapper">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="!hideSitename"
|
class="site-brand"
|
||||||
class="site-name"
|
|
||||||
:to="{ name: 'root' }"
|
:to="{ name: 'root' }"
|
||||||
active-class="home"
|
active-class="home"
|
||||||
>
|
>
|
||||||
{{ sitename }}
|
<img
|
||||||
|
v-if="!hideSiteFavicon"
|
||||||
|
class="favicon"
|
||||||
|
src="/favicon.png"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!hideSiteName"
|
||||||
|
class="site-name"
|
||||||
|
>
|
||||||
|
{{ sitename }}
|
||||||
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<div
|
||||||
|
v-if="(currentUser || !privateMode) && showNavShortcuts"
|
||||||
|
class="nav-items left"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="currentUser"
|
||||||
|
:to="{ name: 'friends' }"
|
||||||
|
class="nav-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="home"
|
||||||
|
:title="$t('nav.home_timeline')"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'public-timeline' }"
|
||||||
|
class="nav-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="users"
|
||||||
|
:title="$t('nav.public_tl')"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-if="currentUser"
|
||||||
|
:to="{ name: 'bubble-timeline' }"
|
||||||
|
class="nav-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="circle"
|
||||||
|
:title="$t('nav.bubble_timeline')"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'public-external-timeline' }"
|
||||||
|
class="nav-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="globe"
|
||||||
|
:title="$t('nav.twkn')"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
class="logo"
|
class="logo"
|
||||||
@@ -34,8 +97,49 @@
|
|||||||
<search-bar
|
<search-bar
|
||||||
v-if="currentUser || !privateMode"
|
v-if="currentUser || !privateMode"
|
||||||
@toggled="onSearchBarToggled"
|
@toggled="onSearchBarToggled"
|
||||||
@click.stop.native
|
@click.stop
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="(currentUser || !privateMode) && showNavShortcuts"
|
||||||
|
class="nav-items right"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="nav-icon"
|
||||||
|
v-if="currentUser"
|
||||||
|
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="bolt"
|
||||||
|
:title="$t('nav.interactions')"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-if="currentUser"
|
||||||
|
:to="{ name: 'lists' }"
|
||||||
|
class="nav-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="list"
|
||||||
|
:title="$t('nav.lists')"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-if="currentUser"
|
||||||
|
:to="{ name: 'bookmarks' }"
|
||||||
|
class="nav-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="bookmark"
|
||||||
|
:title="$t('nav.bookmarks')"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled nav-icon"
|
class="button-unstyled nav-icon"
|
||||||
@click.stop="openSettingsModal"
|
@click.stop="openSettingsModal"
|
||||||
@@ -52,6 +156,7 @@
|
|||||||
href="/pleroma/admin/#/login-pleroma"
|
href="/pleroma/admin/#/login-pleroma"
|
||||||
class="nav-icon"
|
class="nav-icon"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
@@ -60,18 +165,6 @@
|
|||||||
:title="$t('nav.administration')"
|
:title="$t('nav.administration')"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<button
|
|
||||||
v-if="currentUser"
|
|
||||||
class="button-unstyled nav-icon"
|
|
||||||
@click.prevent="logout"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110 fa-old-padding"
|
|
||||||
icon="sign-out-alt"
|
|
||||||
:title="$t('login.logout')"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -58,16 +58,7 @@
|
|||||||
background-color: var(--bg, $fallback--bg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
|
|
||||||
.dialog-modal-heading {
|
.dialog-modal-heading {
|
||||||
padding: .5em .5em;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-bottom: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--panelText);
|
|
||||||
background-color: $fallback--fg;
|
|
||||||
background-color: var(--panel, $fallback--fg);
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-bottom: 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ library.add(
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const EmojiInput = {
|
const EmojiInput = {
|
||||||
|
emits: ['update:modelValue', 'shown'],
|
||||||
props: {
|
props: {
|
||||||
suggest: {
|
suggest: {
|
||||||
/**
|
/**
|
||||||
@@ -57,8 +58,7 @@ const EmojiInput = {
|
|||||||
required: true,
|
required: true,
|
||||||
type: Function
|
type: Function
|
||||||
},
|
},
|
||||||
// TODO VUE3: change to modelValue, change 'input' event to 'input'
|
modelValue: {
|
||||||
value: {
|
|
||||||
/**
|
/**
|
||||||
* Used for v-model
|
* Used for v-model
|
||||||
*/
|
*/
|
||||||
@@ -137,8 +137,8 @@ const EmojiInput = {
|
|||||||
return (this.wordAtCaret || {}).word || ''
|
return (this.wordAtCaret || {}).word || ''
|
||||||
},
|
},
|
||||||
wordAtCaret () {
|
wordAtCaret () {
|
||||||
if (this.value && this.caret) {
|
if (this.modelValue && this.caret) {
|
||||||
const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
|
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||||
return word
|
return word
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,8 +189,11 @@ const EmojiInput = {
|
|||||||
img: imageUrl || ''
|
img: imageUrl || ''
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
suggestions (newValue) {
|
suggestions: {
|
||||||
this.$nextTick(this.resize)
|
handler (newValue) {
|
||||||
|
this.$nextTick(this.resize)
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -225,13 +228,13 @@ const EmojiInput = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
replace (replacement) {
|
replace (replacement) {
|
||||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||||
this.$emit('input', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
this.caret = 0
|
this.caret = 0
|
||||||
},
|
},
|
||||||
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
||||||
const before = this.value.substring(0, this.caret) || ''
|
const before = this.modelValue.substring(0, this.caret) || ''
|
||||||
const after = this.value.substring(this.caret) || ''
|
const after = this.modelValue.substring(this.caret) || ''
|
||||||
|
|
||||||
/* Using a bit more smart approach to padding emojis with spaces:
|
/* Using a bit more smart approach to padding emojis with spaces:
|
||||||
* - put a space before cursor if there isn't one already, unless we
|
* - put a space before cursor if there isn't one already, unless we
|
||||||
@@ -259,7 +262,7 @@ const EmojiInput = {
|
|||||||
after
|
after
|
||||||
].join('')
|
].join('')
|
||||||
this.keepOpen = keepOpen
|
this.keepOpen = keepOpen
|
||||||
this.$emit('input', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
this.input.focus()
|
this.input.focus()
|
||||||
@@ -278,8 +281,8 @@ const EmojiInput = {
|
|||||||
if (len > 0 || suggestion) {
|
if (len > 0 || suggestion) {
|
||||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||||
const replacement = chosenSuggestion.replacement
|
const replacement = chosenSuggestion.replacement
|
||||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||||
this.$emit('input', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
this.highlighted = 0
|
this.highlighted = 0
|
||||||
const position = this.wordAtCaret.start + replacement.length
|
const position = this.wordAtCaret.start + replacement.length
|
||||||
|
|
||||||
@@ -455,7 +458,7 @@ const EmojiInput = {
|
|||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
this.resize()
|
||||||
this.$emit('input', e.target.value)
|
this.$emit('update:modelValue', e.target.value)
|
||||||
},
|
},
|
||||||
onClickInput (e) {
|
onClickInput (e) {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
@@ -488,11 +491,10 @@ const EmojiInput = {
|
|||||||
},
|
},
|
||||||
setPlacement (container, target, offsetBottom) {
|
setPlacement (container, target, offsetBottom) {
|
||||||
if (!container || !target) return
|
if (!container || !target) return
|
||||||
|
if (this.placement === 'bottom' || (this.placement === 'auto' && !this.overflowsBottom(container))) {
|
||||||
target.style.top = offsetBottom + 'px'
|
target.style.top = offsetBottom + 'px'
|
||||||
target.style.bottom = 'auto'
|
target.style.bottom = 'auto'
|
||||||
|
} else {
|
||||||
if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
|
|
||||||
target.style.top = 'auto'
|
target.style.top = 'auto'
|
||||||
target.style.bottom = this.input.offsetHeight + 'px'
|
target.style.bottom = this.input.offsetHeight + 'px'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: .2em .25em;
|
margin: .2em .25em;
|
||||||
font-size: 16px;
|
font-size: 1.3em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
@@ -5,6 +6,7 @@ import {
|
|||||||
faStickyNote,
|
faStickyNote,
|
||||||
faSmileBeam
|
faSmileBeam
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { trim, escapeRegExp, startCase } from 'lodash'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
@@ -19,23 +21,6 @@ const LOAD_EMOJI_BY = 60
|
|||||||
// When to start loading new batch emoji, in pixels
|
// When to start loading new batch emoji, in pixels
|
||||||
const LOAD_EMOJI_MARGIN = 64
|
const LOAD_EMOJI_MARGIN = 64
|
||||||
|
|
||||||
const filterByKeyword = (list, keyword = '') => {
|
|
||||||
if (keyword === '') return list
|
|
||||||
|
|
||||||
const keywordLowercase = keyword.toLowerCase()
|
|
||||||
let orderedEmojiList = []
|
|
||||||
for (const emoji of list) {
|
|
||||||
const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
|
|
||||||
if (indexOfKeyword > -1) {
|
|
||||||
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
|
||||||
orderedEmojiList[indexOfKeyword] = []
|
|
||||||
}
|
|
||||||
orderedEmojiList[indexOfKeyword].push(emoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return orderedEmojiList.flat()
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmojiPicker = {
|
const EmojiPicker = {
|
||||||
props: {
|
props: {
|
||||||
enableStickerPicker: {
|
enableStickerPicker: {
|
||||||
@@ -47,7 +32,7 @@ const EmojiPicker = {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
keyword: '',
|
keyword: '',
|
||||||
activeGroup: 'custom',
|
activeGroup: 'standard',
|
||||||
showingStickers: false,
|
showingStickers: false,
|
||||||
groupsScrolledClass: 'scrolled-top',
|
groupsScrolledClass: 'scrolled-top',
|
||||||
keepOpen: false,
|
keepOpen: false,
|
||||||
@@ -57,7 +42,7 @@ const EmojiPicker = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
|
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||||
Checkbox
|
Checkbox
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -78,13 +63,8 @@ const EmojiPicker = {
|
|||||||
this.triggerLoadMore(target)
|
this.triggerLoadMore(target)
|
||||||
},
|
},
|
||||||
highlight (key) {
|
highlight (key) {
|
||||||
const ref = this.$refs['group-' + key]
|
|
||||||
const top = ref[0].offsetTop
|
|
||||||
this.setShowStickers(false)
|
this.setShowStickers(false)
|
||||||
this.activeGroup = key
|
this.activeGroup = key
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs['emoji-groups'].scrollTop = top + 1
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
updateScrolledClass (target) {
|
updateScrolledClass (target) {
|
||||||
if (target.scrollTop <= 5) {
|
if (target.scrollTop <= 5) {
|
||||||
@@ -96,7 +76,7 @@ const EmojiPicker = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
triggerLoadMore (target) {
|
triggerLoadMore (target) {
|
||||||
const ref = this.$refs['group-end-custom'][0]
|
const ref = this.$refs['group-end-custom']
|
||||||
if (!ref) return
|
if (!ref) return
|
||||||
const bottom = ref.offsetTop + ref.offsetHeight
|
const bottom = ref.offsetTop + ref.offsetHeight
|
||||||
|
|
||||||
@@ -119,7 +99,7 @@ const EmojiPicker = {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.emojisView.forEach(group => {
|
this.emojisView.forEach(group => {
|
||||||
const ref = this.$refs['group-' + group.id]
|
const ref = this.$refs['group-' + group.id]
|
||||||
if (ref[0].offsetTop <= top) {
|
if (ref.offsetTop <= top) {
|
||||||
this.activeGroup = group.id
|
this.activeGroup = group.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -153,6 +133,13 @@ const EmojiPicker = {
|
|||||||
},
|
},
|
||||||
setShowStickers (value) {
|
setShowStickers (value) {
|
||||||
this.showingStickers = value
|
this.showingStickers = value
|
||||||
|
},
|
||||||
|
filterByKeyword (list) {
|
||||||
|
if (this.keyword === '') return list
|
||||||
|
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
|
||||||
|
return list.filter(emoji => {
|
||||||
|
return regex.test(emoji.displayText)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -173,9 +160,8 @@ const EmojiPicker = {
|
|||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
filteredEmoji () {
|
filteredEmoji () {
|
||||||
return filterByKeyword(
|
return this.filterByKeyword(
|
||||||
this.$store.state.instance.customEmoji || [],
|
this.$store.state.instance.customEmoji || []
|
||||||
this.keyword
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
customEmojiBuffer () {
|
customEmojiBuffer () {
|
||||||
@@ -183,28 +169,53 @@ const EmojiPicker = {
|
|||||||
},
|
},
|
||||||
emojis () {
|
emojis () {
|
||||||
const standardEmojis = this.$store.state.instance.emoji || []
|
const standardEmojis = this.$store.state.instance.emoji || []
|
||||||
const customEmojis = this.customEmojiBuffer
|
const customEmojis = this.sortedEmoji
|
||||||
|
const emojiPacks = []
|
||||||
|
customEmojis.forEach((pack, id) => {
|
||||||
|
emojiPacks.push({
|
||||||
|
id: id.replace(/^pack:/, ''),
|
||||||
|
text: startCase(id.replace(/^pack:/, '')),
|
||||||
|
first: pack[0],
|
||||||
|
emojis: this.filterByKeyword(pack)
|
||||||
|
})
|
||||||
|
})
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
id: 'custom',
|
|
||||||
text: this.$t('emoji.custom'),
|
|
||||||
icon: 'smile-beam',
|
|
||||||
emojis: customEmojis
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'standard',
|
id: 'standard',
|
||||||
text: this.$t('emoji.unicode'),
|
text: this.$t('emoji.unicode'),
|
||||||
icon: 'box-open',
|
first: {
|
||||||
emojis: filterByKeyword(standardEmojis, this.keyword)
|
imageUrl: '',
|
||||||
|
replacement: '🥴'
|
||||||
|
},
|
||||||
|
emojis: this.filterByKeyword(standardEmojis)
|
||||||
}
|
}
|
||||||
]
|
].concat(emojiPacks)
|
||||||
|
},
|
||||||
|
sortedEmoji () {
|
||||||
|
const customEmojis = this.$store.state.instance.customEmoji || []
|
||||||
|
const sortedEmojiGroups = new Map()
|
||||||
|
customEmojis.forEach((emoji) => {
|
||||||
|
if (!sortedEmojiGroups.has(emoji.tags[0])) {
|
||||||
|
sortedEmojiGroups.set(emoji.tags[0], [emoji])
|
||||||
|
} else {
|
||||||
|
sortedEmojiGroups.get(emoji.tags[0]).push(emoji)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return new Map([...sortedEmojiGroups.entries()].sort())
|
||||||
},
|
},
|
||||||
emojisView () {
|
emojisView () {
|
||||||
return this.emojis.filter(value => value.emojis.length > 0)
|
if (this.keyword === '') {
|
||||||
|
return this.emojis.filter(pack => {
|
||||||
|
return pack.id === this.activeGroup
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return this.emojis.filter(pack => {
|
||||||
|
return pack.emojis.length > 0
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
stickerPickerEnabled () {
|
stickerPickerEnabled () {
|
||||||
return (this.$store.state.instance.stickers || []).length !== 0
|
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--popover, $fallback--bg);
|
background-color: var(--popover, $fallback--bg);
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
@@ -35,9 +35,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
display: flex;
|
margin-top: 10px;
|
||||||
height: 32px;
|
height: 4.8em;
|
||||||
padding: 10px 7px 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -65,20 +64,40 @@
|
|||||||
|
|
||||||
.additional-tabs,
|
.additional-tabs,
|
||||||
.emoji-tabs {
|
.emoji-tabs {
|
||||||
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 0;
|
flex-wrap: nowrap;
|
||||||
flex-basis: auto;
|
|
||||||
flex-shrink: 1;
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
padding: 0 7px;
|
vertical-align: top;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: .4em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 24px;
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 1.9em;
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-bottom: 4px solid;
|
border-bottom: 4px solid;
|
||||||
|
|
||||||
@@ -87,6 +106,9 @@
|
|||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.fa-sticky-note {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,9 +173,10 @@
|
|||||||
justify-content: left;
|
justify-content: left;
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
font-size: 12px;
|
font-size: 0.85em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,15 @@
|
|||||||
:title="group.text"
|
:title="group.text"
|
||||||
@click.prevent="highlight(group.id)"
|
@click.prevent="highlight(group.id)"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
|
||||||
:icon="group.icon"
|
<img
|
||||||
fixed-width
|
v-else
|
||||||
/>
|
:src="group.first.imageUrl"
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="stickerPickerEnabled"
|
|
||||||
class="additional-tabs"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="stickers-tab-icon additional-tabs-item"
|
v-if="stickerPickerEnabled"
|
||||||
|
class="stickers-tab-icon emoji-tabs-item"
|
||||||
:class="{active: showingStickers}"
|
:class="{active: showingStickers}"
|
||||||
:title="$t('emoji.stickers')"
|
:title="$t('emoji.stickers')"
|
||||||
@click.prevent="toggleStickers"
|
@click.prevent="toggleStickers"
|
||||||
@@ -47,6 +44,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:placeholder="$t('emoji.search_emoji')"
|
:placeholder="$t('emoji.search_emoji')"
|
||||||
|
@input="$event.target.composing = false"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ const EmojiReactions = {
|
|||||||
},
|
},
|
||||||
accountsForEmoji () {
|
accountsForEmoji () {
|
||||||
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||||
acc[reaction.name] = reaction.accounts || []
|
if (reaction.url) {
|
||||||
|
acc[reaction.url] = reaction.accounts || []
|
||||||
|
} else {
|
||||||
|
acc[reaction.name] = reaction.accounts || []
|
||||||
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
@@ -42,6 +46,14 @@ const EmojiReactions = {
|
|||||||
reactedWith (emoji) {
|
reactedWith (emoji) {
|
||||||
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
||||||
},
|
},
|
||||||
|
isLocalReaction (emojiUrl) {
|
||||||
|
if (!emojiUrl) return true
|
||||||
|
const reacted = this.accountsForEmoji[emojiUrl]
|
||||||
|
if (reacted.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return reacted[0].is_local
|
||||||
|
},
|
||||||
fetchEmojiReactionsByIfMissing () {
|
fetchEmojiReactionsByIfMissing () {
|
||||||
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
||||||
if (hasNoAccounts) {
|
if (hasNoAccounts) {
|
||||||
|
|||||||
@@ -2,17 +2,33 @@
|
|||||||
<div class="emoji-reactions">
|
<div class="emoji-reactions">
|
||||||
<UserListPopover
|
<UserListPopover
|
||||||
v-for="(reaction) in emojiReactions"
|
v-for="(reaction) in emojiReactions"
|
||||||
:key="reaction.name"
|
:key="reaction.url || reaction.name"
|
||||||
:users="accountsForEmoji[reaction.name]"
|
:users="accountsForEmoji[reaction.url || reaction.name]"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="emoji-reaction btn button-default"
|
class="emoji-reaction btn button-default"
|
||||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||||
|
:disabled="!isLocalReaction(reaction.url)"
|
||||||
@click="emojiOnClick(reaction.name, $event)"
|
@click="emojiOnClick(reaction.name, $event)"
|
||||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||||
>
|
>
|
||||||
<span class="reaction-emoji">{{ reaction.name }}</span>
|
<span
|
||||||
<span>{{ reaction.count }}</span>
|
v-if="reaction.url !== null"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="reaction.url"
|
||||||
|
:title="reaction.name"
|
||||||
|
class="reaction-emoji"
|
||||||
|
width="2.55em"
|
||||||
|
>
|
||||||
|
{{ reaction.count }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<span class="reaction-emoji unicode-emoji">
|
||||||
|
{{ reaction.name }}
|
||||||
|
</span>
|
||||||
|
<span>{{ reaction.count }}</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</UserListPopover>
|
</UserListPopover>
|
||||||
<a
|
<a
|
||||||
@@ -36,6 +52,10 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unicode-emoji {
|
||||||
|
font-size: 210%;
|
||||||
|
}
|
||||||
|
|
||||||
.emoji-reaction {
|
.emoji-reaction {
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
@@ -45,7 +65,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
.reaction-emoji {
|
.reaction-emoji {
|
||||||
width: 1.25em;
|
width: 2.55em !important;
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|||||||
@@ -15,18 +15,8 @@ const Exporter = {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'export.csv'
|
default: 'export.csv'
|
||||||
},
|
},
|
||||||
exportButtonLabel: {
|
exportButtonLabel: { type: String },
|
||||||
type: String,
|
processingMessage: { type: String }
|
||||||
default () {
|
|
||||||
return this.$t('exporter.export')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
processingMessage: {
|
|
||||||
type: String,
|
|
||||||
default () {
|
|
||||||
return this.$t('exporter.processing')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
spin
|
spin
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span>{{ processingMessage }}</span>
|
<span>{{ processingMessage || $t('exporter.processing') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
@click="process"
|
@click="process"
|
||||||
>
|
>
|
||||||
{{ exportButtonLabel }}
|
{{ exportButtonLabel || $t('exporter.export') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
|||||||
|
|
||||||
const FeaturesPanel = {
|
const FeaturesPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
shout: function () { return this.$store.state.instance.shoutAvailable },
|
|
||||||
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
|
|
||||||
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
|
||||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||||
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
||||||
|
|||||||
@@ -8,15 +8,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body features-panel">
|
<div class="panel-body features-panel">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-if="shout">
|
|
||||||
{{ $t('features_panel.shout') }}
|
|
||||||
</li>
|
|
||||||
<li v-if="pleromaChatMessages">
|
|
||||||
{{ $t('features_panel.pleroma_chat_messages') }}
|
|
||||||
</li>
|
|
||||||
<li v-if="gopher">
|
|
||||||
{{ $t('features_panel.gopher') }}
|
|
||||||
</li>
|
|
||||||
<li v-if="whoToFollow">
|
<li v-if="whoToFollow">
|
||||||
{{ $t('features_panel.who_to_follow') }}
|
{{ $t('features_panel.who_to_follow') }}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -39,12 +39,13 @@ const Flash = {
|
|||||||
this.player = 'error'
|
this.player = 'error'
|
||||||
})
|
})
|
||||||
this.ruffleInstance = player
|
this.ruffleInstance = player
|
||||||
|
this.$emit('playerOpened')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
closePlayer () {
|
closePlayer () {
|
||||||
console.log(this.ruffleInstance)
|
this.ruffleInstance && this.ruffleInstance.remove()
|
||||||
this.ruffleInstance.remove()
|
|
||||||
this.player = false
|
this.player = false
|
||||||
|
this.$emit('playerClosed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||