1
0
Fork 0

Compare commits

...

61 commits

Author SHA1 Message Date
Alexander Yakovlev c6824b9b0d Merge remote-tracking branch 'upstream/main' 2024-02-05 18:36:26 +06:00
Lim Chee Aun 36f38230c4 Attempt to shorten links if not shortened
This usually comes from non-Mastodon instances
2024-02-03 20:36:25 +08:00
Lim Chee Aun a66a4e238e More subtle style change to reply parent 2024-02-02 13:20:55 +08:00
Lim Chee Aun aa7fb4441f Subtle style change to reply parent 2024-02-02 12:58:35 +08:00
Lim Chee Aun f1dbb9ec42 Further delay filtered status peek, remove tooltip 2024-02-02 00:27:12 +08:00
Lim Chee Aun a59668ea9a Slight adjustment to carousel colors 2024-02-01 22:49:16 +08:00
Lim Chee Aun 6581bc2881 Prevent reply parent hint from being GC-ed 2024-01-31 13:45:34 +08:00
Lim Chee Aun 28bb66f185 Show total at end of list 2024-01-31 09:03:33 +08:00
Lim Chee Aun 46d7cba1ea Show join date if there's nothing to show 2024-01-30 22:46:18 +08:00
Lim Chee Aun ff35c458c3 Don't return 2024-01-30 18:57:28 +08:00
Lim Chee Aun 26d445af7d Fix reply parent hint not appearing
Also respect language
2024-01-30 17:43:44 +08:00
Lim Chee Aun 3470b9adec Fix forgot to opt-in new experiment 2024-01-30 15:22:01 +08:00
Lim Chee Aun f3d77dd04e Experimental reply parent hint 2024-01-30 14:34:54 +08:00
Lim Chee Aun 14f5c37721 Don't show comment hint for timeline item container 2024-01-30 14:28:28 +08:00
Lim Chee Aun 94c59c47d1 Upgrade dependencies 2024-01-29 21:11:19 +08:00
Lim Chee Aun a66307b757 Fixes + improvements to search UI 2024-01-29 21:11:08 +08:00
Lim Chee Aun 9792700f30 Fix wrong CSS
Add more checks
2024-01-29 01:38:53 +08:00
Lim Chee Aun 36e852bebb Fix weird overflow: clip bug on Chrome 2024-01-28 00:49:11 +08:00
Lim Chee Aun 6075542071 Exclude the JS-injected hashtag stuffing class 2024-01-26 16:09:21 +08:00
Lim Chee Aun 0386357688 Fix weird bug with wrong cache of icon 2024-01-26 00:28:03 +08:00
Alexander Yakovlev 3f0b933654 Merge remote-tracking branch 'upstream/main' 2024-01-25 21:35:50 +06:00
Lim Chee Aun 9cac63c37d Experimental more-harsh hashtag stuffing collapsing 2024-01-25 22:13:38 +08:00
Lim Chee Aun 5cfcfdc98b Squeeze all the micro-perf 2024-01-25 21:28:41 +08:00
Lim Chee Aun a2d995ec07 Support unofficial status.quote 2024-01-25 12:59:53 +08:00
Lim Chee Aun 4ca9a802e3 Remove console.log 2024-01-25 08:00:55 +08:00
Lim Chee Aun 990f2b2e29 Handle unknown audio attachments 2024-01-24 13:08:54 +08:00
Lim Chee Aun 725da37063 Slight adjustments to post actions bar 2024-01-21 13:10:57 +08:00
Lim Chee Aun 1b41d39032 Stretch svg dimensions 2024-01-20 10:26:01 +08:00
Lim Chee Aun 23dd7f5a7a Extract ICONS out 2024-01-20 10:25:47 +08:00
Lim Chee Aun 7d95c50c7a Remove width/height in svg 2024-01-20 01:45:54 +08:00
Lim Chee Aun a352f94c2c Use more beautiful quotes 2024-01-20 01:45:36 +08:00
Lim Chee Aun 38e2b176bc Make embeds larger 2024-01-19 20:31:05 +08:00
Lim Chee Aun 6b4c1c8505 Change menu alignment 2024-01-19 20:29:46 +08:00
Lim Chee Aun 46dfd9aab0 MVP-ish pin/unpin post 2024-01-18 19:05:12 +08:00
Lim Chee Aun 59d0138ca8 If there's selected text, don't show custom context menu 2024-01-17 13:42:46 +08:00
Lim Chee Aun 3fbd5b8622 s/allowNofitications/allowNotifications
Also very embarrassing
2024-01-17 11:32:16 +08:00
Lim Chee Aun b6c4045cb4 Escape HTML chars in composer highlights
This is very embarrassing, I know
2024-01-17 11:31:33 +08:00
Lim Chee Aun 37c784dad2 Make refresh button more prominent 2024-01-16 15:47:10 +08:00
Lim Chee Aun 04d431cf71 Add more conditions 2024-01-15 22:05:18 +08:00
Lim Chee Aun 97458b66eb Update languages list 2024-01-15 20:39:29 +08:00
Lim Chee Aun fadfc6052d Only show for coarse pointer 2024-01-15 00:31:42 +08:00
Lim Chee Aun 0ca92e7509 Fix icon alignment in shortcut settings 2024-01-14 23:04:14 +08:00
Lim Chee Aun b8484eff79 Differentiate menu open from right-click vs actions bar
Kinda hacky for now
2024-01-14 21:34:21 +08:00
Lim Chee Aun 1017d1d270 Style changes for focused more button 2024-01-14 21:33:52 +08:00
Lim Chee Aun 04179340f6 Further enhance actions bar
- Focus color when context menu is open
- Focus color for more button when context menu is open
- Reuse menu instead of creating another menu
- Show like toast when liked/unliked
2024-01-14 19:36:14 +08:00
Lim Chee Aun 9b0889fe23 Test show refresh button after a minute 2024-01-14 18:31:53 +08:00
Lim Chee Aun 79e87b7d89 A little transition when expanding replies 2024-01-14 18:29:11 +08:00
Lim Chee Aun 0ebc0fa64c First step in introducing actions bar 2024-01-14 00:32:08 +08:00
Lim Chee Aun 35974cc89c Show more consistent icon for "comment" 2024-01-14 00:30:12 +08:00
Lim Chee Aun 00675c827f Upgrade react-hotkeys-hook 2024-01-14 00:29:30 +08:00
Lim Chee Aun 2b3f65f28c Fix wrong account shown
Need the hostname to be more accurate
2024-01-12 14:47:59 +08:00
Alexander Yakovlev 2912d2e2e6 Merge remote-tracking branch 'upstream/main' 2024-01-12 10:00:04 +06:00
Lim Chee Aun 500f877d4b Fix error when r is undefined 2024-01-11 10:44:37 +08:00
Lim Chee Aun 4b9ff0ca5b Hide "more" icon for posts in notifications 2024-01-11 10:44:24 +08:00
Lim Chee Aun 07f927d4ff Add notice if there's only 1 shortcut 2024-01-10 14:48:29 +08:00
Lim Chee Aun 8c6563a671 More contextual copy 2024-01-10 14:48:08 +08:00
Lim Chee Aun ffabd6188d Truncate URLs 2024-01-10 01:48:20 +08:00
Lim Chee Aun d71b1a7e36 Test add "more" icon near timestamp 2024-01-10 01:47:50 +08:00
Lim Chee Aun c47687e2e4 Fix / and ? key shortcuts suddenly not working 2024-01-10 00:03:36 +08:00
Lim Chee Aun 5b0d6dd58b Upgrade dependencies 2024-01-09 23:47:21 +08:00
Lim Chee Aun ecd5c7b91e . (period) keyboard shortcut = load new posts 2024-01-09 23:47:21 +08:00
40 changed files with 1869 additions and 992 deletions

211
package-lock.json generated
View file

