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", "name": "phanpy",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@formatjs/intl-localematcher": "~0.5.2", "@formatjs/intl-localematcher": "~0.5.4",
"@formkit/auto-animate": "~0.8.1", "@formkit/auto-animate": "~0.8.1",
"@github/text-expander-element": "~2.6.1", "@github/text-expander-element": "~2.6.1",
"@iconify-icons/mingcute": "~1.2.9", "@iconify-icons/mingcute": "~1.2.9",
@ -23,12 +23,12 @@
"idb-keyval": "~6.2.1", "idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0", "just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0", "lz-string": "~1.5.0",
"masto": "~6.5.1", "masto": "~6.5.2",
"moize": "~6.1.6", "moize": "~6.1.6",
"p-retry": "~6.2.0", "p-retry": "~6.2.0",
"p-throttle": "~6.1.0", "p-throttle": "~6.1.0",
"preact": "~10.19.3", "preact": "~10.19.3",
"react-hotkeys-hook": "~4.4.1", "react-hotkeys-hook": "~4.4.4",
"react-intersection-observer": "~9.5.3", "react-intersection-observer": "~9.5.3",
"react-quick-pinch-zoom": "~5.1.0", "react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2", "react-router-dom": "6.6.2",
@ -43,16 +43,16 @@
"valtio": "1.9.0" "valtio": "1.9.0"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "~2.7.0", "@preact/preset-vite": "~2.8.1",
"@trivago/prettier-plugin-sort-imports": "~4.3.0", "@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.32", "postcss": "~8.4.33",
"postcss-dark-theme-class": "~1.1.0", "postcss-dark-theme-class": "~1.1.0",
"postcss-preset-env": "~9.3.0", "postcss-preset-env": "~9.3.0",
"twitter-text": "~3.1.0", "twitter-text": "~3.1.0",
"vite": "~5.0.10", "vite": "~5.0.12",
"vite-plugin-generate-file": "~0.1.1", "vite-plugin-generate-file": "~0.1.1",
"vite-plugin-html-config": "~1.0.11", "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", "vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.0.0", "workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0", "workbox-expiration": "~7.0.0",
@ -2899,10 +2899,9 @@
} }
}, },
"node_modules/@formatjs/intl-localematcher": { "node_modules/@formatjs/intl-localematcher": {
"version": "0.5.2", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
"integrity": "sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==", "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
"license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
@ -3081,11 +3080,10 @@
} }
}, },
"node_modules/@preact/preset-vite": { "node_modules/@preact/preset-vite": {
"version": "2.7.0", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.7.0.tgz", "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.8.1.tgz",
"integrity": "sha512-m5N0FVtxbCCDxNk55NGhsRpKJChYcupcuQHzMJc/Bll07IKZKn8amwYciyKFS9haU6AgzDAJ/ewvApr6Qg1DHw==", "integrity": "sha512-a9KV4opdj17X2gOFuGup0aE+sXYABX/tJi/QDptOrleX4FlnoZgDWvz45tHOdVfrZX+3uvVsIYPHxRsTerkDNA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/plugin-transform-react-jsx": "^7.22.15", "@babel/plugin-transform-react-jsx": "^7.22.15",
"@babel/plugin-transform-react-jsx-development": "^7.22.5", "@babel/plugin-transform-react-jsx-development": "^7.22.5",
@ -3094,6 +3092,8 @@
"babel-plugin-transform-hook-names": "^1.0.2", "babel-plugin-transform-hook-names": "^1.0.2",
"debug": "^4.3.4", "debug": "^4.3.4",
"kolorist": "^1.8.0", "kolorist": "^1.8.0",
"magic-string": "0.30.5",
"node-html-parser": "^6.1.10",
"resolve": "^1.22.8" "resolve": "^1.22.8"
}, },
"peerDependencies": { "peerDependencies": {
@ -3101,6 +3101,24 @@
"vite": "2.x || 3.x || 4.x || 5.x" "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": { "node_modules/@prefresh/babel-plugin": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.0.tgz", "resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.0.tgz",
@ -3752,6 +3770,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -4103,6 +4127,34 @@
"postcss": "^8.4" "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": { "node_modules/cssdb": {
"version": "7.9.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.9.0.tgz", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.9.0.tgz",
@ -4193,6 +4245,61 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/dot-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
@ -4232,6 +4339,18 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/es-abstract": {
"version": "1.21.2", "version": "1.21.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
@ -4811,6 +4930,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/header-case": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz",
@ -5479,9 +5607,9 @@
} }
}, },
"node_modules/masto": { "node_modules/masto": {
"version": "6.5.1", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/masto/-/masto-6.5.1.tgz", "resolved": "https://registry.npmjs.org/masto/-/masto-6.5.2.tgz",
"integrity": "sha512-jQTWSNmwtKPQ/H9gW6dIvX4cYIQZE5tKwFFwv6/hcuwqHuYaNHMMU51Qt9pqC1y9NZshivwsMijC9QKUKIiHhg==", "integrity": "sha512-JfnG7MSQmhszWnLsvdBuxXc2tcVUyCvlTxnSH/5S+In4dU1tvc1wGhFR87kO+YW8gfDsDw9CHh+AD/z+DbTTfQ==",
"dependencies": { "dependencies": {
"change-case": "^4.1.2", "change-case": "^4.1.2",
"events-to-async": "^2.0.1", "events-to-async": "^2.0.1",
@ -5613,6 +5741,16 @@
"tslib": "^2.0.3" "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": { "node_modules/node-releases": {
"version": "2.0.13", "version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
@ -5630,6 +5768,18 @@
"node": ">=0.10.0" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -5783,9 +5933,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.32", "version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -6646,10 +6796,9 @@
} }
}, },
"node_modules/react-hotkeys-hook": { "node_modules/react-hotkeys-hook": {
"version": "4.4.1", "version": "4.4.4",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.4.tgz",
"integrity": "sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==", "integrity": "sha512-wzZmqb/Obr0ds9Myc1sIFPJ52GA/Eeg/vXBWV0HA1LvHlVAW5Va3KB0q6EZNlNSHQWscWZ2K8+6w0GYSie2o7A==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": ">=16.8.1", "react": ">=16.8.1",
"react-dom": ">=16.8.1" "react-dom": ">=16.8.1"
@ -7624,9 +7773,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.0.10", "version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
"integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==", "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.19.3", "esbuild": "^0.19.3",
@ -7705,9 +7854,9 @@
} }
}, },
"node_modules/vite-plugin-pwa": { "node_modules/vite-plugin-pwa": {
"version": "0.17.4", "version": "0.17.5",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.4.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.5.tgz",
"integrity": "sha512-j9iiyinFOYyof4Zk3Q+DtmYyDVBDAi6PuMGNGq6uGI0pw7E+LNm9e+nQ2ep9obMP/kjdWwzilqUrlfVRj9OobA==", "integrity": "sha512-UxRNPiJBzh4tqU/vc8G2TxmrUTzT6BqvSzhszLk62uKsf+npXdvLxGDz9C675f4BJi6MbD2tPnJhi5txlMzxbQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",

View file

@ -10,7 +10,7 @@
"sourcemap": "npx source-map-explorer dist/assets/*.js" "sourcemap": "npx source-map-explorer dist/assets/*.js"
}, },
"dependencies": { "dependencies": {
"@formatjs/intl-localematcher": "~0.5.2", "@formatjs/intl-localematcher": "~0.5.4",
"@formkit/auto-animate": "~0.8.1", "@formkit/auto-animate": "~0.8.1",
"@github/text-expander-element": "~2.6.1", "@github/text-expander-element": "~2.6.1",
"@iconify-icons/mingcute": "~1.2.9", "@iconify-icons/mingcute": "~1.2.9",
@ -25,12 +25,12 @@
"idb-keyval": "~6.2.1", "idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0", "just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0", "lz-string": "~1.5.0",
"masto": "~6.5.1", "masto": "~6.5.2",
"moize": "~6.1.6", "moize": "~6.1.6",
"p-retry": "~6.2.0", "p-retry": "~6.2.0",
"p-throttle": "~6.1.0", "p-throttle": "~6.1.0",
"preact": "~10.19.3", "preact": "~10.19.3",
"react-hotkeys-hook": "~4.4.1", "react-hotkeys-hook": "~4.4.4",
"react-intersection-observer": "~9.5.3", "react-intersection-observer": "~9.5.3",
"react-quick-pinch-zoom": "~5.1.0", "react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2", "react-router-dom": "6.6.2",
@ -45,16 +45,16 @@
"valtio": "1.9.0" "valtio": "1.9.0"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "~2.7.0", "@preact/preset-vite": "~2.8.1",
"@trivago/prettier-plugin-sort-imports": "~4.3.0", "@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.32", "postcss": "~8.4.33",
"postcss-dark-theme-class": "~1.1.0", "postcss-dark-theme-class": "~1.1.0",
"postcss-preset-env": "~9.3.0", "postcss-preset-env": "~9.3.0",
"twitter-text": "~3.1.0", "twitter-text": "~3.1.0",
"vite": "~5.0.10", "vite": "~5.0.12",
"vite-plugin-generate-file": "~0.1.1", "vite-plugin-generate-file": "~0.1.1",
"vite-plugin-html-config": "~1.0.11", "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", "vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.0.0", "workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~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) 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 { .timeline.contextual > li .replies[open] > .replies-summary {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
@ -659,6 +669,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.replies-summary-chevron { .replies-summary-chevron {
transform: rotate(180deg); transform: rotate(180deg);
} }
+ * {
animation: summary-fade 0.3s ease-out both;
}
} }
.timeline.contextual > li .replies .replies-summary[hidden] { .timeline.contextual > li .replies .replies-summary[hidden] {
display: none; display: none;
@ -933,7 +947,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
background: linear-gradient( background: linear-gradient(
to bottom right, to bottom right,
var(--carousel-faded-color), var(--carousel-faded-color),
transparent 150% transparent
); );
position: relative; position: relative;
container-type: inline-size; container-type: inline-size;
@ -948,7 +962,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
var(--carousel-faded-color), var(--carousel-faded-color),
transparent 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-repeat: no-repeat;
background-position: bottom center; background-position: bottom center;
} }
@ -1059,6 +1073,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.ui-state { .ui-state {
padding: 16px; padding: 16px;
text-align: center; text-align: center;
.icon {
vertical-align: middle;
}
} }
.status-carousel-link { .status-carousel-link {
@ -1659,6 +1677,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
svg { svg {
contain: none; contain: none;
width: 100%;
height: 100%;
} }
} }

View file

@ -14,7 +14,7 @@ import { subscribe } from 'valtio';
import BackgroundService from './components/background-service'; import BackgroundService from './components/background-service';
import ComposeButton from './components/compose-button'; import ComposeButton from './components/compose-button';
import { ICONS } from './components/icon'; import { ICONS } from './components/ICONS';
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help'; import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
import Loader from './components/loader'; import Loader from './components/loader';
import Modals from './components/modals'; 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, note,
group, group,
followersCount, followersCount,
createdAt,
} = account; } = account;
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
if (accountInstance) { if (accountInstance) {
@ -188,6 +189,21 @@ function AccountBlock({
/> />
</span> </span>
)} )}
{!bot &&
!group &&
!hasRelationship &&
!followersCount &&
!verifiedField &&
!!createdAt && (
<span class="created-at">
Joined{' '}
<time datetime={createdAt}>
{niceDateTime(createdAt, {
hideTime: true,
})}
</time>
</span>
)}
</div> </div>
)} )}
</span> </span>

