Compare commits
68 commits
97978f4a65
...
c72bd47bbd
Author | SHA1 | Date | |
---|---|---|---|
Natsu Kagami | c72bd47bbd | ||
Natsu Kagami | bc0f856d72 | ||
Natsu Kagami | 0e4dd6ee39 | ||
Natsu Kagami | 0fa57fc0aa | ||
Natsu Kagami | bb440c5d28 | ||
Natsu Kagami | 66078a1867 | ||
Natsu Kagami | 3ff14d942e | ||
Natsu Kagami | 109b919c6c | ||
Natsu Kagami | 28fb3e4102 | ||
9bf50615cb | |||
dfa1123ac3 | |||
a0f2eb7305 | |||
1bd9ceb4fc | |||
082409a09f | |||
225eaf4a2d | |||
60289cdb29 | |||
a1c419b675 | |||
89e8bdf77b | |||
b3681a93ee | |||
ad7193d067 | |||
f05e3012e3 | |||
2aff1dc1fd | |||
99ee6c3979 | |||
4ebfb544aa | |||
cf2461add5 | |||
4937c5f77e | |||
0febcacb93 | |||
818f58b460 | |||
57db8778a4 | |||
9806d8ae9d | |||
522a324b0d | |||
5be30e0c80 | |||
379ef7cc11 | |||
2d23b15c8d | |||
fa3a0e23cc | |||
631730f2f2 | |||
f1822d54af | |||
4c0bc62ad0 | |||
84b3106f50 | |||
a2b88f1cdd | |||
b88376569e | |||
00e2ba0b34 | |||
a0d75e7e83 | |||
4b2ec14dcd | |||
808c6262d8 | |||
44d440649f | |||
a2f7638257 | |||
57d6889826 | |||
2a91c005a1 | |||
418895e1c3 | |||
180a23f116 | |||
9ea7a1f4db | |||
f26dbeb79a | |||
f0872e79fb | |||
a72400febf | |||
cb9848fe8c | |||
c950a6552c | |||
95bf9e183e | |||
e6e884f1cb | |||
b6a25f5939 | |||
71823fbad2 | |||
046d3d323a | |||
f7024f7723 | |||
1b3938f3d2 | |||
5ab0ea1b59 | |||
09745e3078 | |||
87be0cad16 | |||
04588874c7 |
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -26,4 +26,8 @@ dist-ssr
|
|||
# Custom
|
||||
.env.dev
|
||||
phanpy-dist.zip
|
||||
phanpy-dist.tar.gz
|
||||
phanpy-dist.tar.gz
|
||||
|
||||
# Nix
|
||||
.direnv
|
||||
result
|
||||
|
|
12
.prettierrc
12
.prettierrc
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
61
flake.lock
Normal 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
60
flake.nix
Normal 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
1578
package-lock.json
generated
File diff suppressed because it is too large
Load diff
32
package.json
32
package.json
|
@ -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",
|
||||
|
|
39
public/sw.js
39
public/sw.js
|
@ -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:
|
||||
|
|
336
src/app.css
336
src/app.css
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]?.();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
line-clamp: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
unicode-bidi: isolate;
|
||||
direction: initial;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -120,7 +120,7 @@ function AccountBlock({
|
|||
)}
|
||||
</>
|
||||
)}{' '}
|
||||
<span class="account-block-acct">
|
||||
<span class="account-block-acct bidi-isolate">
|
||||
{acct2 ? '' : '@'}
|
||||
{acct1}
|
||||
<wbr />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
}
|
||||
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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
29
src/components/intersection-view.jsx
Normal file
29
src/components/intersection-view.jsx
Normal 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;
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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…</span></a>
|
||||
</FocusableItem>}
|
||||
{lists?.length > 0 ? (
|
||||
<SubMenu2
|
||||
menuClassName="nav-submenu"
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>`,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 && <>· </>}
|
||||
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import './index.css';
|
||||
|
||||
import './app.css';
|
||||
|
||||
import './polyfills';
|
||||
|
||||
import { render } from 'preact';
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import './index.css';
|
||||
|
||||
import './cloak-mode.css';
|
||||
|
||||
import './polyfills';
|
||||
|
||||
// Polyfill needed for Firefox < 122
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -1257,6 +1257,10 @@ function Catchup() {
|
|||
}
|
||||
onChange={() => {
|
||||
setSelectedFilterCategory(label);
|
||||
if (label === 'Boosts') {
|
||||
setSortBy('reblogsCount');
|
||||
setGroupBy(null);
|
||||
}
|
||||
// setSelectedAuthor(null);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -365,6 +365,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
|||
defaultValue={keyword}
|
||||
disabled={uiState === 'loading'}
|
||||
required
|
||||
dir="auto"
|
||||
/>
|
||||
<div class="filter-keyword-actions">
|
||||
<label>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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); */
|
||||
|
|
|
@ -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
13
src/utils/dtth.js
Normal 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';
|
|
@ -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
26
src/utils/is-rtl.js
Normal 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';
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import mem from './mem';
|
||||
|
||||
const IntlDN = new Intl.DisplayNames(navigator.languages, {
|
||||
const IntlDN = new Intl.DisplayNames(undefined, {
|
||||
type: 'language',
|
||||
});
|
||||
|
||||
|
|
|
@ -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] = [];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
28
src/utils/useWindowSize.js
Normal file
28
src/utils/useWindowSize.js
Normal 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;
|
||||
}
|
|
@ -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'],
|
||||
|
|
Loading…
Reference in a new issue