@ -8,7 +8,7 @@
"name": "phanpy",
"version": "0.1.0",
"dependencies": {
"@formatjs/intl-localematcher": "~0.5.2",
"@formatjs/intl-localematcher": "~0.5.4",
"@formkit/auto-animate": "~0.8.1",
"@github/text-expander-element": "~2.6.1",
"@iconify-icons/mingcute": "~1.2.9",
@ -23,12 +23,12 @@
"idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~6.5.1",
"masto": "~6.5.2",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.19.3",
"react-hotkeys-hook": "~4.4.1",
"react-hotkeys-hook": "~4.4.4",
"react-intersection-observer": "~9.5.3",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
@ -43,16 +43,16 @@
"valtio": "1.9.0"
},
"devDependencies": {
"@preact/preset-vite": "~2.7.0",
"@preact/preset-vite": "~2.8.1",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.32",
"postcss": "~8.4.33",
"postcss-dark-theme-class": "~1.1.0",
"postcss-preset-env": "~9.3.0",
"twitter-text": "~3.1.0",
"vite": "~5.0.10",
"vite": "~5.0.12",
"vite-plugin-generate-file": "~0.1.1",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.17.4",
"vite-plugin-pwa": "~0.17.5",
"vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",
@ -2899,10 +2899,9 @@
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz",
"integrity": "sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==",
"license": "MIT",
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
"dependencies": {
"tslib": "^2.4.0"
}
@ -3081,11 +3080,10 @@
}
},
"node_modules/@preact/preset-vite": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.7.0.tgz",
"integrity": "sha512-m5N0FVtxbCCDxNk55NGhsRpKJChYcupcuQHzMJc/Bll07IKZKn8amwYciyKFS9haU6AgzDAJ/ewvApr6Qg1DHw==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.8.1.tgz",
"integrity": "sha512-a9KV4opdj17X2gOFuGup0aE+sXYABX/tJi/QDptOrleX4FlnoZgDWvz45tHOdVfrZX+3uvVsIYPHxRsTerkDNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-transform-react-jsx": "^7.22.15",
"@babel/plugin-transform-react-jsx-development": "^7.22.5",
@ -3094,6 +3092,8 @@
"babel-plugin-transform-hook-names": "^1.0.2",
"debug": "^4.3.4",
"kolorist": "^1.8.0",
"magic-string": "0.30.5",
"node-html-parser": "^6.1.10",
"resolve": "^1.22.8"
},
"peerDependencies": {
@ -3101,6 +3101,24 @@
"vite": "2.x || 3.x || 4.x || 5.x"
}
},
"node_modules/@preact/preset-vite/node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
},
"node_modules/@preact/preset-vite/node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@prefresh/babel-plugin": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.0.tgz",
@ -3752,6 +3770,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -4103,6 +4127,34 @@
"postcss": "^8.4"
}
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"dev": true,
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"dev": true,
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssdb": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.9.0.tgz",
@ -4193,6 +4245,61 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dev": true,
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
]
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dev": true,
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"dev": true,
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
@ -4232,6 +4339,18 @@
"dev": true,
"license": "ISC"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-abstract": {
"version": "1.21.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
@ -4811,6 +4930,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"bin": {
"he": "bin/he"
}
},
"node_modules/header-case": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz",
@ -5479,9 +5607,9 @@
}
},
"node_modules/masto": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/masto/-/masto-6.5.1.tgz",
"integrity": "sha512-jQTWSNmwtKPQ/H9gW6dIvX4cYIQZE5tKwFFwv6/hcuwqHuYaNHMMU51Qt9pqC1y9NZshivwsMijC9QKUKIiHhg==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/masto/-/masto-6.5.2.tgz",
"integrity": "sha512-JfnG7MSQmhszWnLsvdBuxXc2tcVUyCvlTxnSH/5S+In4dU1tvc1wGhFR87kO+YW8gfDsDw9CHh+AD/z+DbTTfQ==",
"dependencies": {
"change-case": "^4.1.2",
"events-to-async": "^2.0.1",
@ -5613,6 +5741,16 @@
"tslib": "^2.0.3"
}
},
"node_modules/node-html-parser": {
"version": "6.1.12",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.12.tgz",
"integrity": "sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==",
"dev": true,
"dependencies": {
"css-select": "^5.1.0",
"he": "1.2.0"
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
@ -5630,6 +5768,18 @@
"node": ">=0.10.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dev": true,
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -5783,9 +5933,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"dev": true,
"funding": [
{
@ -6646,10 +6796,9 @@
}
},
"node_modules/react-hotkeys-hook": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz",
"integrity": "sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==",
"license": "MIT",
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.4.tgz",
"integrity": "sha512-wzZmqb/Obr0ds9Myc1sIFPJ52GA/Eeg/vXBWV0HA1LvHlVAW5Va3KB0q6EZNlNSHQWscWZ2K8+6w0GYSie2o7A==",
"peerDependencies": {
"react": ">=16.8.1",
"react-dom": ">=16.8.1"
@ -7624,9 +7773,9 @@
}
},
"node_modules/vite": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
"integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==",
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
"integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",
@ -7705,9 +7854,9 @@
}
},
"node_modules/vite-plugin-pwa": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.4.tgz",
"integrity": "sha512-j9iiyinFOYyof4Zk3Q+DtmYyDVBDAi6PuMGNGq6uGI0pw7E+LNm9e+nQ2ep9obMP/kjdWwzilqUrlfVRj9OobA==",
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.5.tgz",
"integrity": "sha512-UxRNPiJBzh4tqU/vc8G2TxmrUTzT6BqvSzhszLk62uKsf+npXdvLxGDz9C675f4BJi6MbD2tPnJhi5txlMzxbQ==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",

View file

