Compare commits

...

68 commits

Author SHA1 Message Date
Natsu Kagami c72bd47bbd
Don't statically pin the size of the carousel 2024-08-12 15:42:44 +02:00
Natsu Kagami bc0f856d72
Link to GtS settings when we know we are on GtS 2024-08-12 15:42:44 +02:00
Natsu Kagami 0e4dd6ee39
Make main column bigger 2024-08-12 15:42:44 +02:00
Natsu Kagami 0fa57fc0aa
Add a bit more touch 2024-08-12 15:42:43 +02:00
Natsu Kagami bb440c5d28
Force display instance
Because I don't like this decision from Phanpy
2024-08-12 15:42:43 +02:00
Natsu Kagami 66078a1867
Automatically put people into DTTHDon login 2024-08-12 15:42:43 +02:00
Natsu Kagami 3ff14d942e
Add some DTTH notice 2024-08-12 15:42:43 +02:00
Natsu Kagami 109b919c6c
Incorporate commit hash 2024-08-12 15:42:43 +02:00
Natsu Kagami 28fb3e4102
Get flakes to work 2024-08-12 15:42:28 +02:00
Chee Aun 9bf50615cb
Merge pull request #615 from verymilan/phanpy.social.tchncs.de
add phanpy.social.tchncs.de deployment to readme
2024-08-07 09:51:17 +08:00
Chee Aun dfa1123ac3
Merge pull request #616 from Fastidious/patch-2
Update README.md
2024-08-07 09:50:39 +08:00
Fastidious a0f2eb7305
Update README.md
Moved servers. This updates to the new one.
2024-08-06 10:47:13 -04:00
Milan 1bd9ceb4fc
add phanpy.social.tchncs.de deployment to readme 2024-08-06 12:39:12 +02:00
Chee Aun 082409a09f
Merge pull request #613 from illfygli/main
Filter out languages that aren't RFC5646-shaped
2024-08-06 08:58:16 +08:00
owl 225eaf4a2d
Pass undefined to Intl.DisplayNames, so '*' doesn't break it 2024-08-05 14:14:03 +02:00
Lim Chee Aun 60289cdb29 Upgrade dependencies 2024-08-04 19:09:46 +08:00
Lim Chee Aun a1c419b675 Try fix select field bug on Windows again
Previously: b47c043699
2024-08-04 19:01:21 +08:00
Lim Chee Aun 89e8bdf77b Use pinned instead of _pinned 2024-08-04 18:06:26 +08:00
Lim Chee Aun b3681a93ee Workbox expiration plugin not working as expected 2024-08-04 18:05:03 +08:00
Lim Chee Aun ad7193d067 Fix notifications popover not close-able on iPad 2024-08-04 13:53:06 +08:00
Lim Chee Aun f05e3012e3 Preliminary step for RTL 2024-08-04 13:32:46 +08:00
Lim Chee Aun 2aff1dc1fd Try switch to 20s interval 2024-08-04 13:32:46 +08:00
Lim Chee Aun 99ee6c3979 Don't reuse var for both timeout and interval 2024-08-04 13:32:46 +08:00
Lim Chee Aun 4ebfb544aa This caching seems still buggy
Revert to SWR with 1-min expiry
2024-08-04 13:32:46 +08:00
Lim Chee Aun cf2461add5 Better checks 2024-08-04 13:32:46 +08:00
Chee Aun 4937c5f77e
Merge pull request #610 from fhemberger/patch-1
fix(shortcuts-settings): `settingsJSON` must be defined if note doesn't exist
2024-08-04 10:11:42 +08:00
Frederic Hemberger 0febcacb93
fix(shortcuts-settings): settingsJSON must be defined if note doesn't exist 2024-08-03 13:30:22 +02:00
Lim Chee Aun 818f58b460 Fix profile URLs not working for http route 2024-08-01 20:18:44 +08:00
Lim Chee Aun 57db8778a4 Adapt to new changes in group notifications API
Reference: https://github.com/mastodon/mastodon/pull/31214
2024-08-01 20:18:10 +08:00
Chee Aun 9806d8ae9d
Merge pull request #607 from kizu/fix-overflow
Fix overflow for the columns wrapper
2024-08-01 09:56:05 +08:00
Roman Komarov 522a324b0d
Fix overflow for the columns wrapper 2024-07-31 13:59:35 +02:00
Lim Chee Aun 5be30e0c80 Upgrade dependencies 2024-07-29 20:05:03 +08:00
Lim Chee Aun 379ef7cc11 Random unused IntersectionView
Keeping this for future use
2024-07-28 16:09:44 +08:00
Lim Chee Aun 2d23b15c8d Assume title is the author for .card-post 2024-07-28 16:09:03 +08:00
Lim Chee Aun fa3a0e23cc Unhide some text for posts inside Edit History
Every char matters when looking at post edit history
2024-07-28 16:08:18 +08:00
Lim Chee Aun 631730f2f2 Replace SWR with CacheFirst
This SWR strategy is sometimes too stale, possibly a bug with Workbox
2024-07-28 16:07:22 +08:00
Lim Chee Aun f1822d54af Fix poll radio button position on Safari
Plus a color
2024-07-25 18:39:14 +08:00
Lim Chee Aun 4c0bc62ad0 Group filtered carousel items 2024-07-22 14:31:52 +08:00
Lim Chee Aun 84b3106f50 Undo font size inherit for card posts 2024-07-22 14:19:25 +08:00
Lim Chee Aun a2b88f1cdd Distinct both implementation of grouped notifications 2024-07-21 20:31:10 +08:00
Lim Chee Aun b88376569e Test this out for bridgy fed links 2024-07-21 19:06:38 +08:00
Lim Chee Aun 00e2ba0b34 Fix notification markers not working
Also the ids are getting confusing, so need to clean this up.
2024-07-21 18:59:38 +08:00
Lim Chee Aun a0d75e7e83 Upgrade dependencies 2024-07-20 17:45:43 +08:00
Lim Chee Aun 4b2ec14dcd Try set default sort and group when choosing Boosts 2024-07-19 20:00:10 +08:00
Chee Aun 808c6262d8
Merge pull request #597 from graue/graue/copy-handle-with-instance
Include domain when copying local user's handle
2024-07-18 17:08:51 +08:00
Scott Feeney 44d440649f Include domain when copying local user's handle
Fixes #596
2024-07-13 01:15:01 -07:00
Lim Chee Aun a2f7638257 Experimental opt-in server-side grouped notifications 2024-07-12 18:57:48 +08:00
Lim Chee Aun 57d6889826 Test memoize Media 2024-07-12 13:35:43 +08:00
Lim Chee Aun 2a91c005a1 Test fix self-recursive quote posts 2024-07-12 13:34:57 +08:00
Lim Chee Aun 418895e1c3 Another attempt: upgrade dependencies 2024-07-08 17:40:16 +08:00
Lim Chee Aun 180a23f116 Fix wrong exceeded chars highlighting 2024-07-07 22:56:24 +08:00
Lim Chee Aun 9ea7a1f4db Use onClose for this 2024-07-06 09:47:42 +08:00
Lim Chee Aun f26dbeb79a Fix more cloaking business 2024-07-06 09:47:28 +08:00
Lim Chee Aun f0872e79fb Revert "Upgrade dependencies"
This reverts commit cb9848fe8c.
2024-07-05 18:56:52 +08:00
Lim Chee Aun a72400febf Test support Hollo 2024-07-05 16:19:04 +08:00
Lim Chee Aun cb9848fe8c Upgrade dependencies 2024-07-03 20:02:47 +08:00
Lim Chee Aun c950a6552c Experiment: unhide header when clicking on timeline items 2024-07-03 20:01:11 +08:00
Lim Chee Aun 95bf9e183e Replace trivago/ with ianvs/prettier-plugin-sort-imports 2024-07-01 17:41:21 +08:00
Lim Chee Aun e6e884f1cb Refactor + make card post work for no-image cards 2024-06-28 07:49:30 +08:00
Lim Chee Aun b6a25f5939 MVP-ish add/remove featured tags 2024-06-27 22:05:16 +08:00
Lim Chee Aun 71823fbad2 Fix typo 2024-06-27 22:05:16 +08:00
Lim Chee Aun 046d3d323a Enable unfurling when fetching reply hints 2024-06-27 22:05:16 +08:00
Lim Chee Aun f7024f7723 Only allow trending link posts for current instance, not remote instance
For this to work on remote instance, will need to fetch its version and check first
2024-06-27 22:05:16 +08:00
Lim Chee Aun 1b3938f3d2 Add bundle-visualizer 2024-06-27 22:05:16 +08:00
Lim Chee Aun 5ab0ea1b59 Remove usehooks dep
In the end, only used one hook out of so many hooks
2024-06-27 22:05:16 +08:00
Lim Chee Aun 09745e3078 Don't show account if notification = severed_relationships 2024-06-27 22:05:16 +08:00
Chee Aun 87be0cad16
Merge pull request #584 from coxde/patch-1
fix: enable/disable boosts button logic
2024-06-27 22:00:01 +08:00
COxDE 04588874c7
fix: enable/disable boosts button logic 2024-06-27 13:38:55 +01:00
82 changed files with 2281 additions and 1486 deletions

6
.gitignore vendored
View file

@ -26,4 +26,8 @@ dist-ssr
# Custom
.env.dev
phanpy-dist.zip
phanpy-dist.tar.gz
phanpy-dist.tar.gz
# Nix
.direnv
result

View file

@ -3,18 +3,20 @@
"useTabs": false,
"singleQuote": true,
"trailingComma": "all",
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrder": [
"^[^.].*.css$",
"index.css$",
".css$",
"",
"./polyfills",
"",
"<THIRD_PARTY_MODULES>",
"",
"/assets/",
"",
"^../",
"",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderGroupNamespaceSpecifiers": true,
"importOrderCaseInsensitive": true
]
}

View file