View file

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

View file

@ -133,7 +133,14 @@ const SCAN_RE = new RegExp(
function highlightText(text, { maxCharacters = Infinity }) { function highlightText(text, { maxCharacters = Infinity }) {
// Accept text string, return formatted HTML string // 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 // Exceeded characters limit
const { composerCharacterCount } = states; const { composerCharacterCount } = states;
let leftoverHTML = ''; let leftoverHTML = '';

View file

@ -22,6 +22,10 @@
iframe { iframe {
pointer-events: auto; pointer-events: auto;
max-width: 100%; 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'; import Icon from './icon';
function EmbedModal({ html, url, onClose = () => {} }) { function EmbedModal({ html, url, width, height, onClose = () => {} }) {
return ( return (
<div class="embed-modal-container"> <div class="embed-modal-container">
<div class="top-controls"> <div class="top-controls">
@ -20,7 +20,15 @@ function EmbedModal({ html, url, onClose = () => {} }) {
</a> </a>
)} )}
</div> </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> </div>
); );
} }

View file

@ -1,3 +1,5 @@
import { memo } from 'preact/compat';
function EmojiText({ text, emojis }) { function EmojiText({ text, emojis }) {
if (!text) return ''; if (!text) return '';
if (!emojis?.length) return text; if (!emojis?.length) return text;
@ -31,4 +33,9 @@ function EmojiText({ text, emojis }) {
return elements; 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 moize from 'moize';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import { ICONS } from './ICONS';
const SIZES = { const SIZES = {
s: 12, s: 12,
m: 16, m: 16,
@ -9,115 +11,13 @@ const SIZES = {
xxl: 32, 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 = {}; const ICONDATA = {};
// Memoize the dangerouslySetInnerHTML of the SVGs // Memoize the dangerouslySetInnerHTML of the SVGs
const SVGICon = moize( const SVGICon = moize(
function ({ size, width, height, body, rotate, flip }) { function ({ width, height, body, rotate, flip }) {
return ( return (
<svg <svg
width={size}
height={size}
viewBox={`0 0 ${width} ${height}`} viewBox={`0 0 ${width} ${height}`}
dangerouslySetInnerHTML={{ __html: body }} dangerouslySetInnerHTML={{ __html: body }}
style={{ style={{
@ -131,6 +31,8 @@ const SVGICon = moize(
{ {
isShallowEqual: true, isShallowEqual: true,
maxSize: Object.keys(ICONS).length, maxSize: Object.keys(ICONS).length,
matchesArg: (cacheKeyArg, keyArg) =>
cacheKeyArg.icon === keyArg.icon && cacheKeyArg.body === keyArg.body,
}, },
); );
@ -191,7 +93,7 @@ function Icon({
// }} // }}
// /> // />
<SVGICon <SVGICon
size={iconSize} icon={icon}
width={iconData.width} width={iconData.width}
height={iconData.height} height={iconData.height}
body={iconData.body} body={iconData.body}

View file

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

View file

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

View file

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

View file

@ -210,6 +210,8 @@ export default function Modals() {
<EmbedModal <EmbedModal
html={snapStates.showEmbedModal.html} html={snapStates.showEmbedModal.html}
url={snapStates.showEmbedModal.url} url={snapStates.showEmbedModal.url}
width={snapStates.showEmbedModal.width}
height={snapStates.showEmbedModal.height}
onClose={() => { onClose={() => {
states.showEmbedModal = false; 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}` instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
} }
> >
<Status status={status} size="s" /> <Status
status={status}
size="s"
previewMode
allowContextMenu
/>
</TruncatedLink> </TruncatedLink>
</li> </li>
))} ))}
@ -326,9 +331,19 @@ function Notification({
} }
> >
{isStatic ? ( {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> </TruncatedLink>
)} )}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -206,7 +206,7 @@
.status-card:not(.status-carousel .status) .status-card:not(.status-carousel .status)
:is(.content, .poll, .media-container) { :is(.content, .poll, .media-container) {
max-height: 160px !important; max-height: 160px !important;
overflow: clip; overflow: hidden;
} }
.status.small:not(.status-carousel .status, .status.large .status) .status.small:not(.status-carousel .status, .status.large .status)
.status-card .status-card
@ -290,7 +290,7 @@
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
} }
.status.filtered:hover :is(.status-filtered-info-1, .status-filtered-info-2) { .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 { .status.filtered .status-filtered-info-1 {
opacity: 0.5; opacity: 0.5;
@ -330,6 +330,68 @@
font-size: 90%; 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 { .status .container {
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
@ -364,9 +426,8 @@
vertical-align: middle; vertical-align: middle;
} }
.status > .container > .meta :is(.time, .edited) { .status > .container > .meta :is(.time, .edited) {
color: inherit; color: var(--text-insignificant-color);
text-align: end; text-align: end;
opacity: 0.5;
text-decoration: none; text-decoration: none;
flex-shrink: 0; flex-shrink: 0;
margin-left: 4px; margin-left: 4px;
@ -375,9 +436,21 @@
.status > .container > .meta a.time { .status > .container > .meta a.time {
position: relative; position: relative;
overflow: visible; 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) { .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:active,
.status > .container > .meta a.time.is-open { .status > .container > .meta a.time.is-open {
@ -643,6 +716,10 @@
display: flex; display: flex;
gap: 4px; gap: 4px;
align-items: center; align-items: center;
.timeline-item-container & {
display: none;
}
} }
.status.compact-thread .spoiler-badge { .status.compact-thread .spoiler-badge {
@ -1232,6 +1309,22 @@ body:has(#modal-container .carousel) .status .media img:hover {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
white-space: normal; 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 { .media-figure-multiple {
margin: 0; margin: 0;
@ -1755,6 +1848,87 @@ a.card:is(:hover, :focus):visited {
color: var(--green-color); 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 */ /* BADGE */
.status-badge { .status-badge {
@ -1853,6 +2027,12 @@ a.card:is(:hover, :focus):visited {
font-size: 80%; font-size: 80%;
} }
/* MENU OPEN */
.status-menu-open {
background-color: var(--link-bg-hover-color) !important;
}
/* FILTERED */ /* FILTERED */
#filtered-status-peek { #filtered-status-peek {

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,32 @@
.status-deck header { .status-deck {
white-space: nowrap; 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; @keyframes spin {
flex-grow: 1; to {
text-overflow: ellipsis; transform: rotate(360deg);
overflow: hidden; }
white-space: nowrap;
align-self: stretch;
}
.status-deck header h1 .deck-back {
margin-left: -16px;
} }
.hero-heading { .hero-heading {

View file

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

View file

@ -37,6 +37,7 @@ function _enhanceContent(content, opts = {}) {
links.forEach((link) => { links.forEach((link) => {
if (/^https?:\/\//i.test(link.textContent.trim())) { if (/^https?:\/\//i.test(link.textContent.trim())) {
link.classList.add('has-url-text'); link.classList.add('has-url-text');
shortenLink(link);
} }
}); });
} }
@ -287,6 +288,30 @@ const defaultRejectFilter = [
const defaultRejectFilterMap = Object.fromEntries( const defaultRejectFilterMap = Object.fromEntries(
defaultRejectFilter.map((nodeName) => [nodeName, true]), 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 = {}) { function extractTextNodes(dom, opts = {}) {
const textNodes = []; const textNodes = [];
const rejectFilterMap = Object.assign( const rejectFilterMap = Object.assign(

View file

@ -9,6 +9,17 @@ function getHTMLText(html) {
div.querySelectorAll('br').forEach((br) => { div.querySelectorAll('br').forEach((br) => {
br.replaceWith('\n'); 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(); return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
} }

View file

@ -37,6 +37,7 @@ const states = proxy({
unfurledLinks: {}, unfurledLinks: {},
statusQuotes: {}, statusQuotes: {},
statusFollowedTags: {}, statusFollowedTags: {},
statusReply: {},
accounts: {}, accounts: {},
routeNotification: null, routeNotification: null,
// Modals // Modals
@ -187,9 +188,19 @@ export function saveStatus(status, instance, opts) {
if (oldStatus?._pinned) status._pinned = oldStatus._pinned; if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
// if (oldStatus?._filtered) status._filtered = oldStatus._filtered; // if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
states.statuses[key] = status; states.statuses[key] = status;
if (status.reblog) { if (status.reblog?.id) {
const key = statusKey(status.reblog.id, instance); const srKey = statusKey(status.reblog.id, instance);
states.statuses[key] = status.reblog; 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 { extractTagsFromStatus, getFollowedTags } from './followed-tags';
import pmem from './pmem';
import { fetchRelationships } from './relationships'; import { fetchRelationships } from './relationships';
import states, { statusKey } from './states'; import states, { saveStatus, statusKey } from './states';
import store from './store'; import store from './store';
export function groupBoosts(values) { export function groupBoosts(values) {
@ -81,7 +83,7 @@ export function dedupeBoosts(items, instance) {
return filteredItems; return filteredItems;
} }
export function groupContext(items) { export function groupContext(items, instance) {
const contexts = []; const contexts = [];
let contextIndex = 0; let contextIndex = 0;
items.forEach((item) => { items.forEach((item) => {
@ -173,12 +175,44 @@ export function groupContext(items) {
return; 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); newItems.push(item);
}); });
return newItems; return newItems;
} }
const fetchStatus = pmem((statusID, masto) => {
return masto.v1.statuses.$select(statusID).fetch();
});
export async function assignFollowedTags(items, instance) { export async function assignFollowedTags(items, instance) {
const followedTags = await getFollowedTags(); // [{name: 'tag'}, {...}] const followedTags = await getFollowedTags(); // [{name: 'tag'}, {...}]
if (!followedTags.length) return; if (!followedTags.length) return;
@ -219,7 +253,7 @@ export async function assignFollowedTags(items, instance) {
statusWithFollowedTags.forEach((s) => { statusWithFollowedTags.forEach((s) => {
const { item, sKey, followedTags } = s; const { item, sKey, followedTags } = s;
const r = relationships[item.account.id]; const r = relationships[item.account.id];
if (!r.following) { if (r && !r.following) {
statusFollowedTags[sKey] = followedTags; statusFollowedTags[sKey] = followedTags;
} }
}); });