@ -10,7 +10,7 @@
"sourcemap": "npx source-map-explorer dist/assets/*.js"
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.5.2",
"@formatjs/intl-localematcher": "~0.5.4",
"@formkit/auto-animate": "~0.8.1",
"@github/text-expander-element": "~2.6.1",
"@iconify-icons/mingcute": "~1.2.9",
@ -25,12 +25,12 @@
"idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~6.5.1",
"masto": "~6.5.2",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.19.3",
"react-hotkeys-hook": "~4.4.1",
"react-hotkeys-hook": "~4.4.4",
"react-intersection-observer": "~9.5.3",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
@ -45,16 +45,16 @@
"valtio": "1.9.0"
},
"devDependencies": {
"@preact/preset-vite": "~2.7.0",
"@preact/preset-vite": "~2.8.1",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.32",
"postcss": "~8.4.33",
"postcss-dark-theme-class": "~1.1.0",
"postcss-preset-env": "~9.3.0",
"twitter-text": "~3.1.0",
"vite": "~5.0.10",
"vite": "~5.0.12",
"vite-plugin-generate-file": "~0.1.1",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.17.4",
"vite-plugin-pwa": "~0.17.5",
"vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",

View file

@ -645,6 +645,16 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
var(--bg-faded-color)
);
}
@keyframes summary-fade {
0% {
opacity: 0;
transform: translateY(-8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.timeline.contextual > li .replies[open] > .replies-summary {
border-bottom-left-radius: 0;
@ -659,6 +669,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.replies-summary-chevron {
transform: rotate(180deg);
}
+ * {
animation: summary-fade 0.3s ease-out both;
}
}
.timeline.contextual > li .replies .replies-summary[hidden] {
display: none;
@ -933,7 +947,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
background: linear-gradient(
to bottom right,
var(--carousel-faded-color),
transparent 150%
transparent
);
position: relative;
container-type: inline-size;
@ -948,7 +962,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
var(--carousel-faded-color),
transparent
),
linear-gradient(to top, var(--bg-color), transparent 64px);
linear-gradient(to top, var(--bg-color) 8px, transparent 64px);
background-repeat: no-repeat;
background-position: bottom center;
}
@ -1059,6 +1073,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.ui-state {
padding: 16px;
text-align: center;
.icon {
vertical-align: middle;
}
}
.status-carousel-link {
@ -1659,6 +1677,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
svg {
contain: none;
width: 100%;
height: 100%;
}
}

View file

@ -14,7 +14,7 @@ import { subscribe } from 'valtio';
import BackgroundService from './components/background-service';
import ComposeButton from './components/compose-button';
import { ICONS } from './components/icon';
import { ICONS } from './components/ICONS';
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
import Loader from './components/loader';
import Modals from './components/modals';

103
src/components/ICONS.jsx Normal file
View file

@ -0,0 +1,103 @@
export const ICONS = {
x: () => import('@iconify-icons/mingcute/close-line'),
heart: () => import('@iconify-icons/mingcute/heart-line'),
bookmark: () => import('@iconify-icons/mingcute/bookmark-line'),
'check-circle': () => import('@iconify-icons/mingcute/check-circle-line'),
'x-circle': () => import('@iconify-icons/mingcute/close-circle-line'),
transfer: () => import('@iconify-icons/mingcute/transfer-4-line'),
rocket: () => import('@iconify-icons/mingcute/rocket-line'),
'arrow-left': () => import('@iconify-icons/mingcute/arrow-left-line'),
'arrow-right': () => import('@iconify-icons/mingcute/arrow-right-line'),
'arrow-up': () => import('@iconify-icons/mingcute/arrow-up-line'),
'arrow-down': () => import('@iconify-icons/mingcute/arrow-down-line'),
earth: () => import('@iconify-icons/mingcute/earth-line'),
lock: () => import('@iconify-icons/mingcute/lock-line'),
unlock: () => import('@iconify-icons/mingcute/unlock-line'),
'eye-close': () => import('@iconify-icons/mingcute/eye-close-line'),
'eye-open': () => import('@iconify-icons/mingcute/eye-2-line'),
message: () => import('@iconify-icons/mingcute/mail-line'),
comment: () => import('@iconify-icons/mingcute/chat-3-line'),
comment2: () => import('@iconify-icons/mingcute/comment-2-line'),
home: () => import('@iconify-icons/mingcute/home-3-line'),
notification: () => import('@iconify-icons/mingcute/notification-line'),
follow: () => import('@iconify-icons/mingcute/user-follow-line'),
'follow-add': () => import('@iconify-icons/mingcute/user-add-line'),
poll: [() => import('@iconify-icons/mingcute/chart-bar-line'), '90deg'],
pencil: () => import('@iconify-icons/mingcute/pencil-line'),
quill: () => import('@iconify-icons/mingcute/quill-pen-line'),
at: () => import('@iconify-icons/mingcute/at-line'),
attachment: () => import('@iconify-icons/mingcute/attachment-line'),
upload: () => import('@iconify-icons/mingcute/upload-3-line'),
gear: () => import('@iconify-icons/mingcute/settings-3-line'),
more: () => import('@iconify-icons/mingcute/more-3-line'),
more2: () => import('@iconify-icons/mingcute/more-1-fill'),
external: () => import('@iconify-icons/mingcute/external-link-line'),
popout: () => import('@iconify-icons/mingcute/external-link-line'),
popin: [() => import('@iconify-icons/mingcute/external-link-line'), '180deg'],
plus: () => import('@iconify-icons/mingcute/add-circle-line'),
'chevron-left': () => import('@iconify-icons/mingcute/left-line'),
'chevron-right': () => import('@iconify-icons/mingcute/right-line'),
'chevron-down': () => import('@iconify-icons/mingcute/down-line'),
reply: [
() => import('@iconify-icons/mingcute/share-forward-line'),
'180deg',
'horizontal',
],
thread: () => import('@iconify-icons/mingcute/route-line'),
group: () => import('@iconify-icons/mingcute/group-line'),
bot: () => import('@iconify-icons/mingcute/android-2-line'),
menu: () => import('@iconify-icons/mingcute/rows-4-line'),
list: () => import('@iconify-icons/mingcute/list-check-line'),
search: () => import('@iconify-icons/mingcute/search-2-line'),
hashtag: () => import('@iconify-icons/mingcute/hashtag-line'),
info: () => import('@iconify-icons/mingcute/information-line'),
shortcut: () => import('@iconify-icons/mingcute/lightning-line'),
user: () => import('@iconify-icons/mingcute/user-4-line'),
following: () => import('@iconify-icons/mingcute/walk-line'),
pin: () => import('@iconify-icons/mingcute/pin-line'),
unpin: [() => import('@iconify-icons/mingcute/pin-line'), '180deg'],
bus: () => import('@iconify-icons/mingcute/bus-2-line'),
link: () => import('@iconify-icons/mingcute/link-2-line'),
history: () => import('@iconify-icons/mingcute/history-line'),
share: () => import('@iconify-icons/mingcute/share-2-line'),
sparkles: () => import('@iconify-icons/mingcute/sparkles-line'),
sparkles2: () => import('@iconify-icons/mingcute/sparkles-2-line'),
exit: () => import('@iconify-icons/mingcute/exit-line'),
translate: () => import('@iconify-icons/mingcute/translate-line'),
play: () => import('@iconify-icons/mingcute/play-fill'),
trash: () => import('@iconify-icons/mingcute/delete-2-line'),
mute: () => import('@iconify-icons/mingcute/volume-mute-line'),
unmute: () => import('@iconify-icons/mingcute/volume-line'),
block: () => import('@iconify-icons/mingcute/forbid-circle-line'),
unblock: [
() => import('@iconify-icons/mingcute/forbid-circle-line'),
'180deg',
],
flag: () => import('@iconify-icons/mingcute/flag-4-line'),
time: () => import('@iconify-icons/mingcute/time-line'),
refresh: () => import('@iconify-icons/mingcute/refresh-2-line'),
emoji2: () => import('@iconify-icons/mingcute/emoji-2-line'),
filter: () => import('@iconify-icons/mingcute/filter-2-line'),
chart: () => import('@iconify-icons/mingcute/chart-line-line'),
react: () => import('@iconify-icons/mingcute/react-line'),
layout4: () => import('@iconify-icons/mingcute/layout-4-line'),
layout5: () => import('@iconify-icons/mingcute/layout-5-line'),
announce: () => import('@iconify-icons/mingcute/announcement-line'),
alert: () => import('@iconify-icons/mingcute/alert-line'),
round: () => import('@iconify-icons/mingcute/round-fill'),
'arrow-up-circle': () =>
import('@iconify-icons/mingcute/arrow-up-circle-line'),
'arrow-down-circle': () =>
import('@iconify-icons/mingcute/arrow-down-circle-line'),
clipboard: () => import('@iconify-icons/mingcute/clipboard-line'),
'account-edit': () => import('@iconify-icons/mingcute/user-edit-line'),
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
media: () => import('@iconify-icons/mingcute/photo-album-line'),
speak: () => import('@iconify-icons/mingcute/radar-line'),
building: () => import('@iconify-icons/mingcute/building-5-line'),
history: () => import('@iconify-icons/mingcute/history-2-line'),
document: () => import('@iconify-icons/mingcute/document-line'),
};

View file

@ -61,6 +61,7 @@ function AccountBlock({
note,
group,
followersCount,
createdAt,
} = account;
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
if (accountInstance) {
@ -188,6 +189,21 @@ function AccountBlock({
/>
</span>
)}
{!bot &&
!group &&
!hasRelationship &&
!followersCount &&
!verifiedField &&
!!createdAt && (
<span class="created-at">
Joined{' '}
<time datetime={createdAt}>
{niceDateTime(createdAt, {
hideTime: true,
})}
</time>
</span>
)}
</div>
)}
</span>

View file

@ -59,7 +59,11 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
return result.accounts[0];
} else if (/https?:\/\/[^/]+\/@/.test(account)) {
const accountURL = new URL(account);
const acct = accountURL.pathname.replace(/^\//, '');
const { hostname, pathname } = accountURL;
const acct =
pathname.replace(/^\//, '').replace(/\/$/, '') +
'@' +
hostname;
const result = await masto.v2.search.fetch({
q: acct,
type: 'accounts',

View file

@ -133,7 +133,14 @@ const SCAN_RE = new RegExp(
function highlightText(text, { maxCharacters = Infinity }) {
// Accept text string, return formatted HTML string
let html = text;
// Escape all HTML special characters
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
// Exceeded characters limit
const { composerCharacterCount } = states;
let leftoverHTML = '';

View file

@ -22,6 +22,10 @@
iframe {
pointer-events: auto;
max-width: 100%;
max-height: 100%;
width: max(var(--width), 480px);
height: auto;
aspect-ratio: var(--aspect-ratio);
}
}
}

View file

@ -2,7 +2,7 @@ import './embed-modal.css';
import Icon from './icon';
function EmbedModal({ html, url, onClose = () => {} }) {
function EmbedModal({ html, url, width, height, onClose = () => {} }) {
return (
<div class="embed-modal-container">
<div class="top-controls">
@ -20,7 +20,15 @@ function EmbedModal({ html, url, onClose = () => {} }) {
</a>
)}
</div>
<div class="embed-content" dangerouslySetInnerHTML={{ __html: html }} />
<div
class="embed-content"
dangerouslySetInnerHTML={{ __html: html }}
style={{
'--width': width + 'px',
'--height': height + 'px',
'--aspect-ratio': `${width}/${height}`,
}}
/>
</div>
);
}

View file

@ -1,3 +1,5 @@
import { memo } from 'preact/compat';
function EmojiText({ text, emojis }) {
if (!text) return '';
if (!emojis?.length) return text;
@ -31,4 +33,9 @@ function EmojiText({ text, emojis }) {
return elements;
}
export default EmojiText;
export default memo(
EmojiText,
(oldProps, newProps) =>
oldProps.text === newProps.text &&
oldProps.emojis?.length === newProps.emojis?.length,
);

View file

@ -1,6 +1,8 @@
import moize from 'moize';
import { useEffect, useRef, useState } from 'preact/hooks';
import { ICONS } from './ICONS';
const SIZES = {
s: 12,
m: 16,
@ -9,115 +11,13 @@ const SIZES = {
xxl: 32,
};
export const ICONS = {
x: () => import('@iconify-icons/mingcute/close-line'),
heart: () => import('@iconify-icons/mingcute/heart-line'),
bookmark: () => import('@iconify-icons/mingcute/bookmark-line'),
'check-circle': () => import('@iconify-icons/mingcute/check-circle-line'),
'x-circle': () => import('@iconify-icons/mingcute/close-circle-line'),
transfer: () => import('@iconify-icons/mingcute/transfer-4-line'),
rocket: () => import('@iconify-icons/mingcute/rocket-line'),
'arrow-left': () => import('@iconify-icons/mingcute/arrow-left-line'),
'arrow-right': () => import('@iconify-icons/mingcute/arrow-right-line'),
'arrow-up': () => import('@iconify-icons/mingcute/arrow-up-line'),
'arrow-down': () => import('@iconify-icons/mingcute/arrow-down-line'),
earth: () => import('@iconify-icons/mingcute/earth-line'),
lock: () => import('@iconify-icons/mingcute/lock-line'),
unlock: () => import('@iconify-icons/mingcute/unlock-line'),
'eye-close': () => import('@iconify-icons/mingcute/eye-close-line'),
'eye-open': () => import('@iconify-icons/mingcute/eye-2-line'),
message: () => import('@iconify-icons/mingcute/mail-line'),
comment: () => import('@iconify-icons/mingcute/chat-3-line'),
comment2: () => import('@iconify-icons/mingcute/comment-2-line'),
home: () => import('@iconify-icons/mingcute/home-3-line'),
notification: () => import('@iconify-icons/mingcute/notification-line'),
follow: () => import('@iconify-icons/mingcute/user-follow-line'),
'follow-add': () => import('@iconify-icons/mingcute/user-add-line'),
poll: [() => import('@iconify-icons/mingcute/chart-bar-line'), '90deg'],
pencil: () => import('@iconify-icons/mingcute/pencil-line'),
quill: () => import('@iconify-icons/mingcute/quill-pen-line'),
at: () => import('@iconify-icons/mingcute/at-line'),
attachment: () => import('@iconify-icons/mingcute/attachment-line'),
upload: () => import('@iconify-icons/mingcute/upload-3-line'),
gear: () => import('@iconify-icons/mingcute/settings-3-line'),
more: () => import('@iconify-icons/mingcute/more-3-line'),
external: () => import('@iconify-icons/mingcute/external-link-line'),
popout: () => import('@iconify-icons/mingcute/external-link-line'),
popin: [() => import('@iconify-icons/mingcute/external-link-line'), '180deg'],
plus: () => import('@iconify-icons/mingcute/add-circle-line'),
'chevron-left': () => import('@iconify-icons/mingcute/left-line'),
'chevron-right': () => import('@iconify-icons/mingcute/right-line'),
'chevron-down': () => import('@iconify-icons/mingcute/down-line'),
reply: [
() => import('@iconify-icons/mingcute/share-forward-line'),
'180deg',
'horizontal',
],
thread: () => import('@iconify-icons/mingcute/route-line'),
group: () => import('@iconify-icons/mingcute/group-line'),
bot: () => import('@iconify-icons/mingcute/android-2-line'),
menu: () => import('@iconify-icons/mingcute/rows-4-line'),
list: () => import('@iconify-icons/mingcute/list-check-line'),
search: () => import('@iconify-icons/mingcute/search-2-line'),
hashtag: () => import('@iconify-icons/mingcute/hashtag-line'),
info: () => import('@iconify-icons/mingcute/information-line'),
shortcut: () => import('@iconify-icons/mingcute/lightning-line'),
user: () => import('@iconify-icons/mingcute/user-4-line'),
following: () => import('@iconify-icons/mingcute/walk-line'),
pin: () => import('@iconify-icons/mingcute/pin-line'),
bus: () => import('@iconify-icons/mingcute/bus-2-line'),
link: () => import('@iconify-icons/mingcute/link-2-line'),
history: () => import('@iconify-icons/mingcute/history-line'),
share: () => import('@iconify-icons/mingcute/share-2-line'),
sparkles: () => import('@iconify-icons/mingcute/sparkles-line'),
sparkles2: () => import('@iconify-icons/mingcute/sparkles-2-line'),
exit: () => import('@iconify-icons/mingcute/exit-line'),
translate: () => import('@iconify-icons/mingcute/translate-line'),
play: () => import('@iconify-icons/mingcute/play-fill'),
trash: () => import('@iconify-icons/mingcute/delete-2-line'),
mute: () => import('@iconify-icons/mingcute/volume-mute-line'),
unmute: () => import('@iconify-icons/mingcute/volume-line'),
block: () => import('@iconify-icons/mingcute/forbid-circle-line'),
unblock: [
() => import('@iconify-icons/mingcute/forbid-circle-line'),
'180deg',
],
flag: () => import('@iconify-icons/mingcute/flag-4-line'),
time: () => import('@iconify-icons/mingcute/time-line'),
refresh: () => import('@iconify-icons/mingcute/refresh-2-line'),
emoji2: () => import('@iconify-icons/mingcute/emoji-2-line'),
filter: () => import('@iconify-icons/mingcute/filter-2-line'),
chart: () => import('@iconify-icons/mingcute/chart-line-line'),
react: () => import('@iconify-icons/mingcute/react-line'),
layout4: () => import('@iconify-icons/mingcute/layout-4-line'),
layout5: () => import('@iconify-icons/mingcute/layout-5-line'),
announce: () => import('@iconify-icons/mingcute/announcement-line'),
alert: () => import('@iconify-icons/mingcute/alert-line'),
round: () => import('@iconify-icons/mingcute/round-fill'),
'arrow-up-circle': () =>
import('@iconify-icons/mingcute/arrow-up-circle-line'),
'arrow-down-circle': () =>
import('@iconify-icons/mingcute/arrow-down-circle-line'),
clipboard: () => import('@iconify-icons/mingcute/clipboard-line'),
'account-edit': () => import('@iconify-icons/mingcute/user-edit-line'),
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
media: () => import('@iconify-icons/mingcute/photo-album-line'),
speak: () => import('@iconify-icons/mingcute/radar-line'),
building: () => import('@iconify-icons/mingcute/building-5-line'),
};
const ICONDATA = {};
// Memoize the dangerouslySetInnerHTML of the SVGs
const SVGICon = moize(
function ({ size, width, height, body, rotate, flip }) {
function ({ width, height, body, rotate, flip }) {
return (
<svg
width={size}
height={size}
viewBox={`0 0 ${width} ${height}`}
dangerouslySetInnerHTML={{ __html: body }}
style={{
@ -131,6 +31,8 @@ const SVGICon = moize(
{
isShallowEqual: true,
maxSize: Object.keys(ICONS).length,
matchesArg: (cacheKeyArg, keyArg) =>
cacheKeyArg.icon === keyArg.icon && cacheKeyArg.body === keyArg.body,
},
);
@ -191,7 +93,7 @@ function Icon({
// }}
// />
<SVGICon
size={iconSize}
icon={icon}
width={iconData.width}
height={iconData.height}
body={iconData.body}

View file

@ -17,7 +17,7 @@ export default memo(function KeyboardShortcutsHelp() {
}
useHotkeys(
'?, shift+?',
'?, shift+?, shift+slash',
(e) => {
console.log('help');
states.showKeyboardShortcutsHelp = true;
@ -71,6 +71,10 @@ export default memo(function KeyboardShortcutsHelp() {
</>
),
},
{
action: 'Load new posts',
keys: <kbd>.</kbd>,
},
{
action: 'Open post details',
keys: (

View file

@ -273,7 +273,7 @@ function MediaModal({
<span>
<Menu2
overflow="auto"
align="end"
align="center"
position="anchor"
gap={4}
menuClassName="glass-menu"

View file

@ -151,11 +151,18 @@ function Media({
[to],
);
const remoteMediaURLObj = remoteMediaURL ? new URL(remoteMediaURL) : null;
const isVideoMaybe =
type === 'unknown' &&
/\.(mp4|m4a|m4p|m4b|m4r|m4v|mov|webm)$/i.test(remoteMediaURL);
remoteMediaURLObj &&
/\.(mp4|m4r|m4v|mov|webm)$/i.test(remoteMediaURLObj.pathname);
const isAudioMaybe =
type === 'unknown' &&
remoteMediaURLObj &&
/\.(mp3|ogg|wav|m4a|m4p|m4b)$/i.test(remoteMediaURLObj.pathname);
const isImage =
type === 'image' || (type === 'unknown' && previewUrl && !isVideoMaybe);
type === 'image' ||
(type === 'unknown' && previewUrl && !isVideoMaybe && !isAudioMaybe);
const parentRef = useRef();
const [imageSmallerThanParent, setImageSmallerThanParent] = useState(false);
@ -476,7 +483,7 @@ function Media({
</Parent>
</Figure>
);
} else if (type === 'audio') {
} else if (type === 'audio' || isAudioMaybe) {
const formattedDuration = formatDuration(original.duration);
return (
<Figure>
@ -499,6 +506,12 @@ function Media({
height={height}
data-orientation={orientation}
loading="lazy"
onError={(e) => {
try {
// Remove self if broken
e.target?.remove?.();
} catch (e) {}
}}
/>
) : null}
{!showOriginal && (

View file

@ -210,6 +210,8 @@ export default function Modals() {
<EmbedModal
html={snapStates.showEmbedModal.html}
url={snapStates.showEmbedModal.url}
width={snapStates.showEmbedModal.width}
height={snapStates.showEmbedModal.height}
onClose={() => {
states.showEmbedModal = false;
}}

View file

@ -97,4 +97,9 @@ function NameText({
);
}
export default memo(NameText);
export default memo(NameText, (oldProps, newProps) => {
// Only care about account.id, the other props usually don't change
const { account } = oldProps;
const { account: newAccount } = newProps;
return account?.acct === newAccount?.acct;
});

View file

@ -292,7 +292,12 @@ function Notification({
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
}
>
<Status status={status} size="s" />
<Status
status={status}
size="s"
previewMode
allowContextMenu
/>
</TruncatedLink>
</li>
))}
@ -326,9 +331,19 @@ function Notification({
}
>
{isStatic ? (
<Status status={actualStatus} size="s" />
<Status
status={actualStatus}
size="s"
previewMode
allowContextMenu
/>
) : (
<Status statusID={actualStatusID} size="s" />
<Status
statusID={actualStatusID}
size="s"
previewMode
allowContextMenu
/>
)}
</TruncatedLink>
)}

View file

@ -8,6 +8,7 @@ import dayjs from 'dayjs';
import dayjsTwitter from 'dayjs-twitter';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useMemo } from 'preact/hooks';
dayjs.extend(dayjsTwitter);
dayjs.extend(localizedFormat);
@ -17,23 +18,25 @@ const dtf = new Intl.DateTimeFormat();
export default function RelativeTime({ datetime, format }) {
if (!datetime) return null;
const date = dayjs(datetime);
let dateStr;
if (format === 'micro') {
// If date <= 1 day ago or day is within this year
const now = dayjs();
const dayDiff = now.diff(date, 'day');
if (dayDiff <= 1 || now.year() === date.year()) {
dateStr = date.twitter();
} else {
dateStr = dtf.format(date.toDate());
const date = useMemo(() => dayjs(datetime), [datetime]);
const dateStr = useMemo(() => {
if (format === 'micro') {
// If date <= 1 day ago or day is within this year
const now = dayjs();
const dayDiff = now.diff(date, 'day');
if (dayDiff <= 1 || now.year() === date.year()) {
return date.twitter();
} else {
return dtf.format(date.toDate());
}
}
} else {
dateStr = date.fromNow();
}
return date.fromNow();
}, [date, format]);
const dt = useMemo(() => date.toISOString(), [date]);
const title = useMemo(() => date.format('LLLL'), [date]);
return (
<time datetime={date.toISOString()} title={date.format('LLLL')}>
<time datetime={dt} title={title}>
{dateStr}
</time>
);

View file

@ -11,7 +11,7 @@ export default memo(function SearchCommand({ onClose = () => {} }) {
const searchFormRef = useRef(null);
useHotkeys(
'/',
['Slash', '/'],
(e) => {
setShowSearch(true);
setTimeout(() => {

View file

@ -73,6 +73,7 @@ const SearchForm = forwardRef((props, ref) => {
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
onSearch={(e) => {
if (!e.target.value) {
setSearchParams({});
@ -84,6 +85,9 @@ const SearchForm = forwardRef((props, ref) => {
}}
onFocus={() => {
setSearchMenuOpen(true);
formRef.current
?.querySelector('.search-popover-item')
?.classList.add('focus');
}}
onBlur={() => {
setTimeout(() => {
@ -178,8 +182,33 @@ const SearchForm = forwardRef((props, ref) => {
}}
/>
<div class="search-popover" hidden={!searchMenuOpen || !query}>
{/* {!!query && (
<Link
to={`/search?q=${encodeURIComponent(query)}`}
class="search-popover-item focus"
onClick={(e) => {
props?.onSubmit?.(e);
}}
>
<Icon icon="search" />
<span>{query}</span>
</Link>
)} */}
{!!query &&
[
{
label: (
<>
{query}{' '}
<small class="insignificant">
accounts, hashtags &amp; posts
</small>
</>
),
to: `/search?q=${encodeURIComponent(query)}`,
top: !type && !/\s/.test(query),
hidden: !!type,
},
{
label: (
<>
@ -188,6 +217,8 @@ const SearchForm = forwardRef((props, ref) => {
),
to: `/search?q=${encodeURIComponent(query)}&type=statuses`,
hidden: /^https?:/.test(query),
top: /\s/.test(query),
icon: 'document',
},
{
label: (
@ -200,6 +231,7 @@ const SearchForm = forwardRef((props, ref) => {
/^@/.test(query) || /^https?:/.test(query) || /\s/.test(query),
top: /^#/.test(query),
type: 'link',
icon: 'hashtag',
},
{
label: (
@ -219,6 +251,7 @@ const SearchForm = forwardRef((props, ref) => {
</>
),
to: `/search?q=${encodeURIComponent(query)}&type=accounts`,
icon: 'group',
},
]
.sort((a, b) => {
@ -226,17 +259,18 @@ const SearchForm = forwardRef((props, ref) => {
if (!a.top && b.top) return 1;
return 0;
})
.map(({ label, to, hidden, type }) => (
.filter(({ hidden }) => !hidden)
.map(({ label, to, icon, type }, i) => (
<Link
to={to}
class="search-popover-item"
hidden={hidden}
class={`search-popover-item ${i === 0 ? 'focus' : ''}`}
// hidden={hidden}
onClick={(e) => {
props?.onSubmit?.(e);
}}
>
<Icon
icon={type === 'link' ? 'arrow-right' : 'search'}
icon={icon || (type === 'link' ? 'arrow-right' : 'search')}
class="more-insignificant"
/>
<span>{label}</span>{' '}

View file

@ -36,7 +36,7 @@
#shortcuts-settings-container .shortcuts-view-mode {
display: flex;
align-items: center;
align-items: stretch;
gap: 2px;
margin: 8px 0 0;
}
@ -52,6 +52,7 @@
gap: 8px;
flex-direction: column;
align-items: center;
justify-content: center;
}
#shortcuts-settings-container .shortcuts-view-mode label:first-child {
border-top-left-radius: 16px;

View file

@ -170,7 +170,7 @@ export const SHORTCUTS_META = {
},
search: {
id: 'search',
title: ({ query }) => (query ? `"${query}"` : 'Search'),
title: ({ query }) => (query ? `${query}` : 'Search'),
path: ({ query }) =>
query
? `/search?q=${encodeURIComponent(query)}&type=statuses`
@ -279,92 +279,93 @@ function ShortcutsSettings({ onClose }) {
})}
</div>
{shortcuts.length > 0 ? (
<ol class="shortcuts-list" ref={shortcutsListParent}>
{shortcuts.filter(Boolean).map((shortcut, i) => {
// const key = i + Object.values(shortcut);
const key = Object.values(shortcut).join('-');
const { type } = shortcut;
if (!SHORTCUTS_META[type]) return null;
let { icon, title, subtitle, excludeViewMode } =
SHORTCUTS_META[type];
if (typeof title === 'function') {
title = title(shortcut, i);
}
if (typeof subtitle === 'function') {
subtitle = subtitle(shortcut, i);
}
if (typeof icon === 'function') {
icon = icon(shortcut, i);
}
if (typeof excludeViewMode === 'function') {
excludeViewMode = excludeViewMode(shortcut, i);
}
const excludedViewMode = excludeViewMode?.includes(
snapStates.settings.shortcutsViewMode,
);
return (
<li key={key}>
<Icon icon={icon} />
<span class="shortcut-text">
<AsyncText>{title}</AsyncText>
{subtitle && (
<>
{' '}
<small class="ib insignificant">{subtitle}</small>
</>
)}
{excludedViewMode && (
<span class="tag">
Not available in current view mode
</span>
)}
</span>
<span class="shortcut-actions">
<button
type="button"
class="plain small"
disabled={i === 0}
onClick={() => {
const shortcutsArr = Array.from(states.shortcuts);
if (i > 0) {
const temp = states.shortcuts[i - 1];
shortcutsArr[i - 1] = shortcut;
shortcutsArr[i] = temp;
states.shortcuts = shortcutsArr;
}
}}
>
<Icon icon="arrow-up" alt="Move up" />
</button>
<button
type="button"
class="plain small"
disabled={i === shortcuts.length - 1}
onClick={() => {
const shortcutsArr = Array.from(states.shortcuts);
if (i < states.shortcuts.length - 1) {
const temp = states.shortcuts[i + 1];
shortcutsArr[i + 1] = shortcut;
shortcutsArr[i] = temp;
states.shortcuts = shortcutsArr;
}
}}
>
<Icon icon="arrow-down" alt="Move down" />
</button>
<button
type="button"
class="plain small"
onClick={() => {
setShowForm({
shortcut,
shortcutIndex: i,
});
}}
>
<Icon icon="pencil" alt="Edit" />
</button>
{/* <button
<>
<ol class="shortcuts-list" ref={shortcutsListParent}>
{shortcuts.filter(Boolean).map((shortcut, i) => {
// const key = i + Object.values(shortcut);
const key = Object.values(shortcut).join('-');
const { type } = shortcut;
if (!SHORTCUTS_META[type]) return null;
let { icon, title, subtitle, excludeViewMode } =
SHORTCUTS_META[type];
if (typeof title === 'function') {
title = title(shortcut, i);
}
if (typeof subtitle === 'function') {
subtitle = subtitle(shortcut, i);
}
if (typeof icon === 'function') {
icon = icon(shortcut, i);
}
if (typeof excludeViewMode === 'function') {
excludeViewMode = excludeViewMode(shortcut, i);
}
const excludedViewMode = excludeViewMode?.includes(
snapStates.settings.shortcutsViewMode,
);
return (
<li key={key}>
<Icon icon={icon} />
<span class="shortcut-text">
<AsyncText>{title}</AsyncText>
{subtitle && (
<>
{' '}
<small class="ib insignificant">{subtitle}</small>
</>
)}
{excludedViewMode && (
<span class="tag">
Not available in current view mode
</span>
)}
</span>
<span class="shortcut-actions">
<button
type="button"
class="plain small"
disabled={i === 0}
onClick={() => {
const shortcutsArr = Array.from(states.shortcuts);
if (i > 0) {
const temp = states.shortcuts[i - 1];
shortcutsArr[i - 1] = shortcut;
shortcutsArr[i] = temp;
states.shortcuts = shortcutsArr;
}
}}
>
<Icon icon="arrow-up" alt="Move up" />
</button>
<button
type="button"
class="plain small"
disabled={i === shortcuts.length - 1}
onClick={() => {
const shortcutsArr = Array.from(states.shortcuts);
if (i < states.shortcuts.length - 1) {
const temp = states.shortcuts[i + 1];
shortcutsArr[i + 1] = shortcut;
shortcutsArr[i] = temp;
states.shortcuts = shortcutsArr;
}
}}
>
<Icon icon="arrow-down" alt="Move down" />
</button>
<button
type="button"
class="plain small"
onClick={() => {
setShowForm({
shortcut,
shortcutIndex: i,
});
}}
>
<Icon icon="pencil" alt="Edit" />
</button>
{/* <button
type="button"
class="plain small"
onClick={() => {
@ -373,11 +374,21 @@ function ShortcutsSettings({ onClose }) {
>
<Icon icon="x" alt="Remove" />
</button> */}
</span>
</li>
);
})}
</ol>
</span>
</li>
);
})}
</ol>
{shortcuts.length === 1 &&
snapStates.settings.shortcutsViewMode !== 'float-button' && (
<div class="ui-state insignificant">
<Icon icon="info" />{' '}
<small>
Add more than one shortcut/column to make this work.
</small>
</div>
)}
</>
) : (
<div class="ui-state insignificant">
<p>No shortcuts yet. Tap on the Add shortcut button.</p>
@ -428,7 +439,12 @@ function ShortcutsSettings({ onClose }) {
disabled={shortcuts.length >= SHORTCUTS_LIMIT}
onClick={() => setShowForm(true)}
>
<Icon icon="plus" /> <span>Add shortcut</span>
<Icon icon="plus" />{' '}
<span>
{snapStates.settings.shortcutsViewMode === 'multi-column'
? 'Add column…'
: 'Add shortcut…'}
</span>
</button>
</p>
</main>

View file

@ -206,7 +206,7 @@
.status-card:not(.status-carousel .status)
:is(.content, .poll, .media-container) {
max-height: 160px !important;
overflow: clip;
overflow: hidden;
}
.status.small:not(.status-carousel .status, .status.large .status)
.status-card
@ -290,7 +290,7 @@
transition: all 0.2s ease-out;
}
.status.filtered:hover :is(.status-filtered-info-1, .status-filtered-info-2) {
transition-delay: 0.5s;
transition-delay: 1.5s;
}
.status.filtered .status-filtered-info-1 {
opacity: 0.5;
@ -330,6 +330,68 @@
font-size: 90%;
}
.status.compact-reply {
--avatar-size: 20px;
--line-start: 40px;
--line-width: 3px;
--line-end: calc(var(--line-start) + var(--line-width));
display: flex;
gap: 12px;
--top-padding: 16px;
padding-top: var(--top-padding);
padding-bottom: 0;
margin-bottom: calc(-1 * var(--top-padding) / 2);
background-image: linear-gradient(
160deg,
transparent 2.5%,
var(--reply-to-faded-color) 10%,
transparent
);
background-repeat: no-repeat;
background-size: 100% calc(100% - var(--top-padding) / 2);
> * {
opacity: 0.65;
transition: opacity 1s ease-out;
}
.status-link:hover & > * {
opacity: 1;
}
&:before {
content: '';
position: absolute;
top: calc(var(--top-padding) + var(--avatar-size));
left: var(--line-start);
width: var(--line-width);
height: calc(
100% - var(--top-padding) - var(--avatar-size) + (var(--top-padding) / 2)
);
background-color: var(--comment-line-color);
z-index: 0;
mask-image: linear-gradient(to bottom, #000 8px, transparent);
}
.avatar {
margin-left: calc((50px - var(--avatar-size)) / 2);
justify-self: center;
z-index: 1;
}
.content-compact {
overflow: hidden;
display: -webkit-box;
display: box;
-webkit-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
font-size: 90%;
line-height: var(--avatar-size);
}
}
.status .container {
flex-grow: 1;
min-width: 0;
@ -364,9 +426,8 @@
vertical-align: middle;
}
.status > .container > .meta :is(.time, .edited) {
color: inherit;
color: var(--text-insignificant-color);
text-align: end;
opacity: 0.5;
text-decoration: none;
flex-shrink: 0;
margin-left: 4px;
@ -375,9 +436,21 @@
.status > .container > .meta a.time {
position: relative;
overflow: visible;
display: flex;
align-items: center;
gap: 2px;
font-size: 90%;
.more {
margin-left: 4px;
transition: transform 0.2s ease-out;
}
}
.status > .container > .meta a.time:is(:hover, :focus) {
text-decoration: underline;
.more {
transform: scale(1.2);
color: var(--link-color);
}
}
.status > .container > .meta a.time:active,
.status > .container > .meta a.time.is-open {
@ -643,6 +716,10 @@
display: flex;
gap: 4px;
align-items: center;
.timeline-item-container & {
display: none;
}
}
.status.compact-thread .spoiler-badge {
@ -1232,6 +1309,22 @@ body:has(#modal-container .carousel) .status .media img:hover {
-webkit-box-orient: vertical;
white-space: normal;
}
/* Collapse possible hashtag stuffing */
/* If >= 9 hashtags, collapse */
/* TODO: lower the threshold one day */
.status:not(.large, .contextual .status)
p:not(.hashtag-stuffing):has(.hashtag:nth-of-type(1)):has(
.hashtag:nth-of-type(2)
):has(.hashtag:nth-of-type(3)):has(.hashtag:nth-of-type(4)):has(
.hashtag:nth-of-type(5)
):has(.hashtag:nth-of-type(6)):has(.hashtag:nth-of-type(7)):has(
.hashtag:nth-of-type(8)
):has(.hashtag:nth-of-type(9)) {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.media-figure-multiple {
margin: 0;
@ -1755,6 +1848,87 @@ a.card:is(:hover, :focus):visited {
color: var(--green-color);
}
/* ACTIONS */
.status-actions {
display: flex;
position: absolute;
top: 4px;
right: 4px;
background-color: var(--bg-color);
border-radius: 8px;
z-index: 1;
border: 1px solid var(--outline-color);
box-shadow: 0 2px 6px -3px var(--drop-shadow-color);
overflow: clip;
opacity: 0;
pointer-events: none;
transform: translateX(8px);
transform-origin: right center;
transition: all 0.15s ease-out 0.3s, border-color 0.3s ease-out;
button.plain {
color: var(--text-insignificant-color);
backdrop-filter: none;
padding: 10px;
border-radius: 8px;
outline-offset: -5px;
outline: 1px solid transparent;
&:is(:hover, :focus) {
color: var(--text-color);
background-color: var(--bg-faded-color);
filter: none !important;
box-shadow: inset 0 0 0 2px var(--bg-color);
}
&.reblog-button.checked {
color: var(--reblog-color);
outline-color: var(--reblog-color);
}
&.favourite-button.checked {
color: var(--favourite-color);
outline-color: var(--favourite-color);
}
&.bookmark-button.checked {
color: var(--link-color);
outline-color: var(--link-color);
}
}
&:hover {
border-color: var(--outline-hover-color);
}
&:hover,
.status:hover &:not(:hover),
&.open {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
@media (pointer: coarse) {
.status:has(&):hover {
transition: background-color 0.1s ease-out 0.3s;
background-color: var(--bg-faded-blur-color);
}
}
&.open {
button.more-button {
color: var(--text-color);
background-color: var(--outline-color);
box-shadow: inset 0 0 0 2px var(--bg-color);
}
button:not(.more-button) {
opacity: 0.3;
}
}
}
/* BADGE */
.status-badge {
@ -1853,6 +2027,12 @@ a.card:is(:hover, :focus):visited {
font-size: 80%;
}
/* MENU OPEN */
.status-menu-open {
background-color: var(--link-bg-hover-color) !important;
}
/* FILTERED */
#filtered-status-peek {

File diff suppressed because it is too large Load diff

View file

@ -46,6 +46,7 @@ function Timeline({
view,
filterContext,
showFollowedTags,
showReplyParent,
}) {
const snapStates = useSnapshot(states);
const [items, setItems] = useState([]);
@ -84,7 +85,7 @@ function Timeline({
if (boostsCarousel) {
value = groupBoosts(value);
}
value = groupContext(value);
value = groupContext(value, instance);
}
if (pinnedPosts.length) {
value = pinnedPosts.concat(value);
@ -204,6 +205,21 @@ function Timeline({
}
});
const showNewPostsIndicator =
items.length > 0 && uiState !== 'loading' && showNew;
const handleLoadNewPosts = useCallback(() => {
loadItems(true);
scrollableRef.current?.scrollTo({
top: 0,
behavior: 'smooth',
});
}, [loadItems]);
const dotRef = useHotkeys('.', () => {
if (showNewPostsIndicator) {
handleLoadNewPosts();
}
});
// const {
// scrollDirection,
// nearReachStart,
@ -387,24 +403,15 @@ function Timeline({
{!!headerEnd && headerEnd}
</div>
</div>
{items.length > 0 &&
uiState !== 'loading' &&
// !hiddenUI &&
showNew && (
<button
class="updates-button shiny-pill"
type="button"
onClick={() => {
loadItems(true);
scrollableRef.current?.scrollTo({
top: 0,
behavior: 'smooth',
});
}}
>
<Icon icon="arrow-up" /> New posts
</button>
)}
{showNewPostsIndicator && (
<button
class="updates-button shiny-pill"
type="button"
onClick={handleLoadNewPosts}
>
<Icon icon="arrow-up" /> New posts
</button>
)}
</header>
{!!timelineStart && (
<div
@ -426,6 +433,7 @@ function Timeline({
key={status.id + status?._pinned + view}
view={view}
showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent}
/>
))}
{showMore &&
@ -516,6 +524,7 @@ function TimelineItem({
filterContext,
view,
showFollowedTags,
showReplyParent,
}) {
const { id: statusID, reblog, items, type, _pinned } = status;
if (_pinned) useItemID = false;
@ -674,6 +683,7 @@ function TimelineItem({
instance={instance}
enableCommentHint
showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent}
// allowFilters={allowFilters}
/>
) : (
@ -682,6 +692,7 @@ function TimelineItem({
instance={instance}
enableCommentHint
showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent}
// allowFilters={allowFilters}
/>
)}
@ -779,7 +790,7 @@ function StatusCarousel({ title, class: className, children }) {
function TimelineStatusCompact({ status, instance }) {
const snapStates = useSnapshot(states);
const { id, visibility } = status;
const { id, visibility, language } = status;
const statusPeekText = statusPeek(status);
const sKey = statusKey(id, instance);
return (
@ -801,7 +812,12 @@ function TimelineStatusCompact({ status, instance }) {
<Icon icon="thread" size="s" />
</div>
)}
<div class="content-compact" title={statusPeekText}>
<div
class="content-compact"
title={statusPeekText}
lang={language}
dir="auto"
>
{statusPeekText}
{status.sensitive && status.spoilerText && (
<>

View file

@ -959,11 +959,6 @@
"Kabyle",
"Taqbaylit"
],
[
"kmr",
"Kurmanji (Kurdish)",
"Kurmancî"
],
[
"ldn",
"Láadan",

View file

@ -45,19 +45,31 @@ function FollowedHashtags() {
</header>
<main>
{followedHashtags.length > 0 ? (
<ul class="link-list">
{followedHashtags.map((tag) => (
<li>
<Link
to={
instance ? `/${instance}/t/${tag.name}` : `/t/${tag.name}`
}
>
<Icon icon="hashtag" /> <span>{tag.name}</span>
</Link>
</li>
))}
</ul>
<>
<ul class="link-list">
{followedHashtags.map((tag) => (
<li>
<Link
to={
instance
? `/${instance}/t/${tag.name}`
: `/t/${tag.name}`
}
>
<Icon icon="hashtag" /> <span>{tag.name}</span>
</Link>
</li>
))}
</ul>
{followedHashtags.length > 1 && (
<footer class="ui-state">
<small class="insignificant">
{followedHashtags.length} hashtag
{followedHashtags.length === 1 ? '' : 's'}
</small>
</footer>
)}
</>
) : uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />

View file

@ -129,6 +129,7 @@ function Following({ title, path, id, ...props }) {
// allowFilters
filterContext="home"
showFollowedTags
showReplyParent
/>
);
}

View file

@ -104,6 +104,7 @@ function List(props) {
boostsCarousel={snapStates.settings.boostsCarousel}
// allowFilters
filterContext="home"
showReplyParent
// refresh={reloadCount}
headerStart={
<Link to="/l" class="button plain">

View file

@ -61,14 +61,15 @@ function Lists() {
</header>
<main>
{lists.length > 0 ? (
<ul class="link-list">
{lists.map((list) => (
<li>
<Link to={`/l/${list.id}`}>
<span>
<Icon icon="list" /> <span>{list.title}</span>
</span>
{/* <button
<>
<ul class="link-list">
{lists.map((list) => (
<li>
<Link to={`/l/${list.id}`}>
<span>
<Icon icon="list" /> <span>{list.title}</span>
</span>
{/* <button
type="button"
class="plain"
onClick={(e) => {
@ -81,10 +82,19 @@ function Lists() {
>
<Icon icon="pencil" />
</button> */}
</Link>
</li>
))}
</ul>
</Link>
</li>
))}
</ul>
{lists.length > 1 && (
<footer class="ui-state">
<small class="insignificant">
{lists.length} list
{lists.length === 1 ? '' : 's'}
</small>
</footer>
)}
</>
) : uiState === 'loading' ? (
<p class="ui-state">
<Loader />

View file

@ -44,6 +44,18 @@
}
}
#search-page h2 {
a {
.icon {
vertical-align: middle;
transition: transform 0.2s;
}
&:hover .icon {
transform: translateX(4px);
}
}
}
#search-page ul.accounts-list {
display: flex;
flex-wrap: wrap;

View file

@ -174,7 +174,7 @@ function Search({ columnMode, ...props }) {
}, [q, type, instance]);
useHotkeys(
'/',
['/', 'Slash'],
(e) => {
searchFormRef.current?.focus?.();
},
@ -253,7 +253,14 @@ function Search({ columnMode, ...props }) {
{(!type || type === 'accounts') && (
<>
{type !== 'accounts' && (
<h2 class="timeline-header">Accounts</h2>
<h2 class="timeline-header">
Accounts{' '}
<Link
to={`/search?q=${encodeURIComponent(q)}&type=accounts`}
>
<Icon icon="arrow-right" size="l" />
</Link>
</h2>
)}
{accountResults.length > 0 ? (
<>
@ -273,7 +280,9 @@ function Search({ columnMode, ...props }) {
<div class="ui-state">
<Link
class="plain button"
to={`/search?q=${q}&type=accounts`}
to={`/search?q=${encodeURIComponent(
q,
)}&type=accounts`}
>
See more accounts <Icon icon="arrow-right" />
</Link>
@ -295,7 +304,14 @@ function Search({ columnMode, ...props }) {
{(!type || type === 'hashtags') && (
<>
{type !== 'hashtags' && (
<h2 class="timeline-header">Hashtags</h2>
<h2 class="timeline-header">
Hashtags{' '}
<Link
to={`/search?q=${encodeURIComponent(q)}&type=hashtags`}
>
<Icon icon="arrow-right" size="l" />
</Link>
</h2>
)}
{hashtagResults.length > 0 ? (
<>
@ -331,7 +347,9 @@ function Search({ columnMode, ...props }) {
<div class="ui-state">
<Link
class="plain button"
to={`/search?q=${q}&type=hashtags`}
to={`/search?q=${encodeURIComponent(
q,
)}&type=hashtags`}
>
See more hashtags <Icon icon="arrow-right" />
</Link>
@ -353,7 +371,14 @@ function Search({ columnMode, ...props }) {
{(!type || type === 'statuses') && (
<>
{type !== 'statuses' && (
<h2 class="timeline-header">Posts</h2>
<h2 class="timeline-header">
Posts{' '}
<Link
to={`/search?q=${encodeURIComponent(q)}&type=statuses`}
>
<Icon icon="arrow-right" size="l" />
</Link>
</h2>
)}
{statusResults.length > 0 ? (
<>
@ -377,7 +402,9 @@ function Search({ columnMode, ...props }) {
<div class="ui-state">
<Link
class="plain button"
to={`/search?q=${q}&type=statuses`}
to={`/search?q=${encodeURIComponent(
q,
)}&type=statuses`}
>
See more posts <Icon icon="arrow-right" />
</Link>

View file

@ -643,7 +643,7 @@ function PushNotificationsSection({ onClose }) {
const { instance } = api();
const [uiState, setUIState] = useState('default');
const pushFormRef = useRef();
const [allowNofitications, setAllowNotifications] = useState(false);
const [allowNotifications, setAllowNotifications] = useState(false);
const [needRelogin, setNeedRelogin] = useState(false);
const previousPolicyRef = useRef();
useEffect(() => {
@ -689,7 +689,7 @@ function PushNotificationsSection({ onClose }) {
ref={pushFormRef}
onChange={() => {
const values = Object.fromEntries(new FormData(pushFormRef.current));
const allowNofitications = !!values['policy-allow'];
const allowNotifications = !!values['policy-allow'];
const params = {
policy: values.policy,
data: {
@ -718,9 +718,13 @@ function PushNotificationsSection({ onClose }) {
});
const policyChanged = previousPolicyRef.current !== params.policy;
console.log('PN Form', { values, allowNofitications, params });
console.log('PN Form', {
values,
allowNotifications: allowNotifications,
params,
});
if (allowNofitications && alertsCount > 0) {
if (allowNotifications && alertsCount > 0) {
if (policyChanged) {
console.debug('Policy changed.');
removeSubscription()
@ -754,7 +758,7 @@ function PushNotificationsSection({ onClose }) {
type="checkbox"
disabled={isLoading || needRelogin}
name="policy-allow"
checked={allowNofitications}
checked={allowNotifications}
onChange={async (e) => {
const { checked } = e.target;
if (checked) {
@ -778,7 +782,7 @@ function PushNotificationsSection({ onClose }) {
Allow from{' '}
<select
name="policy"
disabled={isLoading || needRelogin || !allowNofitications}
disabled={isLoading || needRelogin || !allowNotifications}
>
{[
{
@ -803,7 +807,7 @@ function PushNotificationsSection({ onClose }) {
style={{
width: '100%',
}}
hidden={!allowNofitications}
hidden={!allowNotifications}
>
<div class="shazam-container-inner">
<div class="sub-section">

View file

@ -1,16 +1,32 @@
.status-deck header {
white-space: nowrap;
.status-deck {
header {
white-space: nowrap;
}
header h1 {
min-width: 0;
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
align-self: stretch;
}
header h1 .deck-back {
margin-left: -16px;
}
.button-refresh .icon {
animation: spin 1s linear;
}
.button-refresh:is(:hover, :focus) .icon {
transition: transform 1s linear;
transform: rotate(360deg);
}
}
.status-deck header h1 {
min-width: 0;
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
align-self: stretch;
}
.status-deck header h1 .deck-back {
margin-left: -16px;
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.hero-heading {

View file

@ -245,6 +245,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
}, [id, uiState !== 'loading']);
const scrollOffsets = useRef();
const lastInitContextTS = useRef();
const initContext = ({ reloadHero } = {}) => {
console.debug('initContext', id);
setUIState('loading');
@ -432,12 +433,31 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
}
})();
lastInitContextTS.current = Date.now();
return () => {
clearTimeout(heroTimer);
};
};
useEffect(initContext, [id, masto]);
const [showRefresh, setShowRefresh] = useState(false);
useEffect(() => {
let interval = setInterval(() => {
const now = Date.now();
if (
lastInitContextTS.current &&
now - lastInitContextTS.current >= 60_000
) {
setShowRefresh(true);
}
}, 60_000); // 1 minute
return () => {
clearInterval(interval);
};
}, []);
useLayoutEffect(() => {
if (!statuses.length) return;
console.debug('STATUSES', statuses);
@ -845,6 +865,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
enableTranslate
onMediaClick={handleMediaClick}
onStatusLinkClick={handleStatusLinkClick}
showActionsBar={!!descendant}
/>
)}
{ancestor && repliesCount > 1 && (
@ -1094,6 +1115,18 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
>
<Icon icon="layout4" size="l" />
</button>
{showRefresh && (
<button
type="button"
class="plain button-refresh"
onClick={() => {
states.reloadStatusPage++;
setShowRefresh(false);
}}
>
<Icon icon="refresh" size="l" />
</button>
)}
<Menu2
align="end"
portal={{
@ -1400,6 +1433,7 @@ function SubComments({
size="s"
enableTranslate
onMediaClick={handleMediaClick}
showActionsBar
/>
{!r.replies?.length && r.repliesCount > 0 && (
<div class="replies-link">

View file

@ -37,6 +37,7 @@ function _enhanceContent(content, opts = {}) {
links.forEach((link) => {
if (/^https?:\/\//i.test(link.textContent.trim())) {
link.classList.add('has-url-text');
shortenLink(link);
}
});
}
@ -287,6 +288,30 @@ const defaultRejectFilter = [
const defaultRejectFilterMap = Object.fromEntries(
defaultRejectFilter.map((nodeName) => [nodeName, true]),
);
const URL_PREFIX_REGEX = /^(https?:\/\/(www\.)?|xmpp:)/;
const URL_DISPLAY_LENGTH = 30;
// Similar to https://github.com/mastodon/mastodon/blob/1666b1955992e16f4605b414c6563ca25b3a3f18/app/lib/text_formatter.rb#L54-L69
function shortenLink(link) {
if (!link || link.querySelector?.('*')) {
return;
}
try {
const url = link.innerText.trim();
const prefix = (url.match(URL_PREFIX_REGEX) || [])[0] || '';
if (!prefix) return;
const displayURL = url.slice(
prefix.length,
prefix.length + URL_DISPLAY_LENGTH,
);
const suffix = url.slice(prefix.length + URL_DISPLAY_LENGTH);
const cutoff = url.slice(prefix.length).length > URL_DISPLAY_LENGTH;
link.innerHTML = `<span class="invisible">${prefix}</span><span class=${
cutoff ? 'ellipsis' : ''
}>${displayURL}</span><span class="invisible">${suffix}</span>`;
} catch (e) {}
}
function extractTextNodes(dom, opts = {}) {
const textNodes = [];
const rejectFilterMap = Object.assign(

View file

@ -9,6 +9,17 @@ function getHTMLText(html) {
div.querySelectorAll('br').forEach((br) => {
br.replaceWith('\n');
});
// MASTODON-SPECIFIC classes
// Remove .invisible
div.querySelectorAll('.invisible').forEach((el) => {
el.remove();
});
// Add at end of .ellipsis
div.querySelectorAll('.ellipsis').forEach((el) => {
el.append('...');
});
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
}

View file

@ -37,6 +37,7 @@ const states = proxy({
unfurledLinks: {},
statusQuotes: {},
statusFollowedTags: {},
statusReply: {},
accounts: {},
routeNotification: null,
// Modals
@ -187,9 +188,19 @@ export function saveStatus(status, instance, opts) {
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
// if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
states.statuses[key] = status;
if (status.reblog) {
const key = statusKey(status.reblog.id, instance);
states.statuses[key] = status.reblog;
if (status.reblog?.id) {
const srKey = statusKey(status.reblog.id, instance);
states.statuses[srKey] = status.reblog;
}
if (status.quote?.id) {
const sKey = statusKey(status.quote.id, instance);
states.statuses[sKey] = status.quote;
states.statusQuotes[key] = [
{
id: status.quote.id,
instance,
},
];
}
});

View file

@ -1,6 +1,8 @@
import { api } from './api';
import { extractTagsFromStatus, getFollowedTags } from './followed-tags';
import pmem from './pmem';
import { fetchRelationships } from './relationships';
import states, { statusKey } from './states';
import states, { saveStatus, statusKey } from './states';
import store from './store';
export function groupBoosts(values) {
@ -81,7 +83,7 @@ export function dedupeBoosts(items, instance) {
return filteredItems;
}
export function groupContext(items) {
export function groupContext(items, instance) {
const contexts = [];
let contextIndex = 0;
items.forEach((item) => {
@ -173,12 +175,44 @@ export function groupContext(items) {
return;
}
}
if (item.inReplyToId && item.inReplyToAccountId !== item.account.id) {
const sKey = statusKey(item.id, instance);
if (!states.statusReply[sKey]) {
// If it's a reply and not a thread
queueMicrotask(async () => {
try {
const { masto } = api({ instance });
// const replyToStatus = await masto.v1.statuses
// .$select(item.inReplyToId)
// .fetch();
const replyToStatus = await fetchStatus(item.inReplyToId, masto);
saveStatus(replyToStatus, instance, {
skipThreading: true,
skipUnfurling: true,
});
states.statusReply[sKey] = {
id: replyToStatus.id,
instance,
};
} catch (e) {
// Silently fail
console.error(e);
}
});
}
}
newItems.push(item);
});
return newItems;
}
const fetchStatus = pmem((statusID, masto) => {
return masto.v1.statuses.$select(statusID).fetch();
});
export async function assignFollowedTags(items, instance) {
const followedTags = await getFollowedTags(); // [{name: 'tag'}, {...}]
if (!followedTags.length) return;
@ -219,7 +253,7 @@ export async function assignFollowedTags(items, instance) {
statusWithFollowedTags.forEach((s) => {
const { item, sKey, followedTags } = s;
const r = relationships[item.account.id];
if (!r.following) {
if (r && !r.following) {
statusFollowedTags[sKey] = followedTags;
}
});