@ -43,7 +43,7 @@ Everything is designed and engineered following my taste and vision. This is a p
- **Status actions (reply, boost, favourite, bookmark, etc) are hidden by default**.<br>They only appear in individual status page. This is to reduce clutter and distraction. It may result in lower engagement, but we're not chasing numbers here.
- **Boost is represented with the rocket icon**.<br>The green double arrow icon (retweet for Twitter) doesn't look right for the term "boost". Green rocket looks weird, so I use purple.
- **Short usernames (`@username`) are displayed in timelines, instead of the full account username (`@username@instance`)**.<br>Despite the [guideline](https://docs.joinmastodon.org/api/guidelines/#username) mentioned that "Decentralization must be transparent to the user", I don't think we should shove it to the face every single time. There are also some [screen-reader-related accessibility concerns](https://twitter.com/lifeofablindgrl/status/1595864647554502656) with the full username, though this web app is unfortunately not accessible yet.
- ~~**Short usernames (`@username`) are displayed in timelines, instead of the full account username (`@username@instance`)**.<br>Despite the [guideline](https://docs.joinmastodon.org/api/guidelines/#username) mentioned that "Decentralization must be transparent to the user", I don't think we should shove it to the face every single time. There are also some [screen-reader-related accessibility concerns](https://twitter.com/lifeofablindgrl/status/1595864647554502656) with the full username, though this web app is unfortunately not accessible yet.~~ DTTHDon fork displays the full username by default.
- **No autoplay for video/GIF/whatever in timeline**.<br>The timeline is already a huge mess with lots of people, brands, news and media trying to grab your attention. Let's not make it worse. (Current exception now would be animated emojis.)
- **Hash-based URLs**.<br>This web app is not meant to be a full-fledged replacement to Mastodon's existing front-end. There's no SEO, database, serverless or any long-running servers. I could be wrong one day.
@ -199,7 +199,7 @@ See documentation for [lingva-translate](https://github.com/thedaviddelta/lingva
These are self-hosted by other wonderful folks.
- [ferengi.one](https://m.ferengi.one/) by [@david@collantes.social](https://collantes.social/@david)
- [ferengi.one](https://m.ferengi.one/) by [@david@weaknotes.com](https://weaknotes.com/@david)
- [phanpy.blaede.family](https://phanpy.blaede.family/) by [@cassidy@blaede.family](https://mastodon.blaede.family/@cassidy)
- [phanpy.mstdn.mx](https://phanpy.mstdn.mx/) by [@maop@mstdn.mx](https://mstdn.mx/@maop)
- [phanpy.vmst.io](https://phanpy.vmst.io/) by [@vmstan@vmst.io](https://vmst.io/@vmstan)
@ -211,6 +211,7 @@ These are self-hosted by other wonderful folks.
- [halo.mookiesplace.com](https://halo.mookiesplace.com) by [@mookie@mookiesplace.com](https://mookiesplace.com/@mookie)
- [social.qrk.one](https://social.qrk.one) by [@kev@fosstodon.org](https://fosstodon.org/@kev)
- [phanpy.cz](https://phanpy.cz) by [@zdendys@mamutovo.cz](https://mamutovo.cz/@zdendys)
- [phanpy.social.tchncs.de](https://phanpy.social.tchncs.de) by [@milan@social.tchncs.de](https://social.tchncs.de/@milan)
> Note: Add yours by creating a pull request.

61
flake.lock Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1723175592,
"narHash": "sha256-M0xJ3FbDUc4fRZ84dPGx5VvgFsOzds77KiBMW/mMTnI=",
"owner": "nixOS",
"repo": "nixpkgs",
"rev": "5e0ca22929f3342b19569b21b2f3462f053e497b",
"type": "github"
},
"original": {
"owner": "nixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

60
flake.nix Normal file
View file

@ -0,0 +1,60 @@
{
inputs.nixpkgs.url = github:nixOS/nixpkgs/nixos-unstable;
inputs.flake-utils.url = github:numtide/flake-utils;
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
esbuild = pkgs.buildGoModule rec {
pname = "esbuild";
version = "0.21.5";
src = pkgs.fetchFromGitHub {
owner = "evanw";
repo = "esbuild";
rev = "v${version}";
hash = "sha256-FpvXWIlt67G8w3pBKZo/mcp57LunxDmRUaCU/Ne89B8=";
};
vendorHash = "sha256-+BfxCyg0KkDQpHt/wycy/8CTG6YBA/VJvJFhhzUnSiQ=";
subPackages = [ "cmd/esbuild" ];
ldflags = [ "-s" "-w" ];
meta.mainProgram = "esbuild";
};
in
rec {
packages.default = pkgs.buildNpmPackage {
pname = "dtth-phanpy";
version = "0.1.0";
nativeBuildInputs = with pkgs; [ git ];
ESBUILD_BINARY_PATH = lib.getExe esbuild;
src = lib.cleanSource ./.;
npmFlags = [ "--legacy-peer-deps" ];
npmDepsHash = "sha256-VROK9Emxi+jFqwidA/CUxQwxitKf7Y6mx0yuOCUwrzI=";
# npmDepsHash = lib.fakeHash;
# DTTH-specific env variables
PHANPY_CLIENT_NAME = "DTTH Phanpy";
PHANPY_CLIENT_ID = "ch.dtth.phanpy";
PHANPY_WEBSITE = "https://social.dtth.ch";
installPhase = ''
runHook preInstall
mkdir -p $out/lib
cp -r dist $out/lib/phanpy
runHook postInstall
'';
};
devShells.default = pkgs.mkShell {
inputsFrom = [ packages.default ];
buildInputs = with pkgs; [ nodejs ];
};
});
}

1578
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,8 @@
"build": "vite build",
"preview": "vite preview",
"fetch-instances": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
"sourcemap": "npx source-map-explorer dist/assets/*.js"
"sourcemap": "npx source-map-explorer dist/assets/*.js",
"bundle-visualizer": "npx vite-bundle-visualizer"
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.5.4",
@ -16,12 +17,11 @@
"@github/text-expander-element": "~2.7.1",
"@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.5.0",
"@szhsin/react-menu": "~4.1.0",
"@uidotdev/usehooks": "~2.4.1",
"compare-versions": "~6.1.0",
"dayjs": "~1.11.11",
"@szhsin/react-menu": "~4.2.1",
"compare-versions": "~6.1.1",
"dayjs": "~1.11.12",
"dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2",
"fast-blurhash": "~1.1.4",
"fast-equals": "~5.0.1",
"fuse.js": "~7.0.0",
"html-prettify": "~1.0.7",
@ -32,10 +32,10 @@
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.22.0",
"preact": "~10.23.1",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.0",
"react-intersection-observer": "~9.10.3",
"react-intersection-observer": "~9.13.0",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
"string-length": "6.0.0",
@ -43,22 +43,22 @@
"tinyld": "~1.3.4",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~10.0.1",
"use-debounce": "~10.0.2",
"use-long-press": "~3.2.0",
"use-resize-observer": "~9.1.0",
"valtio": "1.13.2"
},
"devDependencies": {
"@preact/preset-vite": "~2.8.2",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.38",
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
"@preact/preset-vite": "~2.9.0",
"postcss": "~8.4.40",
"postcss-dark-theme-class": "~1.3.0",
"postcss-preset-env": "~9.5.14",
"postcss-preset-env": "~10.0.0",
"twitter-text": "~3.1.0",
"vite": "~5.3.1",
"vite-plugin-generate-file": "~0.1.1",
"vite": "~5.3.5",
"vite-plugin-generate-file": "~0.2.0",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.20.0",
"vite-plugin-pwa": "~0.20.1",
"vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.1.0",
"workbox-expiration": "~7.1.0",

View file

@ -96,24 +96,27 @@ const apiExtendedRoute = new RegExpRoute(
);
registerRoute(apiExtendedRoute);
const apiIntermediateRoute = new RegExpRoute(
// Matches:
// - trends/*
// - timelines/link
/^https?:\/\/[^\/]+\/api\/v\d+\/(trends|timelines\/link)/,
new StaleWhileRevalidate({
cacheName: 'api-intermediate',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 10 * 60, // 10 minutes
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
}),
);
registerRoute(apiIntermediateRoute);
// Note: expiration is not working as expected
// https://github.com/GoogleChrome/workbox/issues/3316
//
// const apiIntermediateRoute = new RegExpRoute(
// // Matches:
// // - trends/*
// // - timelines/link
// /^https?:\/\/[^\/]+\/api\/v\d+\/(trends|timelines\/link)/,
// new StaleWhileRevalidate({
// cacheName: 'api-intermediate',
// plugins: [
// new ExpirationPlugin({
// maxAgeSeconds: 1 * 60, // 1min
// }),
// new CacheableResponsePlugin({
// statuses: [0, 200],
// }),
// ],
// }),
// );
// registerRoute(apiIntermediateRoute);
const apiRoute = new RegExpRoute(
// Matches:

View file

@ -162,7 +162,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
white-space: nowrap;
}
.deck > header .header-grid > .header-side:last-of-type {
text-align: right;
text-align: end;
grid-column: 3;
}
.deck > header .header-grid :is(button, .button).plain {
@ -181,8 +181,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
grid-template-columns: 1fr max-content;
}
.deck > header .header-grid-2 h1 {
text-align: left;
padding-left: 8px;
text-align: start;
padding-inline-start: 8px;
}
.deck > header .header-grid h1:has(.ancestors-indicator) {
display: flex;
@ -217,6 +217,19 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
opacity: 0.25;
}
}
@keyframes indeterminate-bar-rtl {
0% {
transform: translateX(50%);
opacity: 0.25;
}
50% {
opacity: 1;
}
100% {
transform: translateX(-50%);
opacity: 0.25;
}
}
.deck > header.loading:after {
pointer-events: none;
content: '';
@ -232,6 +245,9 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
transparent
);
animation: indeterminate-bar 1s ease-in-out infinite alternate;
&:dir(rtl) {
animation-name: indeterminate-bar-rtl;
}
}
@media (min-width: 40em) {
.deck > header.loading:after {
@ -268,6 +284,9 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
width: 95vw;
max-width: calc(320px * 3.3);
transform: translateX(calc(-50% + var(--main-width) / 2));
&:dir(rtl) {
transform: translateX(calc(50% - var(--main-width) / 2));
}
}
}
@ -346,6 +365,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
margin: 0;
padding: 0;
border-bottom: var(--hairline-width) solid var(--divider-color);
--line-dir: var(--to-forward);
}
.timeline.flat > li {
border-bottom: none;
@ -362,10 +382,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
--avatar-size: 50px;
--avatar-margin-start: 16px;
--avatar-margin-end: 12px;
--line-curve: 45deg;
:dir(rtl) & {
--line-curve: -45deg;
}
}
.timeline.contextual > li {
background-image: linear-gradient(
to right,
var(--line-dir),
transparent,
transparent var(--line-start),
var(--comment-line-color) var(--line-start),
@ -394,7 +418,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.timeline.contextual
> li.descendant:not(.thread)
> :is(.status-link, .status-focus) {
padding-left: 40px;
padding-inline-start: 40px;
}
.timeline.contextual .replies[data-scroll-left]:not([data-scroll-left='0']) {
background-color: var(--bg-color);
@ -408,7 +432,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
}
.timeline.contextual .replies[data-comments-level='4']:has(.replies) {
overflow-x: auto;
mask-image: linear-gradient(to left, transparent, black 32px);
mask-image: linear-gradient(var(--to-backward), transparent, black 32px);
}
.timeline.contextual
.replies[data-comments-level='4']:has(.replies)
@ -426,145 +450,61 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
> :is(.status-link, .status-focus)
+ .replies
.replies-summary {
margin-left: calc(
margin-inline-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
(var(--line-margin-end) * (var(--comments-level) - 1))
);
}
/* .timeline.contextual
> li.descendant.thread
> .status-link
+ .replies
.replies
> .replies-summary {
margin-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
var(--line-margin-end)
);
}
.timeline.contextual
> li.descendant.thread
> .status-link
+ .replies
.replies
.replies
> .replies-summary {
margin-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
(var(--line-margin-end) * 2)
);
} */
.timeline.contextual
> li.descendant.thread
> :is(.status-link, .status-focus)
+ .replies
:is(.status-link, .status-focus) {
padding-left: calc(
padding-inline-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
(var(--line-margin-end) * (var(--comments-level) - 1))
);
}
/* .timeline.contextual
> li.descendant.thread
> .status-link
+ .replies
.replies
.status-link {
padding-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
var(--line-margin-end)
);
}
.timeline.contextual
> li.descendant.thread
> .status-link
+ .replies
.replies
.replies
.status-link {
padding-left: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
(var(--line-margin-end) * 2)
);
} */
.timeline.contextual
> li.descendant:not(.thread)
> :is(.status-link, .status-focus)
+ .replies
.replies-summary {
margin-left: calc(
margin-inline-start: calc(
var(--thread-start) + var(--line-margin-end) * var(--comments-level)
);
}
/* .timeline.contextual
> li.descendant:not(.thread)
> .status-link
+ .replies
.replies
> .replies-summary {
margin-left: calc(
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
);
}
.timeline.contextual
> li.descendant:not(.thread)
> .status-link
+ .replies
.replies
.replies
> .replies-summary {
margin-left: calc(
var(--thread-start) + var(--line-margin-end) + (var(--line-margin-end) * 2)
);
} */
.timeline.contextual
> li.descendant:not(.thread)
> :is(.status-link, .status-focus)
+ .replies
:is(.status-link, .status-focus) {
padding-left: calc(
padding-inline-start: calc(
var(--thread-start) + var(--line-margin-end) * var(--comments-level)
);
}
/* .timeline.contextual
> li.descendant:not(.thread)
> .status-link
+ .replies
.replies
.status-link {
padding-left: calc(var(--thread-start) + (var(--line-margin-end) * 2));
}
.timeline.contextual
> li.descendant:not(.thread)
> .status-link
+ .replies
.replies
.replies
.status-link {
padding-left: calc(var(--thread-start) + (var(--line-margin-end) * 3));
} */
.timeline.contextual > li.descendant:not(.thread):before {
content: '';
position: absolute;
top: 10px;
left: var(--line-start);
inset-inline-start: var(--line-start);
width: var(--line-diameter);
height: var(--line-diameter);
border-radius: var(--line-radius);
border-style: solid;
border-width: var(--line-width);
border-color: transparent transparent var(--comment-line-color) transparent;
transform: rotate(45deg);
transform: rotate(var(--line-curve));
}
.timeline.contextual > li .replies-link {
color: var(--text-insignificant-color);
margin-left: 16px;
margin-inline-start: 16px;
margin-top: -12px;
padding-bottom: 12px;
font-size: 90%;
}
.timeline.contextual > li.ancestor .replies-link {
margin-left: calc(
margin-inline-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
);
}
@ -572,7 +512,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
> li.thread
> :is(.status-link, .status-focus)
.replies-link {
margin-left: calc(
margin-inline-start: calc(
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
);
}
@ -603,7 +543,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
list-style: none;
gap: 8px;
align-items: center;
margin-right: calc(44px + 8px);
margin-inline-end: calc(44px + 8px);
b {
font-weight: 500;
@ -618,7 +558,9 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
transition: transform 0.3s ease;
&:not(:first-child) {
margin: 0 0 0 -4px;
transform: rotate(0deg);
margin: 0;
margin-inline-start: -4px;
}
}
}
@ -637,7 +579,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.replies-parent-link {
position: absolute;
right: 4px;
inset-inline-end: 4px;
height: 100%;
z-index: 2;
font-size: 16px;
@ -648,8 +590,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
align-items: center;
padding: var(--summary-padding) calc(var(--summary-padding) * 2);
transform: translateX(100%);
margin: calc(-1 * var(--summary-padding)) calc(-1 * var(--summary-padding))
calc(-1 * var(--summary-padding)) 0;
&:dir(rtl) {
transform: translateX(-100%);
}
margin: calc(-1 * var(--summary-padding)) 0;
margin-inline-end: calc(-1 * var(--summary-padding));
border-radius: 8px;
background-color: var(--link-bg-color);
@ -681,7 +626,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
color: var(--text-color);
background-color: var(--comment-line-color);
background-image: linear-gradient(
to top right,
to top var(--forward),
var(--comment-line-color),
var(--bg-faded-color)
);
@ -697,7 +642,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
}
}
.timeline.contextual > li .replies[open] > .replies-summary {
border-bottom-left-radius: 0;
border-end-start-radius: 0;
.avatars {
opacity: 0.5;
@ -727,7 +672,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
);
--line-end: calc(var(--line-start) + var(--line-width));
background-image: linear-gradient(
to right,
var(--line-dir),
transparent,
transparent var(--line-start),
var(--comment-line-color) var(--line-start),
@ -768,14 +713,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
content: '';
position: absolute;
top: 10px;
left: var(--line-start);
inset-inline-start: var(--line-start);
width: var(--line-diameter);
height: var(--line-diameter);
border-radius: var(--line-radius);
border-style: solid;
border-width: var(--line-width);
border-color: transparent transparent var(--comment-line-color) transparent;
transform: rotate(45deg);
transform: rotate(var(--line-curve));
}
/* .timeline.contextual > li .replies .replies li:before {
--line-start: calc(
@ -814,8 +759,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
> ul > li:only-child {
> .replies {
> ul > li:only-child {
margin-left: calc(-1 * var(--line-margin-end));
background-position: calc(16px) 0;
margin-inline-start: calc(-1 * var(--line-margin-end));
background-position: 16px 0;
&:dir(rtl) {
background-position: -16px 0;
}
background-size: 100% calc(20px + 8px);
&:before {
@ -856,7 +804,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
--line-width: 3px;
--line-end: calc(var(--line-start) + var(--line-width));
background-image: linear-gradient(
to right,
var(--line-dir),
transparent,
transparent var(--line-start),
var(--comment-line-color) var(--line-start),
@ -868,8 +816,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
}
.timeline:not(.flat) > li.timeline-item-container-start {
margin-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
border-bottom: 0;
background-position: 0 calc(16px + var(--avatar-size));
}
@ -882,8 +830,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
}
.timeline:not(.flat) > li.timeline-item-container-end {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-start-start-radius: 0;
border-start-end-radius: 0;
border-top: 0;
background-size: 100% 16px;
@ -909,8 +857,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
}
.timeline .show-more {
padding-left: calc(var(--line-end) + var(--line-margin-end)) !important;
text-align: left;
padding-inline-start: calc(
var(--line-end) + var(--line-margin-end)
) !important;
text-align: start;
background-color: transparent !important;
backdrop-filter: none !important;
position: relative;
@ -918,7 +868,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
padding-block: 16px !important;
.avatars-bunch > .avatar:not(:first-child) {
margin-left: -4px;
margin-inline-start: -4px;
}
}
.timeline .show-more:hover {
@ -930,14 +880,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
content: '';
position: absolute;
top: 10px;
left: var(--line-start);
inset-inline-start: var(--line-start);
width: var(--line-diameter);
height: var(--line-diameter);
border-radius: var(--line-radius);
border-style: solid;
border-width: var(--line-width);
border-color: transparent transparent var(--comment-line-color) transparent;
transform: rotate(45deg);
transform: rotate(var(--line-curve));
}
.status-loading {
@ -988,7 +938,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.status-carousel {
--carousel-faded-color: var(--bg-faded-color);
background: linear-gradient(
to bottom right,
to bottom var(--forward),
var(--carousel-faded-color),
transparent
);
@ -1058,12 +1008,12 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
display: none;
}
.status-carousel .status-carousel-beacon {
margin-right: calc(-1 * var(--carousel-gap));
margin-inline-end: calc(-1 * var(--carousel-gap));
pointer-events: none;
opacity: 0;
~ .status-carousel-beacon {
margin-left: calc(-1 * var(--carousel-gap));
margin-inline-start: calc(-1 * var(--carousel-gap));
}
}
/*
@ -1107,12 +1057,21 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
.status-carousel.boosts-carousel > ul > li:before {
content: counter(index);
position: absolute;
left: 0;
inset-inline-start: 0;
font-size: 10px;
color: var(--text-insignificant-color);
padding: 6px;
}
.status-carousel.boosts-carousel .timeline-item-carousel-group {
flex-direction: column;
gap: 8px;
&:before {
content: '';
}
}
.ui-state {
padding: 16px;
text-align: center;
@ -1138,11 +1097,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
box-shadow: 0 1px var(--bg-color);
&:has(.status-badge:not(:empty)) {
border-top-right-radius: 8px;
border-start-end-radius: 8px;
}
.status-carousel.boosts-carousel & {
border-top-left-radius: 8px;
.status-carousel.boosts-carousel &:not(.timeline-item-carousel-group &) {
border-start-start-radius: 8px;
}
}
.status-carousel-link::focus {
@ -1180,14 +1139,29 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
transform: translate3d(0, 0, 0);
}
}
@keyframes slide-in-rtl {
0% {
transform: translate3d(-100%, 0, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
.deck-backdrop .deck {
width: var(--main-width);
max-width: 100vw;
background-color: var(--bg-color);
box-shadow: -1px 0 var(--bg-color);
&:dir(rtl) {
box-shadow: 1px 0 var(--bg-color);
}
}
.deck-backdrop .deck.slide-in:not(.deck-view-full) {
animation: slide-in 0.5s var(--timing-function);
&:dir(rtl) {
animation-name: slide-in-rtl;
}
}
.deck-backdrop .deck .status {
max-width: var(--main-width);
@ -1231,7 +1205,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
content: '';
display: inline-block;
position: absolute;
right: 10px;
inset-inline-end: 10px;
width: 4px;
height: 4px;
border-radius: 50%;
@ -1510,7 +1484,7 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
.media-modal-container + .status-deck {
/* display: none; */
position: absolute;
right: 0;
inset-inline-end: 0;
z-index: -1;
pointer-events: none;
user-select: none;
@ -1538,8 +1512,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
)
#modal-container
> div {
left: 0;
right: 350px;
inset-inline-start: 0;
inset-inline-end: 350px;
width: auto;
}
/* ✨ New */
@ -1570,8 +1544,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
position: fixed;
bottom: 16px;
bottom: max(16px, env(safe-area-inset-bottom));
right: 16px;
right: max(16px, env(safe-area-inset-right));
inset-inline-end: 16px;
inset-inline-end: max(16px, env(safe-area-inset-right));
padding: 16px;
background-color: var(--button-bg-blur-color);
/* backdrop-filter: blur(16px); */
@ -1620,7 +1594,7 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
display: block;
position: absolute;
top: 0;
right: 0;
inset-inline-end: 0;
width: 14px;
height: 14px;
border-radius: 50%;
@ -1687,6 +1661,10 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
border-radius: 0;
padding: 0;
right: env(safe-area-inset-right);
&:dir(rtl) {
right: auto;
left: env(safe-area-inset-left);
}
width: 44px;
height: 44px;
display: inline-flex;
@ -1728,6 +1706,11 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
}
.sheet .sheet-close:not(.outer) + header {
padding-right: max(44px, env(safe-area-inset-right));
&:dir(rtl) {
padding-right: max(16px, env(safe-area-inset-right));
padding-left: max(44px, env(safe-area-inset-left));
}
}
.sheet header :is(h1, h2, h3) {
margin: 0;
@ -1765,6 +1748,10 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
width: 100%;
height: 100%;
}
:dir(rtl) &.rtl-flip {
transform: scaleX(-1);
}
}
/* TAG */
@ -1830,7 +1817,7 @@ body > .szh-menu-container {
border: 1px solid var(--outline-color);
border-radius: 8px;
box-shadow: 0 3px 16px -3px var(--drop-shadow-color);
text-align: left;
text-align: start;
/* animation: appear-smooth 0.15s ease-in-out; */
width: 16em;
max-width: 90vw;
@ -1966,7 +1953,7 @@ body > .szh-menu-container {
}
.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child,
.szh-menu .menu-horizontal > *:not(:only-child):first-child .szh-menu__item {
padding-right: 4px !important;
padding-inline-end: 4px !important;
}
.szh-menu
.menu-horizontal
@ -1975,12 +1962,12 @@ body > .szh-menu-container {
.menu-horizontal
> *:not(:only-child):not(:first-child):not(:last-child)
.szh-menu__item {
padding-left: 8px !important;
padding-right: 4px !important;
padding-inline-start: 8px !important;
padding-inline-end: 4px !important;
}
.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):last-child,
.szh-menu .menu-horizontal > *:not(:only-child):last-child .szh-menu__item {
padding-left: 8px !important;
padding-inline-start: 8px !important;
}
.szh-menu .szh-menu__item .menu-shortcut {
opacity: 0.5;
@ -2044,7 +2031,11 @@ body > .szh-menu-container {
text-align: center;
opacity: 0.5;
text-overflow: clip;
mask-image: linear-gradient(to left, transparent, black 16px);
mask-image: linear-gradient(
var(--to-backward),
transparent,
black 16px
);
}
}
}
@ -2060,10 +2051,10 @@ body > .szh-menu-container {
}
> [class^='szh-menu']:first-child {
border-top-left-radius: 8px;
border-start-start-radius: 8px;
}
> [class^='szh-menu']:last-child {
border-top-right-radius: 8px;
border-start-end-radius: 8px;
}
}
}
@ -2144,6 +2135,9 @@ body > .szh-menu-container {
background-image: var(--middle-circle),
conic-gradient(var(--color) var(--fill), var(--outline-color) 0);
transform: scale(0.7);
&:dir(rtl) {
transform: scale(-0.7, 0.7);
}
transition: transform 0.2s ease-in-out;
&::-webkit-meter-inner-element,
@ -2353,12 +2347,12 @@ ul.link-list li a {
}
}
ul.link-list li:first-child a {
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
border-start-start-radius: var(--radius);
border-start-end-radius: var(--radius);
}
ul.link-list li:last-child a {
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
border-end-start-radius: var(--radius);
border-end-end-radius: var(--radius);
}
ul.link-list li a:is(:hover, :focus) {
color: var(--text-color);
@ -2395,8 +2389,8 @@ ul.link-list li a .icon {
}
.nav-menu-button.with-avatar .icon {
position: absolute;
bottom: 4px;
right: 8px;
inset-block-end: 4px;
inset-inline-end: 8px;
background-color: var(--bg-color);
border-radius: 2px;
}
@ -2410,7 +2404,7 @@ ul.link-list li a .icon {
display: flex;
width: 100vw;
overflow-y: hidden;
overflow-x: scroll;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
/* scrollbar-width: none; */
@ -2424,13 +2418,17 @@ ul.link-list li a .icon {
} */
#columns > * {
overscroll-behavior: auto;
scroll-snap-align: left;
scroll-snap-align: start;
scroll-snap-stop: always;
overscroll-behavior: auto;
flex-basis: min(100vw, 360px);
flex-shrink: 0;
box-shadow: -1px 0 var(--bg-color), -2px 0 var(--drop-shadow-color),
-3px 0 var(--bg-color);
&:dir(rtl) {
box-shadow: 1px 0 var(--bg-color), 2px 0 var(--drop-shadow-color),
3px 0 var(--bg-color);
}
}
#columns:has(> :nth-child(3)) > *:nth-child(even),
#columns:has(> :nth-child(3))
@ -2563,7 +2561,7 @@ ul.link-list li a .icon {
gap: 8px;
overflow-x: auto;
mask-image: linear-gradient(
to right,
var(--to-forward),
transparent,
black 16px,
black calc(100% - 16px),
@ -2577,6 +2575,9 @@ ul.link-list li a .icon {
width: 95vw;
max-width: calc(320px * 3.3);
transform: translateX(calc(-50% + var(--main-width) / 2));
&:dir(rtl) {
transform: translateX(calc(50% - var(--main-width) / 2));
}
}
}
@ -2695,7 +2696,8 @@ ul.link-list li a .icon {
min-width: 16px;
min-height: 16px;
padding: 4px;
margin: -4px -8px -4px 0;
margin: -4px 0;
margin-inline-end: -8px;
background-color: var(--bg-faded-color);
border-radius: 999px;
}
@ -2725,11 +2727,14 @@ ul.link-list li a .icon {
.deck-container:has(~ .deck-backdrop .deck) {
transition: transform 0.4s ease-out;
transform: translate3d(-5vw, 0, 0);
&:dir(rtl) {
transform: translate3d(5vw, 0, 0);
}
}
.deck-backdrop .deck {
/* width: 50%;
min-width: var(--main-width); */
border-left: 1px solid var(--divider-color);
border-inline-start: 1px solid var(--divider-color);
}
.timeline-deck {
border: 0;
@ -2790,16 +2795,19 @@ ul.link-list li a .icon {
> li:not(.timeline-item-container-end, .timeline-item-container-middle):has(
.status-badge:not(:empty)
) {
border-top-right-radius: 8px;
border-start-end-radius: 8px;
}
.timeline:not(.flat) > li:has(.status-link.is-active) {
transition: var(--back-transition);
transform: translate3d(-2.5vw, 0, 0);
&:dir(rtl) {
transform: translate3d(2.5vw, 0, 0);
}
}
.timeline:not(.flat)
> li.timeline-item-container:has(.status-link.is-active) {
border-top-left-radius: var(--item-radius);
border-bottom-left-radius: var(--item-radius);
border-start-start-radius: var(--item-radius);
border-end-start-radius: var(--item-radius);
}
.timeline:not(.flat)
> li:not(:has(.status-carousel)):has(+ li .status-link.is-active),
@ -2808,19 +2816,22 @@ ul.link-list li a .icon {
+ li {
transition: var(--back-transition);
transform: translate3d(-1.25vw, 0, 0);
&:dir(rtl) {
transform: translate3d(1.25vw, 0, 0);
}
}
.timeline:not(.flat)
> li.timeline-item-container:not(:has(.status-carousel)):has(
+ li .status-link.is-active
) {
border-top-left-radius: var(--item-radius);
border-start-start-radius: var(--item-radius);
}
.timeline:not(.flat)
> li.timeline-item-container:not(:has(.status-carousel)):has(
.status-link.is-active
)
+ li.timeline-item-container {
border-bottom-left-radius: var(--item-radius);
border-end-start-radius: var(--item-radius);
}
.box {
padding: 32px;
@ -2829,8 +2840,11 @@ ul.link-list li a .icon {
padding: 32px;
} */
li.timeline-item-carousel {
width: 95vw;
max-width: calc(320px * 3.3);
/* width: 95vw;
max-width: calc(320px * 3.3); */
transform: translateX(calc(-50% + var(--main-width) / 2));
&:dir(rtl) {
transform: translateX(calc(50% - var(--main-width) / 2));
}
}
}

View file

@ -9,7 +9,9 @@ import {
useState,
} from 'preact/hooks';
import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
import 'swiped-events';
import { subscribe } from 'valtio';
import BackgroundService from './components/background-service';
@ -54,6 +56,7 @@ import focusDeck from './utils/focus-deck';
import states, { initStates, statusKey } from './utils/states';
import store from './utils/store';
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
import './utils/toast-alert';
window.__STATES__ = states;
@ -129,6 +132,8 @@ setTimeout(() => {
setTimeout(() => {
if (Array.isArray(ICONS[icon])) {
ICONS[icon][0]?.();
} else if (typeof ICONS[icon] === 'object') {
ICONS[icon].module?.();
} else {
ICONS[icon]?.();
}

View file

@ -9,14 +9,17 @@ body.cloak,
.status .content-container,
.status .content-container *,
.status .content-compact > *,
.account-container .actions small,
.account-container :is(header, main > *:not(.actions)),
.account-container :is(header, main > *:not(.actions)) *,
.header-double-lines,
.header-double-lines *,
.account-block,
.catchup-filters .filter-author *,
.post-peek-html *,
.post-peek-content > *,
.request-notifications-account * {
.request-notifications-account *,
.status.compact-thread *,
.status .content-compact {
text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
@ -50,10 +53,19 @@ body.cloak,
body.cloak,
.cloak {
.header-double-lines *,
.account-container .profile-metadata b,
.account-container .actions small,
.account-container .stats *,
.media-container figcaption,
.media-container figcaption > *,
.catchup-filters .filter-author *,
.request-notifications-account * {
color: var(--text-color) !important;
}
.account-container .actions small,
.status .content-compact {
background-color: currentColor !important;
}
}

View file

@ -6,8 +6,14 @@ export const ICONS = {
'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-left': {
module: () => import('@iconify-icons/mingcute/arrow-left-line'),
rtl: true,
},
'arrow-right': {
module: () => import('@iconify-icons/mingcute/arrow-right-line'),
rtl: true,
},
'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'),
@ -16,8 +22,14 @@ export const ICONS = {
'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'),
comment: {
module: () => import('@iconify-icons/mingcute/chat-3-line'),
rtl: true,
},
comment2: {
module: () => import('@iconify-icons/mingcute/comment-2-line'),
rtl: true,
},
home: () => import('@iconify-icons/mingcute/home-3-line'),
notification: () => import('@iconify-icons/mingcute/notification-line'),
follow: () => import('@iconify-icons/mingcute/user-follow-line'),
@ -31,23 +43,46 @@ export const ICONS = {
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'],
external: {
module: () => import('@iconify-icons/mingcute/external-link-line'),
rtl: true,
},
popout: {
module: () => import('@iconify-icons/mingcute/external-link-line'),
rtl: true,
},
popin: {
module: () => import('@iconify-icons/mingcute/external-link-line'),
rotate: '180deg',
rtl: true,
},
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-left': {
module: () => import('@iconify-icons/mingcute/left-line'),
rtl: true,
},
'chevron-right': {
module: () => import('@iconify-icons/mingcute/right-line'),
rtl: true,
},
'chevron-down': () => import('@iconify-icons/mingcute/down-line'),
reply: [
() => import('@iconify-icons/mingcute/share-forward-line'),
'180deg',
'horizontal',
],
reply: {
module: () => import('@iconify-icons/mingcute/share-forward-line'),
rotate: '180deg',
flip: 'horizontal',
rtl: true,
},
thread: () => import('@iconify-icons/mingcute/route-line'),
group: () => import('@iconify-icons/mingcute/group-line'),
group: {
module: () => import('@iconify-icons/mingcute/group-line'),
rtl: true,
},
bot: () => import('@iconify-icons/mingcute/android-2-line'),
menu: () => import('@iconify-icons/mingcute/rows-4-line'),
list: () => import('@iconify-icons/mingcute/list-check-line'),
list: {
module: () => import('@iconify-icons/mingcute/list-check-line'),
rtl: true,
},
search: () => import('@iconify-icons/mingcute/search-2-line'),
hashtag: () => import('@iconify-icons/mingcute/hashtag-line'),
info: () => import('@iconify-icons/mingcute/information-line'),
@ -62,12 +97,21 @@ export const ICONS = {
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'),
exit: {
module: () => import('@iconify-icons/mingcute/exit-line'),
rtl: true,
},
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'),
mute: {
module: () => import('@iconify-icons/mingcute/volume-mute-line'),
rtl: true,
},
unmute: {
module: () => import('@iconify-icons/mingcute/volume-line'),
rtl: true,
},
block: () => import('@iconify-icons/mingcute/forbid-circle-line'),
unblock: [
() => import('@iconify-icons/mingcute/forbid-circle-line'),
@ -81,32 +125,54 @@ export const ICONS = {
filters: () => import('@iconify-icons/mingcute/filter-line'),
chart: () => import('@iconify-icons/mingcute/chart-line-line'),
react: () => import('@iconify-icons/mingcute/react-line'),
layout4: () => import('@iconify-icons/mingcute/layout-4-line'),
layout4: {
module: () => import('@iconify-icons/mingcute/layout-4-line'),
rtl: true,
},
layout5: () => import('@iconify-icons/mingcute/layout-5-line'),
announce: () => import('@iconify-icons/mingcute/announcement-line'),
announce: {
module: () => import('@iconify-icons/mingcute/announcement-line'),
rtl: true,
},
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'),
clipboard: {
module: () => import('@iconify-icons/mingcute/clipboard-line'),
rtl: true,
},
'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'),
month: {
module: () => import('@iconify-icons/mingcute/calendar-month-line'),
rtl: true,
},
media: () => import('@iconify-icons/mingcute/photo-album-line'),
speak: () => import('@iconify-icons/mingcute/radar-line'),
building: () => import('@iconify-icons/mingcute/building-5-line'),
history2: () => import('@iconify-icons/mingcute/history-2-line'),
history2: {
module: () => import('@iconify-icons/mingcute/history-2-line'),
rtl: true,
},
document: () => import('@iconify-icons/mingcute/document-line'),
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-line'),
'arrows-right': {
module: () => import('@iconify-icons/mingcute/arrows-right-line'),
rtl: true,
},
code: () => import('@iconify-icons/mingcute/code-line'),
copy: () => import('@iconify-icons/mingcute/copy-2-line'),
quote: () => import('@iconify-icons/mingcute/quote-left-line'),
quote: {
module: () => import('@iconify-icons/mingcute/quote-left-line'),
rtl: true,
},
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
'user-setting': () => import('@iconify-icons/mingcute/user-setting-line'),
minimize: () => import('@iconify-icons/mingcute/arrows-down-line'),
};

View file

@ -29,6 +29,8 @@
line-clamp: 1;
text-overflow: ellipsis;
overflow: hidden;
unicode-bidi: isolate;
direction: initial;
}
a {

View file

@ -120,7 +120,7 @@ function AccountBlock({
)}
</>
)}{' '}
<span class="account-block-acct">
<span class="account-block-acct bidi-isolate">
{acct2 ? '' : '@'}
{acct1}
<wbr />

View file

@ -57,7 +57,7 @@
background-repeat: no-repeat;
animation: swoosh-bg-image 0.3s ease-in-out 0.3s both;
background-image: linear-gradient(
to right,
var(--to-forward),
var(--original-color) 0%,
var(--original-color) calc(var(--originals-percentage) - var(--gap)),
var(--gap-color) calc(var(--originals-percentage) - var(--gap)),
@ -181,8 +181,8 @@
opacity: 1;
}
.sheet .account-container .header-banner {
border-top-left-radius: 16px;
border-top-right-radius: 16px;
border-start-start-radius: 16px;
border-start-end-radius: 16px;
}
.account-container .header-banner.header-is-avatar {
mask-image: linear-gradient(
@ -288,10 +288,17 @@
align-self: center !important;
/* clip a dog ear on top right */
clip-path: polygon(0 0, calc(100% - 4px) 0, 100% 4px, 100% 100%, 0 100%);
&:dir(rtl) {
/* top left */
clip-path: polygon(4px 0, 100% 0, 100% 100%, 0 100%, 0 4px);
}
/* 4x4px square on top right */
background-size: 4px 4px;
background-repeat: no-repeat;
background-position: top right;
&:dir(rtl) {
background-position: top left;
}
background-image: linear-gradient(
to bottom,
var(--private-note-border-color),
@ -311,7 +318,7 @@
box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
text-align: left;
text-align: start;
}
&:hover:not(:active) {
@ -370,7 +377,8 @@
animation: appear 1s both ease-in-out;
> *:not(:first-child) {
margin: 0 0 0 -4px;
margin: 0;
margin-inline-start: -4px;
}
}
}
@ -422,15 +430,15 @@
}
&:has(+ .account-metadata-box) {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-end-start-radius: 4px;
border-end-end-radius: 4px;
}
+ .account-metadata-box {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
border-start-start-radius: 4px;
border-start-end-radius: 4px;
border-end-start-radius: 16px;
border-end-end-radius: 16px;
}
}
@ -805,7 +813,7 @@
width: 100%;
th {
text-align: left;
text-align: start;
color: var(--text-insignificant-color);
font-weight: normal;
font-size: 0.8em;

View file

@ -33,9 +33,9 @@ import Icon from './icon';
import Link from './link';
import ListAddEdit from './list-add-edit';
import Loader from './loader';
import Menu2 from './menu2';
import MenuConfirm from './menu-confirm';
import MenuLink from './menu-link';
import Menu2 from './menu2';
import Modal from './modal';
import SubMenu2 from './submenu2';
import TranslationBlock from './translation-block';
@ -568,9 +568,11 @@ function AccountInfo({
</div>
<MenuItem
onClick={() => {
const handle = `@${acct}`;
const handleWithInstance = acct.includes('@')
? `@${acct}`
: `@${acct}@${instance}`;
try {
navigator.clipboard.writeText(handle);
navigator.clipboard.writeText(handleWithInstance);
showToast('Handle copied');
} catch (e) {
console.error(e);
@ -924,6 +926,8 @@ function RelatedActions({
const [currentInfo, setCurrentInfo] = useState(null);
const [isSelf, setIsSelf] = useState(false);
const acctWithInstance = acct.includes('@') ? acct : `${acct}@${instance}`;
useEffect(() => {
if (info) {
const currentAccount = getCurrentAccountID();
@ -1159,8 +1163,8 @@ function RelatedActions({
setRelationshipUIState('default');
showToast(
rel.showingReblogs
? `Boosts from @${username} disabled.`
: `Boosts from @${username} enabled.`,
? `Boosts from @${username} enabled.`
: `Boosts from @${username} disabled.`,
);
} catch (e) {
alert(e);
@ -1205,7 +1209,7 @@ function RelatedActions({
)}
<MenuItem
onClick={() => {
const handle = `@${currentInfo?.acct || acct}`;
const handle = `@${currentInfo?.acct || acctWithInstance}`;
try {
navigator.clipboard.writeText(handle);
showToast('Handle copied');
@ -1219,8 +1223,8 @@ function RelatedActions({
<small>
Copy handle
<br />
<span class="more-insignificant">
@{currentInfo?.acct || acct}
<span class="more-insignificant bidi-isolate">
@{currentInfo?.acct || acctWithInstance}
</span>
</small>
</MenuItem>
@ -1893,6 +1897,7 @@ function PrivateNoteSheet({
ref={textareaRef}
name="note"
disabled={uiState === 'loading'}
dir="auto"
>
{initialNote}
</textarea>
@ -2015,6 +2020,7 @@ function EditProfileSheet({ onClose = () => {} }) {
defaultValue={displayName}
maxLength={30}
disabled={uiState === 'loading'}
dir="auto"
/>
</label>
</p>
@ -2027,6 +2033,7 @@ function EditProfileSheet({ onClose = () => {} }) {
maxLength={500}
rows="5"
disabled={uiState === 'loading'}
dir="auto"
/>
</label>
</p>
@ -2090,6 +2097,7 @@ function FieldsAttributesRow({ name, value, disabled, index: i }) {
disabled={disabled}
maxLength={255}
required={hasValue}
dir="auto"
/>
</td>
<td>
@ -2100,6 +2108,7 @@ function FieldsAttributesRow({ name, value, disabled, index: i }) {
disabled={disabled}
maxLength={255}
onChange={(e) => setHasValue(!!e.currentTarget.value)}
dir="auto"
/>
</td>
</tr>

View file

@ -9,7 +9,7 @@ import useInterval from '../utils/useInterval';
import usePageVisibility from '../utils/usePageVisibility';
const STREAMING_TIMEOUT = 1000 * 3; // 3 seconds
const POLL_INTERVAL = 15_000; // 15 seconds
const POLL_INTERVAL = 20_000; // 20 seconds
export default memo(function BackgroundService({ isLoggedIn }) {
// Notifications service
@ -46,6 +46,7 @@ export default memo(function BackgroundService({ isLoggedIn }) {
useEffect(() => {
let sub;
let streamTimeout;
let pollNotifications;
if (isLoggedIn && visible) {
const { masto, streaming, instance } = api();
@ -56,7 +57,7 @@ export default memo(function BackgroundService({ isLoggedIn }) {
let hasStreaming = false;
// 2. Start streaming
if (streaming) {
pollNotifications = setTimeout(() => {
streamTimeout = setTimeout(() => {
(async () => {
try {
hasStreaming = true;
@ -94,7 +95,7 @@ export default memo(function BackgroundService({ isLoggedIn }) {
return () => {
sub?.unsubscribe?.();
sub = null;
clearTimeout(pollNotifications);
clearTimeout(streamTimeout);
clearInterval(pollNotifications);
};
}, [visible, isLoggedIn]);

View file

@ -16,7 +16,6 @@
}
#compose-container .compose-top {
text-align: right;
display: flex;
justify-content: space-between;
gap: 8px;
@ -62,7 +61,7 @@
box-shadow: 0 -3px 12px -3px var(--drop-shadow-color);
}
#compose-container .status-preview:has(.status-badge:not(:empty)) {
border-top-right-radius: 8px;
border-start-end-radius: 8px;
}
#compose-container .status-preview :is(.content-container, .time) {
pointer-events: none;
@ -208,7 +207,7 @@
left: -100vw !important;
}
#compose-container .toolbar-button select {
background-color: transparent;
background-color: inherit;
border: 0;
padding: 0 0 0 8px;
margin: 0;
@ -216,8 +215,8 @@
line-height: 1em;
}
#compose-container .toolbar-button:not(.show-field) select {
right: 0;
left: auto !important;
inset-inline-end: 0;
inset-inline-start: auto !important;
}
#compose-container
.toolbar-button:not(:disabled):is(
@ -303,6 +302,9 @@
}
#compose-container .text-expander-menu li[aria-selected] {
box-shadow: inset 4px 0 0 0 var(--button-bg-color);
:dir(rtl) & {
box-shadow: inset -4px 0 0 0 var(--button-bg-color);
}
}
#compose-container .text-expander-menu li[data-more] {
&:not(:hover, :focus, [aria-selected]) {
@ -494,14 +496,14 @@
display: flex;
gap: 4px;
align-items: center;
border-left: 1px solid var(--outline-color);
padding-left: 8px;
border-inline-start: 1px solid var(--outline-color);
padding-inline-start: 8px;
}
#compose-container .expires-in {
flex-grow: 1;
border-left: 1px solid var(--outline-color);
padding-left: 8px;
border-inline-start: 1px solid var(--outline-color);
padding-inline-start: 8px;
display: flex;
gap: 4px;
flex-wrap: wrap;
@ -646,7 +648,7 @@
&:hover {
background-image: linear-gradient(
to right,
var(--to-forward),
transparent 75%,
var(--link-bg-color)
);
@ -654,7 +656,7 @@
&.selected {
background-image: linear-gradient(
to right,
var(--to-forward),
var(--bg-faded-color) 75%,
var(--link-bg-color)
);
@ -666,8 +668,8 @@
border-top: var(--hairline-width) solid var(--divider-color);
position: absolute;
bottom: 0;
left: 58px;
right: 0;
inset-inline-start: 58px;
inset-inline-end: 0;
}
&:has(+ li:is(.selected, :hover)):before,
@ -951,7 +953,7 @@
overflow-x: auto;
overflow-y: hidden;
mask-image: linear-gradient(
to right,
var(--to-forward),
transparent 2px,
black 16px,
black calc(100% - 16px),

View file

@ -1,11 +1,10 @@
import './compose.css';
import '@github/text-expander-element';
import { MenuItem } from '@szhsin/react-menu';
import { deepEqual } from 'fast-equals';
import Fuse from 'fuse.js';
import { memo } from 'preact/compat';
import { forwardRef } from 'preact/compat';
import { forwardRef, memo } from 'preact/compat';
import {
useCallback,
useEffect,
@ -28,6 +27,7 @@ import urlRegex from '../data/url-regex';
import { api } from '../utils/api';
import db from '../utils/db';
import emojifyText from '../utils/emojify-text';
import isRTL from '../utils/is-rtl';
import localeMatch from '../utils/locale-match';
import localeCode2Text from '../utils/localeCode2Text';
import openCompose from '../utils/open-compose';
@ -104,7 +104,8 @@ const observer = new IntersectionObserver((entries) => {
const { left, width } = entry.boundingClientRect;
const { innerWidth } = window;
if (left + width > innerWidth) {
menu.style.left = innerWidth - width - windowMargin + 'px';
const insetInlineStart = isRTL() ? 'right' : 'left';
menu.style[insetInlineStart] = innerWidth - width - windowMargin + 'px';
}
}
});
@ -148,23 +149,22 @@ const SCAN_RE = new RegExp(
);
const segmenter = new Intl.Segmenter();
function highlightText(text, { maxCharacters = Infinity }) {
// Accept text string, return formatted HTML string
// Escape all HTML special characters
let html = text
function escapeHTML(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function highlightText(text, { maxCharacters = Infinity }) {
// Exceeded characters limit
const { composerCharacterCount } = states;
if (composerCharacterCount > maxCharacters) {
// Highlight exceeded characters
let withinLimitHTML = '',
exceedLimitHTML = '';
const htmlSegments = segmenter.segment(html);
const htmlSegments = segmenter.segment(text);
for (const { segment, index } of htmlSegments) {
if (index < maxCharacters) {
withinLimitHTML += segment;
@ -175,13 +175,13 @@ function highlightText(text, { maxCharacters = Infinity }) {
if (exceedLimitHTML) {
exceedLimitHTML =
'<mark class="compose-highlight-exceeded">' +
exceedLimitHTML +
escapeHTML(exceedLimitHTML) +
'</mark>';
}
return withinLimitHTML + exceedLimitHTML;
return escapeHTML(withinLimitHTML) + exceedLimitHTML;
}
return html
return escapeHTML(text)
.replace(urlRegexObj, '$2<mark class="compose-highlight-url">$3</mark>') // URLs
.replace(MENTION_RE, '$1<mark class="compose-highlight-mention">$2</mark>') // Mentions
.replace(HASHTAG_RE, '$1<mark class="compose-highlight-hashtag">$2</mark>') // Hashtags
@ -1129,6 +1129,7 @@ function Compose({
setVisibility(e.target.value);
}}
disabled={uiState === 'loading' || !!editStatus}
dir="auto"
>
<option value="public">
Public <Icon icon="earth" />
@ -1385,6 +1386,7 @@ function Compose({
store.session.set('currentLanguage', value || DEFAULT_LANG);
}}
disabled={uiState === 'loading'}
dir="auto"
>
{topSupportedLanguages.map(([code, common, native]) => (
<option value={code} key={code}>
@ -1718,7 +1720,9 @@ const Textarea = forwardRef((props, ref) => {
</span>
<span>
<b>${displayNameWithEmoji || username}</b>
<br>@${encodeHTML(acct)}
<br><span class="bidi-isolate">@${encodeHTML(
acct,
)}</span>
</span>
</li>
`;
@ -2317,10 +2321,8 @@ function MediaAttachment({
</div>
{showModal && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowModal(false);
}
onClose={() => {
setShowModal(false);
}}
>
<div id="media-sheet" class="sheet sheet-max">

View file

@ -27,7 +27,7 @@ button.draft-item {
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--link-faded-color);
text-align: left;
text-align: start;
padding: 0;
}
button.draft-item:is(:hover, :focus) {

View file

@ -62,13 +62,13 @@
border-top: var(--hairline-width) solid var(--divider-color);
position: absolute;
bottom: calc(-1 * var(--list-gap) / 2);
left: 40px;
right: 0;
inset-inline-start: 40px;
inset-inline-end: 0;
}
&:has(.reactions-block):before {
/* avatar + reactions + gap */
left: calc(40px + 16px + 8px);
inset-inline-start: calc(40px + 16px + 8px);
}
}

View file

@ -53,9 +53,14 @@ function Icon({
return null;
}
let rotate, flip;
let rotate,
flip,
rtl = false;
if (Array.isArray(iconBlock)) {
[iconBlock, rotate, flip] = iconBlock;
} else if (typeof iconBlock === 'object') {
({ rotate, flip, rtl } = iconBlock);
iconBlock = iconBlock.module;
}
const [iconData, setIconData] = useState(ICONDATA[icon]);
@ -72,13 +77,14 @@ function Icon({
return (
<span
class={`icon ${className}`}
class={`icon ${className} ${rtl ? 'rtl-flip' : ''}`}
title={title || alt}
style={{
width: `${iconSize}px`,
height: `${iconSize}px`,
...style,
}}
data-icon={icon}
>
{iconData && (
// <svg

View file

@ -0,0 +1,29 @@
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
const IntersectionView = ({ children, root = null, fallback = null }) => {
const ref = useRef();
const [show, setShow] = useState(false);
useLayoutEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
setShow(true);
observer.unobserve(ref.current);
}
},
{
root,
rootMargin: `${screen.height}px`,
},
);
if (ref.current) observer.observe(ref.current);
return () => {
if (ref.current) observer.unobserve(ref.current);
};
}, []);
return show ? children : <div ref={ref}>{fallback}</div>;
};
export default IntersectionView;

View file

@ -6,7 +6,7 @@
overflow-x: auto;
background-color: var(--bg-faded-color);
mask-image: linear-gradient(
to right,
var(--to-forward),
transparent,
black 16px,
black calc(100% - 16px),
@ -20,6 +20,9 @@
width: 95vw;
max-width: calc(320px * 3.3);
transform: translateX(calc(-50% + var(--main-width) / 2));
&:dir(rtl) {
transform: translateX(calc(50% - var(--main-width) / 2));
}
}
}
@ -38,12 +41,16 @@
color: var(--text-insignificant-color);
position: absolute;
top: 8px;
left: 0;
inset-inline-start: 0;
transform-origin: top left;
transform: rotate(-90deg) translateX(-100%);
&:dir(rtl) {
transform-origin: top right;
transform: rotate(90deg) translateX(100%);
}
user-select: none;
background-image: linear-gradient(
to left,
var(--to-backward),
var(--text-color),
var(--link-color)
);

View file

@ -10,14 +10,15 @@ import {
import { useHotkeys } from 'react-hotkeys-hook';
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
import isRTL from '../utils/is-rtl';
import showToast from '../utils/show-toast';
import states from '../utils/states';
import Icon from './icon';
import Link from './link';
import Media from './media';
import Menu2 from './menu2';
import MenuLink from './menu-link';
import Menu2 from './menu2';
const { PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL } = import.meta.env;
@ -54,7 +55,7 @@ function MediaModal({
const differentStatusID = prevStatusID.current !== statusID;
if (differentStatusID) prevStatusID.current = statusID;
carouselRef.current.scrollTo({
left: scrollLeft,
left: scrollLeft * (isRTL() ? -1 : 1),
behavior: differentStatusID ? 'auto' : 'smooth',
});
carouselRef.current.focus();
@ -91,7 +92,7 @@ function MediaModal({
useEffect(() => {
let handleScroll = () => {
const { clientWidth, scrollLeft } = carouselRef.current;
const index = Math.round(scrollLeft / clientWidth);
const index = Math.round(Math.abs(scrollLeft) / clientWidth);
setCurrentIndex(index);
};
if (carouselRef.current) {
@ -178,7 +179,7 @@ function MediaModal({
? {
backgroundAttachment: 'local',
backgroundImage: `linear-gradient(
to right, ${mediaAccentGradient})`,
to ${isRTL() ? 'left' : 'right'}, ${mediaAccentGradient})`,
}
: {}
}
@ -257,7 +258,8 @@ function MediaModal({
e.preventDefault();
e.stopPropagation();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * i,
left:
carouselRef.current.clientWidth * i * (isRTL() ? -1 : 1),
behavior: 'smooth',
});
carouselRef.current.focus();
@ -368,7 +370,10 @@ function MediaModal({
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex - 1),
left:
carouselRef.current.clientWidth *
(currentIndex - 1) *
(isRTL() ? -1 : 1),
behavior: 'smooth',
});
}}
@ -384,7 +389,10 @@ function MediaModal({
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex + 1),
left:
carouselRef.current.clientWidth *
(currentIndex + 1) *
(isRTL() ? -1 : 1),
behavior: 'smooth',
});
}}

View file

@ -23,7 +23,7 @@
pointer-events: none;
position: absolute;
top: 0;
left: 0;
inset-inline-start: 0;
z-index: 1;
background-color: var(--bg-blur-color);
margin: 8px;

View file

@ -1,5 +1,6 @@
import { getBlurHashAverageColor } from 'fast-blurhash';
import { Fragment } from 'preact';
import { memo } from 'preact/compat';
import {
useCallback,
useLayoutEffect,
@ -676,4 +677,14 @@ function getURLObj(url) {
return URL.parse(url, location.origin);
}
export default Media;
export default memo(Media, (oldProps, newProps) => {
const oldMedia = oldProps.media || {};
const newMedia = newProps.media || {};
return (
oldMedia?.id === newMedia?.id &&
oldMedia.url === newMedia.url &&
oldProps.to === newProps.to &&
oldProps.class === newProps.class
);
});

View file

@ -1,21 +1,33 @@
import { Menu } from '@szhsin/react-menu';
import { useWindowSize } from '@uidotdev/usehooks';
import { useRef } from 'preact/hooks';
import isRTL from '../utils/is-rtl';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import useWindowSize from '../utils/useWindowSize';
// It's like Menu but with sensible defaults, bug fixes and improvements.
function Menu2(props) {
const { containerProps, instanceRef: _instanceRef } = props;
const { containerProps, instanceRef: _instanceRef, align } = props;
const size = useWindowSize();
const instanceRef = _instanceRef?.current ? _instanceRef : useRef();
// Values: start, end, center
// Note: don't mess with 'center'
const rtlAlign = isRTL()
? align === 'end'
? 'start'
: align === 'start'
? 'end'
: align
: align;
return (
<Menu
boundingBoxPadding={safeBoundingBoxPadding()}
repositionFlag={`${size.width}x${size.height}`}
unmountOnClose
{...props}
align={rtlAlign}
instanceRef={instanceRef}
containerProps={{
onClick: (e) => {

View file

@ -1,7 +1,7 @@
#modal-container > div {
position: fixed;
top: 0;
right: 0;
inset-inline-end: 0;
height: 100%;
width: 100%;
z-index: 1000;
@ -26,21 +26,30 @@
user-select: none;
overflow: hidden;
transform: scale(0);
--right: max(
--end: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-right)
);
:dir(rtl) & {
--end: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-left)
);
}
--bottom: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-bottom)
);
--origin-right: calc(
100% - var(--compose-button-dimension-half) - var(--right)
--origin-end: calc(
100% - var(--compose-button-dimension-half) - var(--end)
);
:dir(rtl) & {
--origin-end: calc(var(--compose-button-dimension-half) + var(--end));
}
--origin-bottom: calc(
100% - var(--compose-button-dimension-half) - var(--bottom)
);
transform-origin: var(--origin-right) var(--origin-bottom);
transform-origin: var(--origin-end) var(--origin-bottom);
}
.sheet {

View file

@ -26,6 +26,9 @@ a.name-text.short:is(:hover, :focus) i {
font-style: normal;
opacity: 0.75;
}
.name-text i.instance {
opacity: 0.35;
}
.name-text .avatar {
vertical-align: middle;

View file

@ -88,15 +88,16 @@ function NameText({
)}
{displayName && !short ? (
<>
<b>
<b dir="auto">
<EmojiText text={displayName} emojis={emojis} />
</b>
{!showAcct && !hideUsername && (
{!showAcct && !hideUsername ? (
<>
{' '}
<i>@{username}</i>
<i class="bidi-isolate">@{username}</i>
</>
)}
) : ' '}
<i class="instance">{acct2}</i>
</>
) : short ? (
<i>{username}</i>
@ -106,7 +107,7 @@ function NameText({
{showAcct && (
<>
<br />
<i>
<i class="bidi-isolate">
{acct2 ? '' : '@'}
{acct1}
{!!acct2 && <span class="ib">{acct2}</span>}

View file

@ -35,11 +35,15 @@
}
.nav-menu section:last-child {
background-image: linear-gradient(
to right,
var(--to-forward),
var(--divider-color) 1px,
transparent 1px
),
linear-gradient(to bottom left, var(--bg-blur-color), transparent),
linear-gradient(
to bottom var(--backward),
var(--bg-blur-color),
transparent
),
url(../assets/phanpy-bg.svg);
background-repeat: no-repeat;
/* background-size: auto, auto, 200%; */
@ -49,8 +53,8 @@
position: sticky;
top: 0;
animation: phanpying 0.2s ease-in-out both;
border-top-right-radius: inherit;
border-bottom-right-radius: inherit;
border-start-end-radius: inherit;
border-end-end-radius: inherit;
margin-bottom: 0;
display: flex;
flex-direction: column;

View file

@ -1,6 +1,6 @@
import './nav-menu.css';
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
import { ControlledMenu, FocusableItem, MenuDivider, MenuItem } from '@szhsin/react-menu';
import { memo } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useLongPress } from 'use-long-press';
@ -18,6 +18,7 @@ import Avatar from './avatar';
import Icon from './icon';
import MenuLink from './menu-link';
import SubMenu2 from './submenu2';
import { accountsIsDtth, gtsDtthSettings } from '../utils/dtth';
function NavMenu(props) {
const snapStates = useSnapshot(states);
@ -209,6 +210,10 @@ function NavMenu(props) {
<Icon icon="user" size="l" /> <span>Profile</span>
</MenuLink>
)}
{currentAccount && accountsIsDtth(currentAccount) &&
<FocusableItem title="Takes you to DTTHDon settings">
<a href={gtsDtthSettings} target='_blank'><Icon icon="user-setting" size="l" /> <span>User Settings&hellip;</span></a>
</FocusableItem>}
{lists?.length > 0 ? (
<SubMenu2
menuClassName="nav-submenu"

View file

@ -147,8 +147,13 @@ function Notification({
report,
event,
moderation_warning,
// Client-side grouped notification
_ids,
_accounts,
_statuses,
// Server-side grouped notification
sampleAccounts,
notificationsCount,
} = notification;
let { type } = notification;
@ -167,12 +172,14 @@ function Notification({
let favsCount = 0;
let reblogsCount = 0;
if (type === 'favourite+reblog') {
for (const account of _accounts) {
if (account._types?.includes('favourite')) {
favsCount++;
}
if (account._types?.includes('reblog')) {
reblogsCount++;
if (_accounts) {
for (const account of _accounts) {
if (account._types?.includes('favourite')) {
favsCount++;
}
if (account._types?.includes('reblog')) {
reblogsCount++;
}
}
}
if (!reblogsCount && favsCount) type = 'favourite';
@ -261,7 +268,7 @@ function Notification({
return (
<div
class={`notification notification-${type}`}
data-notification-id={id}
data-notification-id={_ids || id}
tabIndex="0"
>
<div
@ -285,7 +292,7 @@ function Notification({
{type !== 'mention' && (
<>
<p>
{!/poll|update/i.test(type) && (
{!/poll|update|severed_relationships/i.test(type) && (
<>
{_accounts?.length > 1 ? (
<>
@ -296,6 +303,15 @@ function Notification({
people
</b>{' '}
</>
) : notificationsCount > 1 ? (
<>
<b>
<span title={notificationsCount}>
{shortenNumber(notificationsCount)}
</span>{' '}
people
</b>{' '}
</>
) : (
account && (
<>
@ -405,6 +421,54 @@ function Notification({
</button>
</p>
)}
{!_accounts?.length && sampleAccounts?.length > 1 && (
<p class="avatars-stack">
{sampleAccounts.map((account) => (
<Fragment key={account.id}>
<a
key={account.id}
href={account.url}
rel="noopener noreferrer"
class="account-avatar-stack"
onClick={(e) => {
e.preventDefault();
states.showAccount = account;
}}
>
<Avatar
url={account.avatarStatic}
size="xxl"
key={account.id}
alt={`${account.displayName} @${account.acct}`}
squircle={account?.bot}
/>
{/* {type === 'favourite+reblog' && (
<div class="account-sub-icons">
{account._types.map((type) => (
<Icon
icon={NOTIFICATION_ICONS[type]}
size="s"
class={`${type}-icon`}
/>
))}
</div>
)} */}
</a>{' '}
</Fragment>
))}
{notificationsCount > sampleAccounts.length && (
<Link
to={
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
}
class="button small plain centered"
>
+{notificationsCount - sampleAccounts.length}
<Icon icon="chevron-right" />
</Link>
)}
</p>
)}
{_statuses?.length > 1 && (
<ul class="notification-group-statuses">
{_statuses.map((status) => (

View file

@ -187,9 +187,6 @@ export default function Poll({
type="button"
class="plain small"
disabled={uiState === 'loading'}
style={{
marginLeft: -8,
}}
onClick={(e) => {
e.preventDefault();
setUIState('loading');

View file

@ -92,7 +92,7 @@
pointer-events: none;
user-select: none;
position: absolute;
right: 32px;
inset-inline-end: 32px;
margin-top: -48px;
animation: rubber-stamp 0.3s ease-in both;
position: absolute;
@ -148,7 +148,7 @@
}
.report-rules {
margin-left: 1.75em;
margin-inline-start: 1.75em;
}
}

View file

@ -273,6 +273,7 @@ const SearchForm = forwardRef((props, ref) => {
class={`search-popover-item ${i === 0 ? 'focus' : ''}`}
// hidden={hidden}
onClick={(e) => {
console.log('onClick', e);
props?.onSubmit?.(e);
}}
>

View file

@ -18,8 +18,8 @@
counter-increment: index;
display: inline-block;
width: 1.2em;
text-align: right;
margin-right: 8px;
text-align: end;
margin-inline-end: 8px;
color: var(--text-insignificant-color);
font-size: 90%;
flex-shrink: 0;
@ -55,12 +55,12 @@
justify-content: center;
}
#shortcuts-settings-container .shortcuts-view-mode label:first-child {
border-top-left-radius: 16px;
border-bottom-left-radius: 16px;
border-start-start-radius: 16px;
border-end-start-radius: 16px;
}
#shortcuts-settings-container .shortcuts-view-mode label:last-child {
border-top-right-radius: 16px;
border-bottom-right-radius: 16px;
border-start-end-radius: 16px;
border-end-end-radius: 16px;
}
#shortcuts-settings-container .shortcuts-view-mode label img {
max-height: 64px;
@ -114,7 +114,7 @@
}
#shortcut-settings-form label > span:first-child {
flex-basis: 5em;
text-align: right;
text-align: end;
}
#shortcut-settings-form :is(input[type='text'], select) {
flex-grow: 1;
@ -185,8 +185,8 @@
counter-increment: index;
display: inline-block;
width: 1.2em;
text-align: right;
margin-right: 8px;
text-align: end;
margin-inline-end: 8px;
color: var(--text-insignificant-color);
font-size: 90%;
flex-shrink: 0;

View file

@ -612,6 +612,7 @@ function ShortcutForm({
}}
defaultValue={editMode ? shortcut.type : undefined}
name="type"
dir="auto"
>
<option></option>
{TYPES.map((type) => (
@ -632,6 +633,7 @@ function ShortcutForm({
required={!notRequired}
disabled={disabled || uiState === 'loading'}
defaultValue={editMode ? shortcut.id : undefined}
dir="auto"
>
<option value=""></option>
{lists.map((list) => (
@ -663,6 +665,7 @@ function ShortcutForm({
autocapitalize="off"
spellCheck={false}
pattern={pattern}
dir="auto"
/>
{currentType === 'hashtag' &&
followedHashtags.length > 0 && (
@ -780,6 +783,7 @@ function ImportExport({ shortcuts, onClose }) {
onInput={(e) => {
setImportShortcutStr(e.target.value);
}}
dir="auto"
/>
{states.settings.shortcutSettingsCloudImportExport && (
<button
@ -996,6 +1000,7 @@ function ImportExport({ shortcuts, onClose }) {
showToast('Unable to copy shortcuts');
}
}}
dir="auto"
/>
</p>
<p>
@ -1055,16 +1060,16 @@ function ImportExport({ shortcuts, onClose }) {
const { note = '' } = relationship;
// const newNote = `${note}\n\n\n$<phanpy-shortcuts-settings>{shortcutsStr}</phanpy-shortcuts-settings>`;
let newNote = '';
const settingsJSON = JSON.stringify({
v: '1', // version
dt: Date.now(), // datetime stamp
data: shortcutsStr, // shortcuts settings string
});
if (
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/.test(
note,
)
) {
const settingsJSON = JSON.stringify({
v: '1', // version
dt: Date.now(), // datetime stamp
data: shortcutsStr, // shortcuts settings string
});
newNote = note.replace(
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/,
`<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`,

View file

@ -2,8 +2,8 @@
position: fixed;
bottom: 16px;
bottom: max(16px, env(safe-area-inset-bottom));
left: 16px;
left: max(16px, env(safe-area-inset-left));
inset-inline-start: 16px;
inset-inline-start: max(16px, env(safe-area-inset-left));
padding: 16px;
background-color: var(--bg-faded-blur-color);
z-index: 101;
@ -34,9 +34,9 @@
@media (min-width: calc(40em + 56px + 8px)) {
#shortcuts-button {
right: 16px;
right: max(16px, env(safe-area-inset-right));
left: auto;
inset-inline-end: 16px;
inset-inline-end: max(16px, env(safe-area-inset-right));
inset-inline-start: auto;
top: 16px;
top: max(16px, env(safe-area-inset-top));
bottom: auto;

View file

@ -15,8 +15,8 @@ import states from '../utils/states';
import AsyncText from './AsyncText';
import Icon from './icon';
import Link from './link';
import Menu2 from './menu2';
import MenuLink from './menu-link';
import Menu2 from './menu2';
import SubMenu2 from './submenu2';
function Shortcuts() {

View file

@ -1,22 +1,31 @@
/* REBLOG + REPLY-TO */
:root {
--post-gradient-angle: 160deg;
--post-gradient-chip-angle: -20deg;
&:dir(rtl) {
--post-gradient-angle: -160deg;
--post-gradient-chip-angle: 20deg;
}
}
.status-reblog {
background: linear-gradient(
160deg,
var(--post-gradient-angle),
var(--reblog-faded-color),
transparent min(160px, 50%)
);
}
.status-group {
background: linear-gradient(
160deg,
var(--post-gradient-angle),
var(--group-faded-color),
transparent min(160px, 50%)
);
}
.status-followed-tags {
background: linear-gradient(
160deg,
var(--post-gradient-angle),
var(--hashtag-faded-color),
transparent min(160px, 50%)
);
@ -33,14 +42,14 @@
}
.status-reply-to {
background: linear-gradient(
160deg,
var(--post-gradient-angle),
var(--reply-to-faded-color),
transparent min(160px, 50%)
);
}
:is(.status-reblog, .status-group, .status-followed-tags) .status-reply-to {
background: linear-gradient(
-20deg,
var(--post-gradient-chip-angle),
var(--reply-to-faded-color),
transparent min(160px, 50%)
);
@ -72,12 +81,12 @@
}
.status-reblog .status-pre-meta .icon {
color: var(--reblog-color);
margin-right: 4px;
margin-inline-end: 4px;
vertical-align: text-bottom;
}
.status-group .status-pre-meta .icon {
color: var(--group-color);
margin-right: 4px;
margin-inline-end: 4px;
vertical-align: text-bottom;
}
.status-followed-tags {
@ -91,7 +100,7 @@
.icon {
color: var(--hashtag-color);
margin-right: 4px;
margin-inline-end: 4px;
vertical-align: text-bottom;
}
a {
@ -208,7 +217,7 @@
/* filter: drop-shadow(0 2px 4px var(--bg-faded-color)); */
}
.status-card:has(.status-badge:not(:empty)) {
border-top-right-radius: 8px;
border-start-end-radius: 8px;
}
.status-card > * {
pointer-events: none;
@ -276,7 +285,8 @@
align-items: center;
.status-carousel & {
padding: 16px 16px 16px 24px;
padding: 16px;
padding-inline-start: 24px;
}
}
.status.filtered .status-filtered-info {
@ -286,7 +296,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
mask-image: linear-gradient(to right, black 90%, transparent);
mask-image: linear-gradient(var(--to-forward), black 90%, transparent);
position: relative;
}
.status.filtered .avatar {
@ -312,7 +322,7 @@
opacity: 0;
transform: translateX(8px);
position: absolute;
left: 0;
inset-inline-start: 0;
}
.status.filtered:is(:hover, :focus, :active) .status-filtered-info-2 {
opacity: 0.75;
@ -353,7 +363,7 @@
padding-bottom: 0;
margin-bottom: calc(-1 * var(--top-padding) / 2);
background-image: linear-gradient(
160deg,
var(--post-gradient-angle),
transparent 2.5%,
var(--reply-to-faded-color) 10%,
transparent
@ -381,7 +391,7 @@
content: '';
position: absolute;
top: calc(var(--top-padding) + var(--avatar-size));
left: var(--line-start);
inset-inline-start: var(--line-start);
width: var(--line-width);
height: calc(
100% - var(--top-padding) - var(--avatar-size) + (var(--top-padding) / 2)
@ -392,7 +402,7 @@
}
.avatar {
margin-left: calc((50px - var(--avatar-size)) / 2);
margin-inline-start: calc((50px - var(--avatar-size)) / 2);
justify-self: center;
z-index: 1;
}
@ -433,7 +443,7 @@
min-width: 0;
}
.status:not(.small) > .container {
padding-left: 12px;
padding-inline-start: 12px;
}
.status > .container > .meta {
@ -451,7 +461,7 @@
/* text-overflow: ellipsis; */
}
.status > .container > .meta .meta-name {
mask-image: linear-gradient(to left, transparent, black 16px);
mask-image: linear-gradient(var(--to-backward), transparent, black 16px);
flex-grow: 1;
.name-text b {
@ -470,7 +480,7 @@
text-align: end;
text-decoration: none;
flex-shrink: 0;
margin-left: 4px;
margin-inline-start: 4px;
white-space: nowrap;
}
.status > .container > .meta a.time {
@ -482,7 +492,7 @@
font-size: 90%;
.more {
margin-left: 4px;
margin-inline-start: 4px;
transition: transform 0.2s ease-out;
}
}
@ -509,7 +519,8 @@
.status-reply-badge {
display: inline-flex;
margin: 2px 0 2px 4px;
margin: 2px 0;
margin-inline-start: 4px;
gap: 4px;
align-items: center;
vertical-align: middle;
@ -609,7 +620,7 @@
position: absolute;
width: 100%;
top: calc(100% + 2px);
left: 0;
inset-inline-start: 0;
text-align: center;
}
.status-filtered-badge.horizontal.badge-meta > span + span {
@ -618,7 +629,7 @@
}
.status.large > .container > .content-container {
margin-left: calc(-50px - 16px);
margin-inline-start: calc(-50px - 16px);
padding-top: 10px;
padding-bottom: 10px;
}
@ -1005,13 +1016,13 @@
.media-gt2
) {
/* 50px = avatar size */
margin-left: calc(-1 * ((50px / 2)));
margin-inline-start: calc(-1 * ((50px / 2)));
/*
outer padding = 16px
gap = 12px
so... 16 - 12 = 4
*/
margin-right: -4px;
margin-inline-end: -4px;
}
.status.large :is(.media-container, .media-container.media-gt2) {
height: auto;
@ -1121,40 +1132,46 @@
}
/* Special media borders */
.status .media-container.media-eq2 .media:first-of-type {
border-radius: var(--media-radius) var(--media-radius-inner)
var(--media-radius-inner) var(--media-radius);
border-start-end-radius: var(--media-radius-inner);
border-end-end-radius: var(--media-radius-inner);
}
.status .media-container.media-eq2 .media:last-of-type {
border-radius: var(--media-radius-inner) var(--media-radius)
var(--media-radius) var(--media-radius-inner);
border-start-start-radius: var(--media-radius-inner);
border-end-start-radius: var(--media-radius-inner);
}
.status .media-container.media-eq3 .media:first-of-type {
border-radius: var(--media-radius) var(--media-radius-inner)
var(--media-radius-inner) var(--media-radius);
border-start-end-radius: var(--media-radius-inner);
border-end-end-radius: var(--media-radius-inner);
}
.status .media-container.media-eq3 .media:nth-of-type(2) {
border-radius: var(--media-radius-inner) var(--media-radius)
var(--media-radius-inner) var(--media-radius-inner);
border-start-start-radius: var(--media-radius-inner);
border-end-end-radius: var(--media-radius-inner);
border-end-start-radius: var(--media-radius-inner);
}
.status .media-container.media-eq3 .media:last-of-type {
border-radius: var(--media-radius-inner) var(--media-radius-inner)
var(--media-radius) var(--media-radius-inner);
border-start-start-radius: var(--media-radius-inner);
border-start-end-radius: var(--media-radius-inner);
border-end-start-radius: var(--media-radius-inner);
}
.status .media-container.media-eq4 .media:first-of-type {
border-radius: var(--media-radius) var(--media-radius-inner)
var(--media-radius-inner) var(--media-radius-inner);
border-start-end-radius: var(--media-radius-inner);
border-end-end-radius: var(--media-radius-inner);
border-end-start-radius: var(--media-radius-inner);
}
.status .media-container.media-eq4 .media:nth-of-type(2) {
border-radius: var(--media-radius-inner) var(--media-radius)
var(--media-radius-inner) var(--media-radius-inner);
border-start-start-radius: var(--media-radius-inner);
border-end-end-radius: var(--media-radius-inner);
border-end-start-radius: var(--media-radius-inner);
}
.status .media-container.media-eq4 .media:nth-of-type(3) {
border-radius: var(--media-radius-inner) var(--media-radius-inner)
var(--media-radius-inner) var(--media-radius);
border-start-start-radius: var(--media-radius-inner);
border-start-end-radius: var(--media-radius-inner);
border-end-end-radius: var(--media-radius-inner);
}
.status .media-container.media-eq4 .media:last-of-type {
border-radius: var(--media-radius-inner) var(--media-radius-inner)
var(--media-radius) var(--media-radius-inner);
border-start-start-radius: var(--media-radius-inner);
border-start-end-radius: var(--media-radius-inner);
border-end-start-radius: var(--media-radius-inner);
}
.status .media:only-child {
grid-area: span 2 / span 2;
@ -1207,7 +1224,7 @@
.alt-badge {
position: absolute;
bottom: 8px;
left: 8px;
inset-inline-start: 8px;
&:before {
content: '';
@ -1266,7 +1283,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
content: attr(data-formatted-duration);
position: absolute;
bottom: 8px;
right: 8px;
inset-inline-end: 8px;
color: var(--media-fg-color);
background-color: var(--media-bg-color);
border: var(--hairline-width) solid var(--media-outline-color);
@ -1283,7 +1300,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
content: attr(data-label);
position: absolute;
bottom: 8px;
right: 8px;
inset-inline-end: 8px;
color: var(--media-fg-color);
background-color: var(--media-bg-color);
border: var(--hairline-width) solid var(--media-outline-color);
@ -1456,7 +1473,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
z-index: 1;
position: absolute;
top: 8px;
right: 8px;
inset-inline-end: 8px;
color: var(--media-fg-color);
background-color: var(--media-bg-color);
padding: 2px 8px;
@ -1484,8 +1501,8 @@ body:has(#modal-container .carousel) .status .media img:hover {
}
+ .carousel-button {
left: auto;
right: 8px;
inset-inline-start: auto;
inset-inline-end: 8px;
}
}
@ -1655,7 +1672,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
display: none;
+ * {
margin-left: 1ex;
margin-inline-start: 1ex;
}
}
}
@ -1746,9 +1763,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
--bottom: 16px;
bottom: var(--bottom);
bottom: calc(var(--bottom) + env(safe-area-inset-bottom));
left: 16px;
left: calc(16px + env(safe-area-inset-left));
text-align: left;
inset-inline-start: 16px;
inset-inline-start: calc(16px + env(safe-area-inset-left));
text-align: start;
border-radius: 8px;
color: var(--text-color);
padding: 4px 8px;
@ -1949,13 +1966,17 @@ a.card:is(:hover, :focus):visited {
.title {
font-weight: 500;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.meta {
-webkit-line-clamp: 5;
line-clamp: 5;
opacity: 1;
font-size: inherit;
/* font-size: inherit; */
}
}
.status.large .card.large.card-post,
@ -2019,8 +2040,8 @@ a.card:is(:hover, :focus):visited {
z-index: 0;
}
.poll-option:first-child:after {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
border-start-start-radius: 12px;
border-start-end-radius: 12px;
}
.poll-option:hover:after {
opacity: 1;
@ -2039,7 +2060,8 @@ a.card:is(:hover, :focus):visited {
.poll-label input:is([type='radio'], [type='checkbox']) {
flex-shrink: 0;
margin: 0 3px;
min-height: 0.9em;
min-height: 1.15em;
accent-color: var(--link-color);
}
.poll-option-votes {
flex-shrink: 0;
@ -2052,7 +2074,9 @@ a.card:is(:hover, :focus):visited {
opacity: 1;
}
.poll-vote-button {
margin: 8px 8px 0 12px;
margin: 8px 0 0;
margin-inline-start: 12px;
margin-inline-end: 8px;
/* padding-inline: 24px; */
min-width: 160px;
}
@ -2061,6 +2085,10 @@ a.card:is(:hover, :focus):visited {
margin: 8px 16px;
font-size: 90%;
user-select: none;
> button:first-child {
margin-inline-start: -8px;
}
}
.poll-option-title {
text-shadow: 0 1px var(--bg-color);
@ -2096,14 +2124,14 @@ a.card:is(:hover, :focus):visited {
}
.status.large .extra-meta {
padding-top: 0;
margin-left: calc(-50px - 16px);
margin-inline-start: calc(-50px - 16px);
}
/* EMOJI REACTIONS */
.status.large .emoji-reactions {
cursor: default;
margin-left: calc(-50px - 16px);
margin-inline-start: calc(-50px - 16px);
}
/* ACTIONS */
@ -2115,7 +2143,7 @@ a.card:is(:hover, :focus):visited {
.status.large .actions {
padding-top: 4px;
padding-bottom: 16px;
margin-left: calc(-50px - 16px);
margin-inline-start: calc(-50px - 16px);
color: var(--text-insignificant-color);
border-top: var(--hairline-width) solid var(--outline-color);
margin-top: 8px;
@ -2274,7 +2302,7 @@ a.card:is(:hover, :focus):visited {
width: 100%;
border: 1px solid var(--outline-color);
background: linear-gradient(
to bottom right,
to bottom var(--forward),
var(--bg-faded-color),
transparent 160px
);
@ -2292,7 +2320,7 @@ a.card:is(:hover, :focus):visited {
display: flex;
position: absolute;
top: -6px;
right: 8px;
inset-inline-end: 8px;
background-color: var(--bg-color);
border-radius: 8px;
z-index: 1;
@ -2302,7 +2330,7 @@ a.card:is(:hover, :focus):visited {
opacity: 0;
pointer-events: none;
transform: translate3d(0, 6px, 0);
transform-origin: right center;
transform-origin: var(--forward) center;
transition: all 0.15s ease-out 0.3s, border-color 0.3s ease-out;
.timeline.contextual .replies[data-comments-level='4'] & {
@ -2381,8 +2409,13 @@ a.card:is(:hover, :focus):visited {
}
}
.timeline.contextual .descendant .status {
--bg-gradient-rotation: -140deg;
:dir(rtl) & {
--bg-gradient-rotation: 140deg;
}
--bg-gradient: linear-gradient(
-140deg,
var(--bg-gradient-rotation),
var(--bg-faded-color),
transparent 75%
);
@ -2409,7 +2442,7 @@ a.card:is(:hover, :focus):visited {
.status-badge {
position: absolute;
top: 4px;
right: 4px;
inset-inline-end: 4px;
line-height: 0;
pointer-events: none;
opacity: 0.75;
@ -2436,8 +2469,21 @@ a.card:is(:hover, :focus):visited {
transform: translateX(0);
}
}
@keyframes swoosh-from-left {
0% {
opacity: 0;
transform: translateX(-300%);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.status-badge > * {
animation: swoosh-from-right 1s cubic-bezier(0.51, 0.28, 0.16, 1.26) both;
:dir(rtl) & {
animation-name: swoosh-from-left;
}
}
.status-badge > *:nth-child(2) {
animation-delay: 0.1s;
@ -2452,7 +2498,8 @@ a.card:is(:hover, :focus):visited {
/* MISC */
.status-aside {
padding: 0 16px 16px 80px;
padding: 0 16px 16px;
padding-inline-start: 80px;
color: var(--text-insignificant-color);
}
@ -2471,24 +2518,39 @@ a.card:is(:hover, :focus):visited {
#edit-history {
min-height: 50vh;
min-height: 50dvh;
}
#edit-history h2 {
margin: 0;
padding: 0;
}
h2 {
margin: 0;
padding: 0;
}
#edit-history ol,
#edit-history ol li {
list-style: none;
margin: 0;
padding: 0;
}
ol,
ol li {
list-style: none;
margin: 0;
padding: 0;
}
#edit-history .history-item .status {
border: 1px solid var(--outline-color);
border-radius: 8px;
pointer-events: none;
.history-item .status {
border: 1px solid var(--outline-color);
border-radius: 8px;
pointer-events: none;
}
.status {
.invisible {
display: revert;
}
.hashtag-stuffing {
white-space: normal;
opacity: 1;
}
a {
color: var(--text-color);
}
}
}
/* EMBED */
@ -2720,7 +2782,7 @@ a.card:is(:hover, :focus):visited {
vertical-align: super;
font-weight: normal;
line-height: 0;
padding-left: 2px;
padding-inline-start: 2px;
}
&.clickable {

View file

@ -1,6 +1,6 @@
import './status.css';
import '@justinribeiro/lite-youtube';
import {
ControlledMenu,
Menu,
@ -32,8 +32,8 @@ import CustomEmoji from '../components/custom-emoji';
import EmojiText from '../components/emoji-text';
import LazyShazam from '../components/lazy-shazam';
import Loader from '../components/loader';
import Menu2 from '../components/menu2';
import MenuConfirm from '../components/menu-confirm';
import Menu2 from '../components/menu2';
import Modal from '../components/modal';
import NameText from '../components/name-text';
import Poll from '../components/poll';
@ -46,6 +46,7 @@ import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links';
import htmlContentLength from '../utils/html-content-length';
import isRTL from '../utils/is-rtl';
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
import localeMatch from '../utils/locale-match';
import mem from '../utils/mem';
@ -69,8 +70,7 @@ import visibilityIconsMap from '../utils/visibility-icons-map';
import Avatar from './avatar';
import Icon from './icon';
import Link from './link';
import Media from './media';
import { isMediaCaptionLong } from './media';
import Media, { isMediaCaptionLong } from './media';
import MenuLink from './menu-link';
import RelativeTime from './relative-time';
import TranslationBlock from './translation-block';
@ -283,6 +283,7 @@ function Status({
url,
emojis,
tags,
pinned,
// Non-API props
_deleted,
_pinned,
@ -1122,22 +1123,20 @@ function Status({
try {
const newStatus = await masto.v1.statuses
.$select(id)
[_pinned ? 'unpin' : 'pin']();
// saveStatus(newStatus, instance);
[pinned ? 'unpin' : 'pin']();
saveStatus(newStatus, instance);
showToast(
_pinned
pinned
? 'Post unpinned from profile'
: 'Post pinned to profile',
);
} catch (e) {
console.error(e);
showToast(
_pinned ? 'Unable to unpin post' : 'Unable to pin post',
);
showToast(pinned ? 'Unable to unpin post' : 'Unable to pin post');
}
}}
>
{_pinned ? (
{pinned ? (
<>
<Icon icon="unpin" />
<span>Unpin from profile</span>
@ -2364,7 +2363,7 @@ function MediaFirstContainer(props) {
useEffect(() => {
let handleScroll = () => {
const { clientWidth, scrollLeft } = carouselRef.current;
const index = Math.round(scrollLeft / clientWidth);
const index = Math.round(Math.abs(scrollLeft) / clientWidth);
setCurrentIndex(index);
};
if (carouselRef.current) {
@ -2408,7 +2407,10 @@ function MediaFirstContainer(props) {
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex - 1),
left:
carouselRef.current.clientWidth *
(currentIndex - 1) *
(isRTL() ? -1 : 1),
behavior: 'smooth',
});
}}
@ -2426,7 +2428,10 @@ function MediaFirstContainer(props) {
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex + 1),
left:
carouselRef.current.clientWidth *
(currentIndex + 1) *
(isRTL() ? -1 : 1),
behavior: 'smooth',
});
}}
@ -2456,6 +2461,22 @@ function MediaFirstContainer(props) {
);
}
function getDomain(url) {
return punycode.toUnicode(
URL.parse(url)
.hostname.replace(/^www\./, '')
.replace(/\/$/, ''),
);
}
// "Post": Quote post + card link preview combo
// Assume all links from these domains are "posts"
// Mastodon links are "posts" too but they are converted to real quote posts and there's too many domains to check
// This is just "Progressive Enhancement"
function isCardPost(domain) {
return ['x.com', 'twitter.com', 'threads.net', 'bsky.app'].includes(domain);
}
function Card({ card, selfReferential, instance }) {
const snapStates = useSnapshot(states);
const {
@ -2534,11 +2555,7 @@ function Card({ card, selfReferential, instance }) {
);
if (hasText && (image || (type === 'photo' && blurhash))) {
const domain = punycode.toUnicode(
URL.parse(url)
.hostname.replace(/^www\./, '')
.replace(/\/$/, ''),
);
const domain = getDomain(url);
let blurhashImage;
const rgbAverageColor =
image && blurhash ? getBlurHashAverageColor(blurhash) : null;
@ -2559,11 +2576,7 @@ function Card({ card, selfReferential, instance }) {
blurhashImage = canvas.toDataURL();
}
// "Post": Quote post + card link preview combo
// Assume all links from these domains are "posts"
// Mastodon links are "posts" too but they are converted to real quote posts and there's too many domains to check
// This is just "Progressive Enhancement"
const isPost = ['x.com', 'twitter.com', 'threads.net'].includes(domain);
const isPost = isCardPost(domain);
return (
<a
@ -2573,8 +2586,6 @@ function Card({ card, selfReferential, instance }) {
class={`card link ${isPost ? 'card-post' : ''} ${
blurhashImage ? '' : size
}`}
lang={language}
dir="auto"
style={{
'--average-color':
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
@ -2601,7 +2612,7 @@ function Card({ card, selfReferential, instance }) {
}}
/>
</div>
<div class="meta-container">
<div class="meta-container" lang={language}>
<p class="meta domain">
<span class="domain">{domain}</span>{' '}
{!!publishedAt && <>&middot; </>}
@ -2669,16 +2680,16 @@ function Card({ card, selfReferential, instance }) {
// );
}
if (hasText && !image) {
const domain = punycode.toUnicode(
URL.parse(url).hostname.replace(/^www\./, ''),
);
const domain = getDomain(url);
const isPost = isCardPost(domain);
return (
<a
href={cardStatusURL || url}
target={cardStatusURL ? null : '_blank'}
rel="nofollow noopener noreferrer"
class={`card link no-image`}
class={`card link ${isPost ? 'card-post' : ''} no-image`}
lang={language}
dir="auto"
onClick={handleClick}
>
<div class="meta-container">
@ -2981,6 +2992,7 @@ function EmbedModal({ post, instance, onClose }) {
onClick={(e) => {
e.target.select();
}}
dir="auto"
>
{htmlCode}
</textarea>
@ -3127,6 +3139,7 @@ function EmbedModal({ post, instance, onClose }) {
<output
class="embed-preview"
dangerouslySetInnerHTML={{ __html: htmlCode }}
dir="auto"
/>
<p>
<small>Note: This preview is lightly styled.</small>

View file

@ -13,6 +13,7 @@ import { useSnapshot } from 'valtio';
import FilterContext from '../utils/filter-context';
import { filteredItems, isFiltered } from '../utils/filters';
import isRTL from '../utils/is-rtl';
import states, { statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek';
import { isMediaFirstInstance } from '../utils/store-utils';
@ -388,6 +389,17 @@ function Timeline({
dotRef.current = node;
}}
tabIndex="-1"
onClick={(e) => {
// If click on timeline item, unhide header
if (
headerRef.current &&
e.target.closest('.timeline-item, .timeline-item-alt')
) {
setTimeout(() => {
headerRef.current.hidden = false;
}, 250);
}
}}
>
<div class="timeline-deck deck">
<header
@ -561,7 +573,7 @@ const TimelineItem = memo(
: `/s/${actualStatusID}`;
if (items) {
const fItems = filteredItems(items, filterContext);
let fItems = filteredItems(items, filterContext);
let title = '';
if (type === 'boosts') {
title = `${fItems.length} Boosts`;
@ -570,6 +582,7 @@ const TimelineItem = memo(
}
const isCarousel = type === 'boosts' || type === 'pinned';
if (isCarousel) {
const filteredItemsIDs = new Set();
// Here, we don't hide filtered posts, but we sort them last
fItems.sort((a, b) => {
// if (a._filtered && !b._filtered) {
@ -580,6 +593,8 @@ const TimelineItem = memo(
// }
const aFiltered = isFiltered(a.filtered, filterContext);
const bFiltered = isFiltered(b.filtered, filterContext);
if (aFiltered) filteredItemsIDs.add(a.id);
if (bFiltered) filteredItemsIDs.add(b.id);
if (aFiltered && !bFiltered) {
return 1;
}
@ -588,11 +603,69 @@ const TimelineItem = memo(
}
return 0;
});
if (filteredItemsIDs.size >= 2) {
const GROUP_SIZE = 5;
// If 2 or more, group filtered items into one, limit to GROUP_SIZE in a group
const unfiltered = [];
const filtered = [];
fItems.forEach((item) => {
if (filteredItemsIDs.has(item.id)) {
filtered.push(item);
} else {
unfiltered.push(item);
}
});
const filteredItems = [];
for (let i = 0; i < filtered.length; i += GROUP_SIZE) {
filteredItems.push({
_grouped: true,
posts: filtered.slice(i, i + GROUP_SIZE),
});
}
fItems = unfiltered.concat(filteredItems);
}
return (
<li key={`timeline-${statusID}`} class="timeline-item-carousel">
<StatusCarousel title={title} class={`${type}-carousel`}>
{fItems.map((item) => {
const { id: statusID, reblog, _pinned } = item;
const { id: statusID, reblog, _pinned, _grouped } = item;
if (_grouped) {
return (
<li key={statusID} class="timeline-item-carousel-group">
{item.posts.map((item) => {
const { id: statusID, reblog, _pinned } = item;
const actualStatusID = reblog?.id || statusID;
const url = instance
? `/${instance}/s/${actualStatusID}`
: `/s/${actualStatusID}`;
if (_pinned) useItemID = false;
return (
<Link
class="status-carousel-link timeline-item-alt"
to={url}
>
{useItemID ? (
<Status
statusID={statusID}
instance={instance}
size="s"
/>
) : (
<Status
status={item}
instance={instance}
size="s"
/>
)}
</Link>
);
})}
</li>
);
}
const actualStatusID = reblog?.id || statusID;
const url = instance
? `/${instance}/s/${actualStatusID}`
@ -792,8 +865,11 @@ function StatusCarousel({ title, class: className, children }) {
class="small plain2"
// disabled={reachStart}
onClick={() => {
const left =
Math.min(320, carouselRef.current?.offsetWidth) *
(isRTL() ? 1 : -1);
carouselRef.current?.scrollBy({
left: -Math.min(320, carouselRef.current?.offsetWidth),
left,
behavior: 'smooth',
});
}}
@ -806,8 +882,11 @@ function StatusCarousel({ title, class: className, children }) {
class="small plain2"
// disabled={reachEnd}
onClick={() => {
const left =
Math.min(320, carouselRef.current?.offsetWidth) *
(isRTL() ? -1 : 1);
carouselRef.current?.scrollBy({
left: Math.min(320, carouselRef.current?.offsetWidth),
left,
behavior: 'smooth',
});
}}

View file

@ -35,7 +35,7 @@
border-bottom: 0;
margin-bottom: -1px;
background-image: linear-gradient(
to top left,
to top var(--backward),
var(--bg-color) 50%,
var(--bg-faded-blur-color)
);
@ -44,12 +44,13 @@
.status-translation-block .translated-block {
border: 1px solid var(--outline-color);
line-height: 1.3;
border-radius: 0 8px 8px 8px;
border-radius: 8px;
border-start-start-radius: 0;
margin: 0;
padding: 8px;
background-color: var(--bg-color);
background-image: linear-gradient(
to bottom right,
to bottom var(--forward),
var(--bg-color),
var(--bg-faded-blur-color)
);

View file

@ -1,7 +1,5 @@
import './index.css';
import './app.css';
import './polyfills';
import { render } from 'preact';

View file

@ -3,5 +3,6 @@
"@mastodon/list-exclusive": ">=4.2",
"@mastodon/filtered-notifications": "~4.3 || >=4.3",
"@mastodon/fetch-multiple-statuses": "~4.3 || >=4.3",
"@mastodon/trending-link-posts": "~4.3 || >=4.3"
"@mastodon/trending-link-posts": "~4.3 || >=4.3",
"@mastodon/grouped-notifications": "~4.3 || >=4.3"
}

View file

@ -8,7 +8,7 @@
--sai-left: env(safe-area-inset-left);
--text-size: 16px;
--main-width: 40em;
--main-width: max(60dvw, 40em);
text-size-adjust: none;
--hairline-width: 1px;
--monospace-font: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono',
@ -110,6 +110,17 @@
--spring-timing-funtion: cubic-bezier(0.175, 0.885, 0.32, 1.275);
--min-dimension: 88px;
--forward: right;
--backward: left;
--to-forward: to right;
--to-backward: to left;
&:dir(rtl) {
--forward: left;
--backward: right;
--to-forward: to left;
--to-backward: to right;
}
}
@media (min-resolution: 2dppx) {
@ -378,11 +389,17 @@ textarea:disabled {
width: 100%;
}
button.small {
:is(button, .button).small {
font-size: 90%;
padding: 4px 8px;
}
.button.centered {
display: inline-flex;
justify-content: center;
align-items: center;
}
select.plain {
border: 0;
background-color: transparent;
@ -436,6 +453,11 @@ kbd {
display: initial;
}
.bidi-isolate {
direction: initial;
unicode-bidi: isolate;
}
/* KEYFRAMES */
@keyframes appear {

View file

@ -1,7 +1,5 @@
import './index.css';
import './cloak-mode.css';
import './polyfills';
// Polyfill needed for Firefox < 122

View file

@ -19,8 +19,7 @@ import Timeline from '../components/timeline';
import { api } from '../utils/api';
import pmem from '../utils/pmem';
import showToast from '../utils/show-toast';
import states from '../utils/states';
import { saveStatus } from '../utils/states';
import states, { saveStatus } from '../utils/states';
import { isMediaFirstInstance } from '../utils/store-utils';
import useTitle from '../utils/useTitle';

View file

@ -28,7 +28,7 @@
}
#accounts-container section > ul > li .current {
margin-right: 8px;
margin-inline-end: 8px;
color: var(--green-color);
opacity: 0.1;
}
@ -47,7 +47,7 @@
}
#accounts-container .avatar {
margin-right: 8px;
margin-inline-end: 8px;
}
#accounts-container .accounts-list li div {

View file

@ -7,8 +7,8 @@ import { useReducer } from 'preact/hooks';
import Avatar from '../components/avatar';
import Icon from '../components/icon';
import Link from '../components/link';
import Menu2 from '../components/menu2';
import MenuConfirm from '../components/menu-confirm';
import Menu2 from '../components/menu2';
import NameText from '../components/name-text';
import { api } from '../utils/api';
import states from '../utils/states';

View file

@ -111,7 +111,7 @@
margin-bottom: 8px;
align-items: center;
gap: 8px;
text-align: left;
text-align: start;
justify-content: space-between;
a {
@ -146,6 +146,9 @@
input[type='range'] {
accent-color: var(--link-color);
direction: rtl;
&:dir(rtl) {
direction: ltr;
}
}
}
@ -251,7 +254,7 @@
overflow-y: hidden;
max-width: 100%;
mask-image: linear-gradient(
to right,
var(--to-forward),
transparent,
black 16px calc(100% - 16px),
transparent
@ -315,7 +318,7 @@
.count {
font-size: 70%;
margin-left: 4px;
margin-inline-start: 4px;
background-color: var(--bg-color);
padding: 4px 6px;
border-radius: 12px;
@ -386,7 +389,7 @@
.count {
position: absolute;
right: -4px;
inset-inline-end: -4px;
top: -4px;
font-size: 10px;
background-color: var(--bg-color);
@ -406,7 +409,7 @@
overflow: hidden;
text-align: center;
mask-image: linear-gradient(
to right,
var(--to-forward),
black calc(100% - 0.5em),
transparent 100%
);
@ -478,13 +481,13 @@
> li {
&:first-child > a {
border-top-left-radius: var(--corner-radius);
border-top-right-radius: var(--corner-radius);
border-start-start-radius: var(--corner-radius);
border-start-end-radius: var(--corner-radius);
}
&:last-child > a {
border-bottom-left-radius: var(--corner-radius);
border-bottom-right-radius: var(--corner-radius);
border-end-start-radius: var(--corner-radius);
border-end-end-radius: var(--corner-radius);
}
}
}
@ -502,13 +505,13 @@
@media (min-width: 40em) {
&.separator + li a {
border-top-left-radius: var(--corner-radius);
border-top-right-radius: var(--corner-radius);
border-start-start-radius: var(--corner-radius);
border-start-end-radius: var(--corner-radius);
}
&:has(+ .separator) a {
border-bottom-left-radius: var(--corner-radius);
border-bottom-right-radius: var(--corner-radius);
border-end-start-radius: var(--corner-radius);
border-end-end-radius: var(--corner-radius);
}
}
@ -572,8 +575,12 @@
'author meta'
'content content';
/* align-items: center; */
--bg-gradient-angle: 140deg;
&:dir(rtl) {
--bg-gradient-angle: -140deg;
}
background-image: linear-gradient(
140deg,
var(--bg-gradient-angle),
var(--post-bg-color),
transparent min(160px, 50%)
);
@ -636,7 +643,7 @@
}
> .avatar ~ .avatar {
margin-left: -8px;
margin-inline-start: -8px;
}
> .icon {
@ -655,7 +662,7 @@
white-space: nowrap;
overflow: hidden;
mask-image: linear-gradient(
to right,
var(--to-forward),
black calc(100% - 1em),
transparent 100%
);
@ -887,12 +894,15 @@
&:has(.post-peek-media),
.post-peek-media:first-child img {
transform-origin: left center;
:dir(rtl) & {
transform-origin: right center;
}
}
}
@media (max-width: 480px) {
.post-peek-media:not(:last-child) {
margin-right: -24px;
margin-inline-end: -24px;
box-shadow: 0 0 0 2px var(--bg-blur-color);
}
/* Max 10, I'm not going to code more than this */

View file

@ -1257,6 +1257,10 @@ function Catchup() {
}
onChange={() => {
setSelectedFilterCategory(label);
if (label === 'Boosts') {
setSortBy('reblogsCount');
setGroupBy(null);
}
// setSelectedAuthor(null);
}}
/>

View file

@ -365,6 +365,7 @@ function FiltersAddEdit({ filter, onClose }) {
defaultValue={keyword}
disabled={uiState === 'loading'}
required
dir="auto"
/>
<div class="filter-keyword-actions">
<label>

View file

@ -4,8 +4,7 @@ import { useSnapshot } from 'valtio';
import Timeline from '../components/timeline';
import { api } from '../utils/api';
import { filteredItems } from '../utils/filters';
import states from '../utils/states';
import { getStatus, saveStatus } from '../utils/states';
import states, { getStatus, saveStatus } from '../utils/states';
import supports from '../utils/supports';
import {
assignFollowedTags,

View file

@ -9,15 +9,14 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import Icon from '../components/icon';
import Menu2 from '../components/menu2';
import MenuConfirm from '../components/menu-confirm';
import Menu2 from '../components/menu2';
import { SHORTCUTS_LIMIT } from '../components/shortcuts-settings';
import Timeline from '../components/timeline';
import { api } from '../utils/api';
import { filteredItems } from '../utils/filters';
import showToast from '../utils/show-toast';
import states from '../utils/states';
import { saveStatus } from '../utils/states';
import states, { saveStatus } from '../utils/states';
import { isMediaFirstInstance } from '../utils/store-utils';
import useTitle from '../utils/useTitle';
@ -140,6 +139,26 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
const reachLimit = hashtags.length >= TOTAL_TAGS_LIMIT;
const [featuredUIState, setFeaturedUIState] = useState('default');
const [featuredTags, setFeaturedTags] = useState([]);
const [isFeaturedTag, setIsFeaturedTag] = useState(false);
useEffect(() => {
if (!authenticated) return;
(async () => {
try {
const featuredTags = await masto.v1.featuredTags.list();
setFeaturedTags(featuredTags);
setIsFeaturedTag(
featuredTags.some(
(tag) => tag.name.toLowerCase() === hashtag.toLowerCase(),
),
);
} catch (e) {
console.error(e);
}
})();
}, []);
return (
<Timeline
key={instance + hashtagTitle}
@ -147,7 +166,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
titleComponent={
!!instance && (
<h1 class="header-double-lines">
<b>{hashtagTitle}</b>
<b dir="auto">{hashtagTitle}</b>
<div>{instance}</div>
</h1>
)
@ -233,6 +252,69 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
</>
)}
</MenuConfirm>
<MenuItem
type="checkbox"
checked={isFeaturedTag}
disabled={featuredUIState === 'loading' || !authenticated}
onClick={() => {
setFeaturedUIState('loading');
if (isFeaturedTag) {
const featuredTagID = featuredTags.find(
(tag) => tag.name.toLowerCase() === hashtag.toLowerCase(),
).id;
if (featuredTagID) {
masto.v1.featuredTags
.$select(featuredTagID)
.remove()
.then(() => {
setIsFeaturedTag(false);
showToast('Unfeatured on profile');
setFeaturedTags(
featuredTags.filter(
(tag) => tag.id !== featuredTagID,
),
);
})
.catch((e) => {
console.error(e);
})
.finally(() => {
setFeaturedUIState('default');
});
} else {
showToast('Unable to unfeature on profile');
}
} else {
masto.v1.featuredTags
.create({
name: hashtag,
})
.then((value) => {
setIsFeaturedTag(true);
showToast('Featured on profile');
setFeaturedTags(featuredTags.concat(value));
})
.catch((e) => {
console.error(e);
})
.finally(() => {
setFeaturedUIState('default');
});
}
}}
>
{isFeaturedTag ? (
<>
<Icon icon="check-circle" />
<span>Featured on profile</span>
</>
) : (
<>
<Icon icon="check-circle" />
<span>Feature on profile</span>
</>
)}
</MenuItem>
<MenuDivider />
</>
)}
@ -297,6 +379,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
// no spaces, no hashtags
pattern="[^#][^\s#]+[^#]"
disabled={reachLimit}
dir="auto"
/>
</form>
)}
@ -320,7 +403,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
}}
>
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
<span>
<span class="bidi-isolate">
<span class="more-insignificant">#</span>
{t}
</span>
@ -366,7 +449,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
}
}}
>
<Icon icon="shortcut" /> <span>Add to Shorcuts</span>
<Icon icon="shortcut" /> <span>Add to Shortcuts</span>
</MenuItem>
<MenuItem
onClick={() => {

View file

@ -12,11 +12,15 @@ import Loader from '../components/loader';
import Notification from '../components/notification';
import { api } from '../utils/api';
import db from '../utils/db';
import groupNotifications from '../utils/group-notifications';
import { massageNotifications2 } from '../utils/group-notifications';
import states, { saveStatus } from '../utils/states';
import { getCurrentAccountNS } from '../utils/store-utils';
import Following from './following';
import {
getGroupedNotifications,
mastoFetchNotifications,
} from './notifications';
function Home() {
const snapStates = useSnapshot(states);
@ -84,20 +88,17 @@ function NotificationsLink() {
);
}
const NOTIFICATIONS_LIMIT = 80;
const NOTIFICATIONS_DISPLAY_LIMIT = 5;
function NotificationsMenu({ anchorRef, state, onClose }) {
const { masto, instance } = api();
const snapStates = useSnapshot(states);
const [uiState, setUIState] = useState('default');
const notificationsIterator = masto.v1.notifications.list({
limit: NOTIFICATIONS_LIMIT,
});
const notificationsIterator = mastoFetchNotifications();
async function fetchNotifications() {
const allNotifications = await notificationsIterator.next();
const notifications = allNotifications.value;
const notifications = massageNotifications2(allNotifications.value);
if (notifications?.length) {
notifications.forEach((notification) => {
@ -106,16 +107,16 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
});
});
const groupedNotifications = groupNotifications(notifications);
const groupedNotifications = getGroupedNotifications(notifications);
states.notificationsLast = notifications[0];
states.notificationsLast = groupedNotifications[0];
states.notifications = groupedNotifications;
// Update last read marker
masto.v1.markers
.create({
notifications: {
lastReadId: notifications[0].id,
lastReadId: groupedNotifications[0].id,
},
})
.catch(() => {});
@ -151,8 +152,11 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
if (state === 'open') loadNotifications();
}, [state]);
const menuRef = useRef();
return (
<ControlledMenu
ref={menuRef}
menuClassName="notifications-menu"
state={state}
anchorRef={anchorRef}
@ -160,6 +164,11 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
portal={{
target: document.body,
}}
containerProps={{
onClick: () => {
menuRef.current?.closeMenu?.();
},
}}
overflow="auto"
viewScroll="close"
position="anchor"
@ -176,7 +185,7 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
.slice(0, NOTIFICATIONS_DISPLAY_LIMIT)
.map((notification) => (
<Notification
key={notification.id}
key={notification._ids || notification.id}
instance={instance}
notification={notification}
disableContextMenu

View file

@ -24,11 +24,13 @@ export default function HttpRoute() {
// Check if status returns 200
try {
const { instance, id } = statusObject;
const { masto } = api({ instance });
const status = await masto.v1.statuses.$select(id).fetch();
if (status) {
window.location.hash = statusURL + '?view=full';
return;
if (id) {
const { masto } = api({ instance });
const status = await masto.v1.statuses.$select(id).fetch();
if (status) {
window.location.hash = statusURL + '?view=full';
return;
}
}
} catch (e) {}

View file

@ -10,9 +10,9 @@ import AccountBlock from '../components/account-block';
import Icon from '../components/icon';
import Link from '../components/link';
import ListAddEdit from '../components/list-add-edit';
import Menu2 from '../components/menu2';
import MenuConfirm from '../components/menu-confirm';
import MenuLink from '../components/menu-link';
import Menu2 from '../components/menu2';
import Modal from '../components/modal';
import Timeline from '../components/timeline';
import { api } from '../utils/api';

View file

@ -30,14 +30,15 @@
#instances-suggestions {
margin: 0.2em 0 0;
padding: 0 0 0 1.2em;
padding: 0;
padding-inline-start: 1.2em;
list-style: none;
width: 90vw;
max-width: 40em;
overflow: auto;
white-space: nowrap;
mask-image: linear-gradient(
to right,
var(--to-forward),
transparent,
black 1.2em,
black calc(100% - 5em),

View file

@ -12,6 +12,7 @@ import instancesListURL from '../data/instances.json?url';
import { getAuthorizationURL, registerApplication } from '../utils/auth';
import store from '../utils/store';
import useTitle from '../utils/useTitle';
import { gtsDtth } from '../utils/dtth';
const { PHANPY_DEFAULT_INSTANCE: DEFAULT_INSTANCE } = import.meta.env;
@ -24,7 +25,7 @@ function Login() {
const instance = searchParams.get('instance');
const submit = searchParams.get('submit');
const [instanceText, setInstanceText] = useState(
instance || cachedInstanceURL?.toLowerCase() || '',
instance || cachedInstanceURL?.toLowerCase() || gtsDtth,
);
const [instancesList, setInstancesList] = useState([]);
@ -158,6 +159,7 @@ function Login() {
onInput={(e) => {
setInstanceText(e.target.value);
}}
dir="auto"
/>
{instancesSuggestions?.length > 0 ? (
<ul id="instances-suggestions">

View file

@ -185,7 +185,7 @@
.notification-group-statuses > li:before {
content: counter(index);
position: absolute;
left: 0;
inset-inline-start: 0;
font-size: 10px;
padding: 8px;
font-weight: bold;
@ -194,16 +194,19 @@
margin-top: -1px;
}
.notification-group-statuses > li:not(:last-child) .status-link {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.notification-group-statuses > li:not(:first-child) .status-link {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-start-start-radius: 0;
border-start-end-radius: 0;
}
#mentions-option {
float: right;
&:dir(rtl) {
float: left;
}
margin-top: 0.5em;
}
#mentions-option label {
@ -388,7 +391,7 @@
width: calc(100% - 16px);
}
.announcements > ul > li:last-child {
border-right: none;
border-inline-end: none;
}
.announcements .announcement-block {
padding: 16px;

View file

@ -20,8 +20,12 @@ import Notification from '../components/notification';
import Status from '../components/status';
import { api } from '../utils/api';
import enhanceContent from '../utils/enhance-content';
import groupNotifications from '../utils/group-notifications';
import groupNotifications, {
groupNotifications2,
massageNotifications2,
} from '../utils/group-notifications';
import handleContentLinks from '../utils/handle-content-links';
import mem from '../utils/mem';
import niceDateTime from '../utils/nice-date-time';
import { getRegistration } from '../utils/push-notifications';
import shortenNumber from '../utils/shorten-number';
@ -33,7 +37,8 @@ import usePageVisibility from '../utils/usePageVisibility';
import useScroll from '../utils/useScroll';
import useTitle from '../utils/useTitle';
const LIMIT = 80;
const NOTIFICATIONS_LIMIT = 80;
const NOTIFICATIONS_GROUPED_LIMIT = 20;
const emptySearchParams = new URLSearchParams();
const scrollIntoViewOptions = {
@ -42,6 +47,43 @@ const scrollIntoViewOptions = {
behavior: 'smooth',
};
const memSupportsGroupedNotifications = mem(
() => supports('@mastodon/grouped-notifications'),
{
maxAge: 1000 * 60 * 5, // 5 minutes
},
);
export function mastoFetchNotifications(opts = {}) {
const { masto } = api();
if (
states.settings.groupedNotificationsAlpha &&
memSupportsGroupedNotifications()
) {
// https://github.com/mastodon/mastodon/pull/29889
return masto.v2_alpha.notifications.list({
limit: NOTIFICATIONS_GROUPED_LIMIT,
...opts,
});
} else {
return masto.v1.notifications.list({
limit: NOTIFICATIONS_LIMIT,
...opts,
});
}
}
export function getGroupedNotifications(notifications) {
if (
states.settings.groupedNotificationsAlpha &&
memSupportsGroupedNotifications()
) {
return groupNotifications2(notifications);
} else {
return groupNotifications(notifications);
}
}
function Notifications({ columnMode }) {
useTitle('Notifications', '/notifications');
const { masto, instance } = api();
@ -67,8 +109,7 @@ function Notifications({ columnMode }) {
async function fetchNotifications(firstLoad) {
if (firstLoad || !notificationsIterator.current) {
// Reset iterator
notificationsIterator.current = masto.v1.notifications.list({
limit: LIMIT,
notificationsIterator.current = mastoFetchNotifications({
excludeTypes: ['follow_request'],
});
}
@ -80,7 +121,7 @@ function Notifications({ columnMode }) {
};
}
const allNotifications = await notificationsIterator.current.next();
const notifications = allNotifications.value;
const notifications = massageNotifications2(allNotifications.value);
if (notifications?.length) {
notifications.forEach((notification) => {
@ -115,17 +156,17 @@ function Notifications({ columnMode }) {
// console.log({ notifications });
const groupedNotifications = groupNotifications(notifications);
const groupedNotifications = getGroupedNotifications(notifications);
if (firstLoad) {
states.notificationsLast = notifications[0];
states.notificationsLast = groupedNotifications[0];
states.notifications = groupedNotifications;
// Update last read marker
masto.v1.markers
.create({
notifications: {
lastReadId: notifications[0].id,
lastReadId: groupedNotifications[0].id,
},
})
.catch(() => {});
@ -676,12 +717,12 @@ function Notifications({ columnMode }) {
hideTime: true,
});
return (
<Fragment key={notification.id}>
<Fragment key={notification._ids || notification.id}>
{differentDay && <h2 class="timeline-header">{heading}</h2>}
<Notification
instance={instance}
notification={notification}
key={notification.id}
key={notification._ids || notification.id}
/>
</Fragment>
);

View file

@ -8,8 +8,7 @@ import Menu2 from '../components/menu2';
import Timeline from '../components/timeline';
import { api } from '../utils/api';
import { filteredItems } from '../utils/filters';
import states from '../utils/states';
import { saveStatus } from '../utils/states';
import states, { saveStatus } from '../utils/states';
import supports from '../utils/supports';
import useTitle from '../utils/useTitle';

View file

@ -48,10 +48,10 @@
a {
.icon {
vertical-align: middle;
transition: transform 0.2s;
transition: margin 0.2s;
}
&:hover .icon {
transform: translateX(4px);
margin-inline-start: 4px;
}
}
}
@ -101,9 +101,8 @@ ul.link-list.hashtag-list li a {
}
.search-popover {
position: absolute;
left: 8px;
inset-inline-start: 8px;
max-width: calc(100% - 16px);
/* right: 8px; */
background-color: var(--bg-color);
border: 1px solid var(--outline-color);
box-shadow: 0 4px 24px var(--drop-shadow-color);
@ -118,7 +117,8 @@ ul.link-list.hashtag-list li a {
}
.search-popover-item {
text-decoration: none;
padding: 8px 16px 8px 8px;
padding: 8px;
padding-inline-end: 16px;
display: flex;
gap: 8px;
align-items: center;
@ -132,10 +132,15 @@ ul.link-list.hashtag-list li a {
}
.search-popover-item:is(:focus, .focus) {
box-shadow: inset 4px 0 0 0 var(--button-bg-color);
:dir(rtl) & {
box-shadow: inset -4px 0 0 0 var(--button-bg-color);
}
}
.search-popover-item :is(mark, q) {
color: var(--text-color);
background-color: var(--link-bg-color);
unicode-bidi: isolate;
direction: initial;
}
.search-popover-item:is(:hover, :focus, .focus) :is(mark, q) {
background-color: var(--link-bg-color);

View file

@ -36,12 +36,12 @@
border-bottom: var(--hairline-width) solid var(--outline-color);
}
#settings-container section > ul > li > div:last-child {
text-align: right;
text-align: end;
}
#settings-container section > ul > li .sub-section {
text-align: left !important;
text-align: start !important;
margin-top: 8px;
margin-left: 24px;
margin-inline-start: 24px;
}
#settings-container section > ul > li .sub-section p {
margin-block: 0.5em;
@ -121,11 +121,11 @@
grid-template-rows: 1fr 1fr;
> span:first-child {
text-align: left;
text-align: start;
}
> span:last-child {
text-align: right;
text-align: end;
}
}
}

View file

@ -21,6 +21,7 @@ import {
import showToast from '../utils/show-toast';
import states from '../utils/states';
import store from '../utils/store';
import supports from '../utils/supports';
const DEFAULT_TEXT_SIZE = 16;
const TEXT_SIZES = [14, 15, 16, 17, 18, 19, 20];
@ -496,6 +497,27 @@ function Settings({ onClose }) {
</div>
</li>
)}
{authenticated && supports('@mastodon/grouped-notifications') && (
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.groupedNotificationsAlpha}
onChange={(e) => {
states.settings.groupedNotificationsAlpha =
e.target.checked;
}}
/>{' '}
Server-side grouped notifications
</label>
<div class="sub-section insignificant">
<small>
Alpha-stage feature. Potentially improved grouping window
but basic grouping logic.
</small>
</div>
</li>
)}
{authenticated && (
<li>
<label>
@ -603,14 +625,18 @@ function Settings({ onClose }) {
}}
>
@phanpy
</a>
</a> (
<a href="https://git.dtth.ch/nki/phanpy" target="_blank">
DTTH Fork
</a>
)
<br />
<a
href="https://github.com/cheeaun/phanpy"
target="_blank"
rel="noopener noreferrer"
>
Built
Original
</a>{' '}
by{' '}
<a
@ -665,10 +691,10 @@ function Settings({ onClose }) {
type="text"
class="version-string"
readOnly
size="18" // Manually calculated here
size={10 /* build time */ + (1+8) /* commit hash */ + '-dtth'.length}
value={`${__BUILD_TIME__.slice(0, 10).replace(/-/g, '.')}${
__COMMIT_HASH__ ? `.${__COMMIT_HASH__}` : ''
}`}
__COMMIT_HASH__ ? `.${__COMMIT_HASH__.slice(0, 8)}` : ''
}-dtth`}
onClick={(e) => {
e.target.select();
// Copy to clipboard
@ -685,7 +711,7 @@ function Settings({ onClose }) {
<span class="ib insignificant">
(
<a
href={`https://github.com/cheeaun/phanpy/commit/${__COMMIT_HASH__}`}
href={`https://git.dtth.ch/nki/phanpy/commit/${__COMMIT_HASH__}`}
target="_blank"
rel="noopener noreferrer"
>

View file

@ -11,7 +11,7 @@
align-self: stretch;
}
header h1 .deck-back {
margin-left: -16px;
margin-inline-start: -16px;
}
.button-refresh .icon {
@ -39,7 +39,7 @@
font-size: 70% !important;
& > .avatar ~ .avatar {
margin-left: -4px;
margin-inline-start: -4px;
}
}
.ancestors-indicator:not([hidden]) {

View file

@ -1370,6 +1370,8 @@ function SubComments({
const detailsRef = useRef();
useLayoutEffect(() => {
function handleScroll(e) {
// NOTE: this scrollLeft works for RTL too
// Browsers do the magic for us
e.target.dataset.scrollLeft = e.target.scrollLeft;
}
detailsRef.current?.addEventListener('scroll', handleScroll, {

View file

@ -19,8 +19,7 @@ import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
import { filteredItems } from '../utils/filters';
import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number';
import states from '../utils/states';
import { saveStatus } from '../utils/states';
import states, { saveStatus } from '../utils/states';
import supports from '../utils/supports';
import useTitle from '../utils/useTitle';
@ -72,6 +71,8 @@ function Trending({ columnMode, ...props }) {
// const navigate = useNavigate();
const latestItem = useRef();
const sameCurrentInstance = instance === currentInstance;
const [hashtags, setHashtags] = useState([]);
const [links, setLinks] = useState([]);
const trendIterator = useRef();
@ -137,7 +138,8 @@ function Trending({ columnMode, ...props }) {
const [currentLink, setCurrentLink] = useState(null);
const hasCurrentLink = !!currentLink;
const currentLinkRef = useRef();
const supportsTrendingLinkPosts = supports('@mastodon/trending-hashtags');
const supportsTrendingLinkPosts =
sameCurrentInstance && supports('@mastodon/trending-hashtags');
useEffect(() => {
if (currentLink && currentLinkRef.current) {
@ -207,7 +209,7 @@ function Trending({ columnMode, ...props }) {
const total = history.reduce((acc, cur) => acc + +cur.uses, 0);
return (
<Link to={`/${instance}/t/${name}`} key={name}>
<span>
<span dir="auto">
<span class="more-insignificant">#</span>
{name}
</span>

View file

@ -140,7 +140,7 @@
height: auto;
max-height: none;
position: fixed;
left: 0;
inset-inline-start: 0;
top: 0;
bottom: 0;
width: 50%;
@ -153,8 +153,9 @@
}
#why-container {
padding: 32px 32px 32px 8px;
margin-left: 50%;
padding: 32px;
padding-inline-start: 8px;
margin-inline-start: 50%;
/* overflow: auto;
mask-image: linear-gradient(to top, transparent 16px, black 64px); */

View file

@ -104,6 +104,7 @@ function Welcome() {
</a>
.
</p>
<p class="desc">A minimalistic opinionated DTTHDon web client.</p>
</div>
<div id="why-container">
<div class="sections">
@ -162,6 +163,38 @@ function Welcome() {
</section>
</div>
</div>
<footer>
<hr />
<p>
<a href="https://git.dtth.ch/nki/phanpy" target="_blank">
DTTHDon Fork
</a>
</p>
<p>
<a href="https://github.com/cheeaun/phanpy" target="_blank">
Original built
</a>{' '}
by{' '}
<a
href="https://mastodon.social/@cheeaun"
target="_blank"
onClick={(e) => {
e.preventDefault();
states.showAccount = 'cheeaun@mastodon.social';
}}
>
@cheeaun
</a>
.{' '}
<a
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
target="_blank"
>
Privacy Policy
</a>
.
</p>
</footer>
</main>
);
}

13
src/utils/dtth.js Normal file
View file

@ -0,0 +1,13 @@
export function accountsIsDtth(account) {
return (
account.info &&
typeof account.info.url === 'string' &&
account.info.url.startsWith(gtsDtth)
);
}
/** URL to DTTHDon */
export const gtsDtth = 'https://gts.dtth.ch';
/** URL to DTTHDon settings */
export const gtsDtthSettings = 'https://gts.dtth.ch/settings';

View file

@ -28,7 +28,118 @@ export function fixNotifications(notifications) {
});
}
function groupNotifications(notifications) {
export function massageNotifications2(notifications) {
if (notifications?.notificationGroups) {
const {
accounts = [],
notificationGroups = [],
statuses = [],
} = notifications;
return notificationGroups.map((group) => {
const { sampleAccountIds, statusId } = group;
const sampleAccounts =
sampleAccountIds?.map((id) => accounts.find((a) => a.id === id)) || [];
const status = statuses?.find((s) => s.id === statusId) || null;
return {
...group,
sampleAccounts,
status,
};
});
}
return notifications;
}
export function groupNotifications2(groupNotifications) {
// Make grouped notifications to look like faux grouped notifications
const newGroupNotifications = groupNotifications.map((gn) => {
const {
latestPageNotificationAt,
mostRecentNotificationId,
sampleAccounts,
notificationsCount,
} = gn;
return {
id: '' + mostRecentNotificationId,
createdAt: latestPageNotificationAt,
account: sampleAccounts[0],
...gn,
};
});
// DISABLED FOR NOW.
// Merge favourited and reblogged of same status into a single notification
// - new type: "favourite+reblog"
// - sum numbers for `notificationsCount` and `sampleAccounts`
// const mappedNotifications = {};
// const newNewGroupNotifications = [];
// for (let i = 0; i < newGroupNotifications.length; i++) {
// const gn = newGroupNotifications[i];
// const { type, status, createdAt, notificationsCount, sampleAccounts } = gn;
// const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
// let virtualType = type;
// if (type === 'favourite' || type === 'reblog') {
// virtualType = 'favourite+reblog';
// }
// const key = `${status?.id}-${virtualType}-${date}`;
// const mappedNotification = mappedNotifications[key];
// if (mappedNotification) {
// const accountIDs = mappedNotification.sampleAccounts.map((a) => a.id);
// sampleAccounts.forEach((a) => {
// if (!accountIDs.includes(a.id)) {
// mappedNotification.sampleAccounts.push(a);
// }
// });
// mappedNotification.notificationsCount = Math.max(
// mappedNotification.notificationsCount,
// notificationsCount,
// mappedNotification.sampleAccounts.length,
// );
// } else {
// mappedNotifications[key] = {
// ...gn,
// type: virtualType,
// };
// newNewGroupNotifications.push(mappedNotifications[key]);
// }
// }
// 2nd pass.
// - Group 1 account favourte/reblog multiple posts
// - _statuses: [status, status, ...]
const notificationsMap2 = {};
const newGroupNotifications2 = [];
for (let i = 0; i < newGroupNotifications.length; i++) {
const gn = newGroupNotifications[i];
const { type, account, _accounts, sampleAccounts, createdAt } = gn;
const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
const hasOneAccount =
sampleAccounts?.length === 1 || _accounts?.length === 1;
if ((type === 'favourite' || type === 'reblog') && hasOneAccount) {
const key = `${account?.id}-${type}-${date}`;
const mappedNotification = notificationsMap2[key];
if (mappedNotification) {
mappedNotification._statuses.push(gn.status);
mappedNotification._ids += `-${gn.id}`;
} else {
let n = (notificationsMap2[key] = {
...gn,
type,
_ids: gn.id,
_statuses: [gn.status],
});
newGroupNotifications2.push(n);
}
} else {
newGroupNotifications2.push(gn);
}
}
return newGroupNotifications2;
}
export default function groupNotifications(notifications) {
// Filter out invalid notifications
notifications = fixNotifications(notifications);
@ -56,17 +167,18 @@ function groupNotifications(notifications) {
if (mappedAccount) {
mappedAccount._types.push(type);
mappedAccount._types.sort().reverse();
mappedNotification.id += `-${id}`;
mappedNotification._ids += `-${id}`;
} else {
account._types = [type];
mappedNotification._accounts.push(account);
mappedNotification.id += `-${id}`;
mappedNotification._ids += `-${id}`;
}
} else {
if (account) account._types = [type];
let n = (notificationsMap[key] = {
...notification,
type: virtualType,
_ids: id,
_accounts: account ? [account] : [],
});
cleanNotifications[j++] = n;
@ -89,11 +201,12 @@ function groupNotifications(notifications) {
const mappedNotification = notificationsMap2[key];
if (mappedNotification) {
mappedNotification._statuses.push(notification.status);
mappedNotification.id += `-${id}`;
mappedNotification._ids += `-${id}`;
} else {
let n = (notificationsMap2[key] = {
...notification,
type,
_ids: id,
_statuses: [notification.status],
});
cleanNotifications2[j++] = n;
@ -108,5 +221,3 @@ function groupNotifications(notifications) {
// return cleanNotifications;
return cleanNotifications2;
}
export default groupNotifications;

26
src/utils/is-rtl.js Normal file
View file

@ -0,0 +1,26 @@
let IS_RTL = false;
// Use MutationObserver to detect RTL
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
const { value } = mutation.target;
if (value === 'rtl') {
IS_RTL = true;
} else {
IS_RTL = false;
}
// Fire custom event 'dirchange' on document
// document.dispatchEvent(new Event('dirchange'));
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['dir'],
});
export default function isRTL() {
return IS_RTL;
// return document.documentElement.dir === 'rtl';
}

View file

@ -1,12 +1,14 @@
export default function isMastodonLinkMaybe(url) {
try {
const { pathname, hash } = URL.parse(url);
const { pathname, hash, hostname } = URL.parse(url);
return (
/^\/.*\/\d+$/i.test(pathname) ||
/^\/(@[^/]+|users\/[^/]+)\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Firefish
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) || // Pleroma
/^\/@[^/]+\/post\/[a-z0-9]+$/i.test(pathname) || // Threads
/^\/@[^/]+\/[a-z0-9]+[a-z0-9\-]+[a-z0-9]+$/i.test(pathname) || // Hollo
(hostname === 'fed.brid.gy' && pathname.startsWith('/r/http')) || // Bridgy Fed
/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣
);
} catch (e) {

View file

@ -1,6 +1,6 @@
import mem from './mem';
const IntlDN = new Intl.DisplayNames(navigator.languages, {
const IntlDN = new Intl.DisplayNames(undefined, {
type: 'language',
});

View file

@ -70,6 +70,7 @@ const states = proxy({
mediaAltGenerator: false,
composerGIFPicker: false,
cloakMode: false,
groupedNotificationsAlpha: false,
},
});
@ -104,6 +105,8 @@ export function initStates() {
states.settings.composerGIFPicker =
store.account.get('settings-composerGIFPicker') ?? false;
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
states.settings.groupedNotificationsAlpha =
store.account.get('settings-groupedNotificationsAlpha') ?? false;
}
subscribeKey(states, 'notificationsLast', (v) => {
@ -153,6 +156,9 @@ subscribe(states, (changes) => {
if (path.join('.') === 'settings.cloakMode') {
store.account.set('settings-cloakMode', !!value);
}
if (path.join('.') === 'settings.groupedNotificationsAlpha') {
store.account.set('settings-groupedNotificationsAlpha', !!value);
}
}
});
@ -295,6 +301,16 @@ export function unfurlStatus(status, instance) {
unfurlMastodonLink(currentInstance, a.href).then((result) => {
if (!result) return;
if (!sKey) return;
if (result?.id === status.id) {
// Unfurled post is the post itself???
// Scenario:
// 1. Post with [URL]
// 2. Unfurl [URL], API returns the same post that contains [URL]
// 3. 💥 Recursive quote posts 💥
// Note: Mastodon search doesn't return posts that contains [URL], it's actually used to *resolve* the URL
// But some non-Mastodon servers, their search API will eventually search posts that contains [URL] and return them
return;
}
if (!Array.isArray(states.statusQuotes[sKey])) {
states.statusQuotes[sKey] = [];
}

View file

@ -227,7 +227,6 @@ export function groupContext(items, instance) {
const replyToStatus = await fetchStatus(inReplyToId, masto);
saveStatus(replyToStatus, instance, {
skipThreading: true,
skipUnfurling: true,
});
states.statusReply[sKey] = {
id: replyToStatus.id,
@ -253,7 +252,6 @@ export function groupContext(items, instance) {
for (const replyToStatus of replyToStatuses) {
saveStatus(replyToStatus, instance, {
skipThreading: true,
skipUnfurling: true,
});
const sKey = inReplyToIds.find(
({ inReplyToId }) => inReplyToId === replyToStatus.id,

View file

@ -0,0 +1,28 @@
import { useLayoutEffect, useState } from 'preact/hooks';
export default function useWindowSize() {
const [size, setSize] = useState({
width: null,
height: null,
});
useLayoutEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
handleResize();
window.addEventListener('resize', handleResize, {
passive: true,
});
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return size;
}

View file

@ -1,7 +1,8 @@
import preact from '@preact/preset-vite';
import { execSync } from 'child_process';
import fs from 'fs';
import { resolve } from 'path';
import preact from '@preact/preset-vite';
import { uid } from 'uid/single';
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
import generateFile from 'vite-plugin-generate-file';
@ -24,8 +25,12 @@ try {
} catch (error) {
// If error, means git is not installed or not a git repo (could be downloaded instead of git cloned)
// Fallback to random hash which should be different on every build run 🤞
commitHash = uid();
fakeCommitHash = true;
if (process.env.PHANPY_COMMIT_HASH) {
commitHash = process.env.PHANPY_COMMIT_HASH;
} else {
commitHash = uid();
fakeCommitHash = true;
}
}
const rollbarCode = fs.readFileSync(
@ -51,7 +56,11 @@ export default defineConfig({
preprocessorMaxWorkers: 1,
},
plugins: [
preact(),
preact({
// Force use Babel instead of ESBuild due to this change: https://github.com/preactjs/preset-vite/pull/114
// Else, a bug will happen with importing variables from import.meta.env
babel: {},
}),
splitVendorChunkPlugin(),
removeConsole({
includes: ['log', 'debug', 'info', 'warn', 'error'],