Merge pull request #472 from cheeaun/main

Update from main
This commit is contained in:
Chee Aun 2024-03-28 20:55:24 +08:00 committed by GitHub
commit ecd308ce39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 309 additions and 118 deletions

View file

@ -295,7 +295,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
video,
img,
audio {
min-height: 88px; /* for extreme dimensions */
min-height: var(--pointer-min-dimension); /* for extreme dimensions */
}
}
}

View file

@ -15,7 +15,8 @@ body.cloak,
.account-block,
.catchup-filters .filter-author *,
.post-peek-html *,
.post-peek-content > * {
.post-peek-content > *,
.request-notifications-account * {
text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
@ -51,7 +52,8 @@ body.cloak,
.cloak {
.media-container figcaption,
.media-container figcaption > *,
.catchup-filters .filter-author * {
.catchup-filters .filter-author *,
.request-notifications-account * {
color: var(--text-color) !important;
}
}

View file

@ -831,3 +831,58 @@
}
}
}
.handle-info {
.handle-handle {
display: inline-block;
margin-block: 5px;
b {
font-weight: 600;
padding: 2px 4px;
border-radius: 4px;
display: inline-block;
box-shadow: 0 0 0 5px var(--bg-blur-color);
&.handle-username {
color: var(--orange-fg-color);
background-color: var(--orange-bg-color);
}
&.handle-server {
color: var(--purple-fg-color);
background-color: var(--purple-bg-color);
}
}
}
.handle-at {
display: inline-block;
margin-inline: -3px;
position: relative;
z-index: 1;
}
.handle-legend {
margin-top: 0.25em;
}
.handle-legend-icon {
overflow: hidden;
display: inline-block;
width: 14px;
height: 14px;
border: 4px solid transparent;
border-radius: 8px;
background-clip: padding-box;
&.username {
background-color: var(--orange-fg-color);
border-color: var(--orange-bg-color);
}
&.server {
background-color: var(--purple-fg-color);
border-color: var(--purple-bg-color);
}
}
}

View file

@ -541,13 +541,55 @@ function AccountInfo({
/>
)}
<header>
<AccountBlock
account={info}
instance={instance}
avatarSize="xxxl"
external={standalone}
internal={!standalone}
/>
{standalone ? (
<Menu2
shift={
window.matchMedia('(min-width: calc(40em))').matches
? 114
: 64
}
menuButton={
<div>
<AccountBlock
account={info}
instance={instance}
avatarSize="xxxl"
onClick={() => {}}
/>
</div>
}
>
<div class="szh-menu__header">
<AccountHandleInfo acct={acct} instance={instance} />
</div>
<MenuItem
onClick={() => {
const handle = `@${acct}`;
try {
navigator.clipboard.writeText(handle);
showToast('Handle copied');
} catch (e) {
console.error(e);
showToast('Unable to copy handle');
}
}}
>
<Icon icon="link" />
<span>Copy handle</span>
</MenuItem>
<MenuItem href={url} target="_blank">
<Icon icon="external" />
<span>Go to original profile page</span>
</MenuItem>
</Menu2>
) : (
<AccountBlock
account={info}
instance={instance}
avatarSize="xxxl"
internal
/>
)}
</header>
<div class="faux-header-bg" aria-hidden="true" />
<main>
@ -2000,4 +2042,27 @@ function FieldsAttributesRow({ name, value, disabled, index: i }) {
);
}
function AccountHandleInfo({ acct, instance }) {
// acct = username or username@server
let [username, server] = acct.split('@');
if (!server) server = instance;
return (
<div class="handle-info">
<span class="handle-handle">
<b class="handle-username">{username}</b>
<span class="handle-at">@</span>
<b class="handle-server">{server}</b>
</span>
<div class="handle-legend">
<span class="ib">
<span class="handle-legend-icon username" /> username
</span>{' '}
<span class="ib">
<span class="handle-legend-icon server" /> server domain name
</span>
</div>
</div>
);
}
export default AccountInfo;

View file

@ -1,6 +1,7 @@
import { useHotkeys } from 'react-hotkeys-hook';
import openCompose from '../utils/open-compose';
import openOSK from '../utils/open-osk';
import states from '../utils/states';
import Icon from './icon';
@ -14,6 +15,7 @@ export default function ComposeButton() {
states.showCompose = true;
}
} else {
openOSK();
states.showCompose = true;
}
}

View file

@ -299,7 +299,7 @@ function Compose({
setVisibility(visibility);
setLanguage(language || presf.postingDefaultLanguage || DEFAULT_LANG);
setSensitive(sensitive);
setPoll(composablePoll);
if (composablePoll) setPoll(composablePoll);
setMediaAttachments(mediaAttachments);
setUIState('default');
} catch (e) {

View file

@ -2,11 +2,13 @@ import { shouldPolyfill } from '@formatjs/intl-segmenter/should-polyfill';
import { Suspense } from 'preact/compat';
import { useEffect, useState } from 'preact/hooks';
import Loader from './loader';
const supportsIntlSegmenter = !shouldPolyfill();
export default function IntlSegmenterSuspense({ children }) {
if (supportsIntlSegmenter) {
return <Suspense>{children}</Suspense>;
return <Suspense fallback={<Loader />}>{children}</Suspense>;
}
const [polyfillLoaded, setPolyfillLoaded] = useState(false);
@ -17,5 +19,9 @@ export default function IntlSegmenterSuspense({ children }) {
})();
}, []);
return polyfillLoaded && <Suspense>{children}</Suspense>;
return polyfillLoaded ? (
<Suspense fallback={<Loader />}>{children}</Suspense>
) : (
<Loader />
);
}

View file

@ -88,3 +88,7 @@
.sparkle-icon {
animation: sparkle-icon 0.3s ease-in-out infinite alternate;
}
.nav-submenu {
max-width: 14em;
}

View file

@ -27,9 +27,10 @@ function NavMenu(props) {
const [currentAccount, moreThanOneAccount] = useMemo(() => {
const accounts = store.local.getJSON('accounts') || [];
const acc = accounts.find(
(account) => account.info.id === store.session.get('currentAccount'),
);
const acc =
accounts.find(
(account) => account.info.id === store.session.get('currentAccount'),
) || accounts[0];
return [acc, accounts.length > 1];
}, []);
@ -147,7 +148,7 @@ function NavMenu(props) {
}}
{...props}
overflow="auto"
viewScroll="close"
// viewScroll="close"
position="anchor"
align="center"
boundingBoxPadding={boundingBoxPadding}
@ -209,6 +210,7 @@ function NavMenu(props) {
)}
{lists?.length > 0 ? (
<SubMenu
menuClassName="nav-submenu"
overflow="auto"
gap={-8}
label={
@ -243,6 +245,7 @@ function NavMenu(props) {
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
</MenuLink>
<SubMenu
menuClassName="nav-submenu"
overflow="auto"
gap={-8}
label={

View file

@ -2,7 +2,7 @@ import { Fragment } from 'preact';
import { memo } from 'preact/compat';
import shortenNumber from '../utils/shorten-number';
import states from '../utils/states';
import states, { statusKey } from '../utils/states';
import store from '../utils/store';
import useTruncated from '../utils/useTruncated';
@ -228,6 +228,7 @@ function Notification({
accounts: _accounts,
showReactions: type === 'favourite+reblog',
excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [],
postID: statusKey(actualStatusID, instance),
};
};

View file

@ -105,7 +105,7 @@
padding: 2px;
vertical-align: top;
text-transform: uppercase;
text-shadow: 0 1px var(--bg-color);
/* text-shadow: 0 1px var(--bg-color); */
&:hover {
color: var(--text-color);
@ -623,26 +623,26 @@
.spoiler-media-button
),
~ .card .meta-container {
/* filter: blur(5px) invert(0.5);
image-rendering: crisp-edges;
image-rendering: pixelated; */
opacity: 0.2;
text-decoration-thickness: 1.5em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
pointer-events: none;
user-select: none;
/* contain: layout; */
/* transform: scale(0.97);
transition: transform 0.1s ease-in-out; */
* {
text-decoration-color: inherit;
text-decoration-thickness: 1.5em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
}
}
~ *:not(
.media-container,
.card,
.media-figure-multiple,
.spoiler-media-button
),
~ .card .meta-container {
img {
filter: invert(0.5);
background-color: black;
@ -908,7 +908,7 @@
grid-auto-rows: 1fr;
gap: 2px;
/* height: 160px; */
min-height: 88px;
min-height: var(--pointer-min-dimension);
height: auto;
max-height: max(160px, 33vh);
}
@ -1037,9 +1037,9 @@
.status .media-container.media-eq1 .media {
display: inline-block;
max-width: 100% !important;
min-width: 88px;
min-width: var(--pointer-min-dimension);
/* width: auto; */
min-height: 88px;
min-height: var(--pointer-min-dimension);
/* --maxAspectHeight: max(160px, 33vh);
--aspectWidth: calc(--width / --height * var(--maxAspectHeight)); */
width: min(var(--aspectWidth), var(--width), 100%);
@ -1300,7 +1300,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
:is(.status, .media-post) .media-audio {
width: 100%;
height: 100%;
min-height: 88px;
min-height: var(--pointer-min-dimension);
background-image: radial-gradient(
circle at center center,
transparent,
@ -1696,6 +1696,7 @@ a.card:is(:hover, :focus):visited {
.poll-label input:is([type='radio'], [type='checkbox']) {
flex-shrink: 0;
margin: 3px;
min-height: 1em;
}
.poll-option-votes {
flex-shrink: 0;
@ -1719,6 +1720,7 @@ a.card:is(:hover, :focus):visited {
}
.poll-option-title {
text-shadow: 0 1px var(--bg-color);
line-height: 1.2;
}
.poll-option-title .icon {
vertical-align: middle;

View file

@ -1593,11 +1593,14 @@ function Status({
}`}
/>
) : (
<Icon
icon={visibilityIconsMap[visibility]}
alt={visibilityText[visibility]}
size="s"
/>
visibility !== 'public' &&
visibility !== 'direct' && (
<Icon
icon={visibilityIconsMap[visibility]}
alt={visibilityText[visibility]}
size="s"
/>
)
)}{' '}
<RelativeTime datetime={createdAtDate} format="micro" />
{!previewMode && !readOnly && (
@ -1648,11 +1651,15 @@ function Status({
// {StatusMenuItems}
// </Menu>
<span class="time">
<Icon
icon={visibilityIconsMap[visibility]}
alt={visibilityText[visibility]}
size="s"
/>{' '}
{visibility !== 'public' && visibility !== 'direct' && (
<>
<Icon
icon={visibilityIconsMap[visibility]}
alt={visibilityText[visibility]}
size="s"
/>{' '}
</>
)}
<RelativeTime datetime={createdAtDate} format="micro" />
</span>
))}

View file

@ -16,6 +16,12 @@
--blue-color: royalblue;
--purple-color: blueviolet;
--purple-fg-color: color-mix(
in srgb-linear,
var(--purple-color) 60%,
var(--text-color) 40%
);
--purple-bg-color: color-mix(in srgb, var(--purple-color) 10%, transparent);
--green-color: darkgreen;
--orange-color: darkorange;
--orange-light-bg-color: color-mix(
@ -23,6 +29,12 @@
var(--orange-color) 20%,
transparent
);
--orange-fg-color: color-mix(
in srgb-linear,
var(--orange-color) 60%,
var(--text-color) 40%
);
--orange-bg-color: color-mix(in srgb, var(--orange-color) 10%, transparent);
--red-color: orangered;
--red-text-color: color-mix(
in srgb-linear,
@ -96,6 +108,14 @@
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
--spring-timing-funtion: cubic-bezier(0.175, 0.885, 0.32, 1.275);
--pointer-min-dimension: 88px;
}
@media (pointer: fine) {
:root {
--pointer-min-dimension: 44px;
}
}
@media (min-resolution: 2dppx) {

View file

@ -1263,7 +1263,7 @@ function Catchup() {
authors[author].avatarStatic || authors[author].avatar
}
size="xxl"
alt={`${authors[author].displayName} (@${authors[author].username})`}
alt={`${authors[author].displayName} (@${authors[author].acct})`}
/>{' '}
<span class="count">{authorCounts[author]}</span>
<span class="username">{authors[author].username}</span>

View file

@ -180,6 +180,8 @@ function Filters() {
);
}
let _id = 1;
const incID = () => _id++;
function FiltersAddEdit({ filter, onClose }) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
@ -193,7 +195,12 @@ function FiltersAddEdit({ filter, onClose }) {
// Hacky way of handling removed keywords for both existing and new ones
const [removedKeywordIDs, setRemovedKeywordIDs] = useState([]);
const [removedNewKeywordIndices, setRemovedNewKeywordIndices] = useState([]);
const [removedKeyword_IDs, setRemovedKeyword_IDs] = useState([]);
const filteredEditKeywords = editKeywords.filter(
(k) =>
!removedKeywordIDs.includes(k.id) && !removedKeyword_IDs.includes(k._id),
);
return (
<div class="sheet" id="filters-add-edit-modal">
@ -335,16 +342,12 @@ function FiltersAddEdit({ filter, onClose }) {
</label>
</div>
<div class="filter-form-keywords" ref={keywordsRef}>
{editKeywords.length ? (
{filteredEditKeywords.length ? (
<ul class="filter-keywords">
{editKeywords.map((k, index) => {
const { id, keyword, wholeWord } = k;
const removed =
removedKeywordIDs.includes(id) ||
removedNewKeywordIndices.includes(index);
if (removed) return null;
{filteredEditKeywords.map((k) => {
const { id, keyword, wholeWord, _id } = k;
return (
<li key={`${index}-${id}`}>
<li key={`${id}-${_id}`}>
<input
type="hidden"
name="keyword_attributes[][id]"
@ -376,12 +379,9 @@ function FiltersAddEdit({ filter, onClose }) {
if (id) {
removedKeywordIDs.push(id);
setRemovedKeywordIDs([...removedKeywordIDs]);
} else {
// If no id, remove by index
removedNewKeywordIndices.push(index);
setRemovedNewKeywordIndices([
...removedNewKeywordIndices,
]);
} else if (_id) {
removedKeyword_IDs.push(_id);
setRemovedKeyword_IDs([...removedKeyword_IDs]);
}
}}
>
@ -405,6 +405,7 @@ function FiltersAddEdit({ filter, onClose }) {
setEditKeywords([
...editKeywords,
{
_id: incID(),
keyword: '',
wholeWord: true,
},
@ -421,10 +422,10 @@ function FiltersAddEdit({ filter, onClose }) {
>
Add keyword
</button>{' '}
{editKeywords?.length > 1 && (
{filteredEditKeywords?.length > 1 && (
<small class="insignificant">
{editKeywords.length} keyword
{editKeywords.length === 1 ? '' : 's'}
{filteredEditKeywords.length} keyword
{filteredEditKeywords.length === 1 ? '' : 's'}
</small>
)}
</footer>

View file

@ -532,66 +532,72 @@ function Notifications({ columnMode }) {
)}
{supportsFilteredNotifications &&
notificationsPolicy?.summary?.pendingRequestsCount > 0 && (
<div class="filtered-notifications">
<details
onToggle={async (e) => {
const { open } = e.target;
if (open) {
const requests = await fetchNotificationsRequest();
setNotificationsRequests(requests);
console.log({ open, requests });
}
}}
>
<summary>
Filtered notifications from{' '}
{notificationsPolicy.summary.pendingRequestsCount} people
</summary>
{!notificationsRequests ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
notificationsRequests?.length > 0 && (
<ul>
{notificationsRequests.map((request) => (
<li key={request.id}>
<div class="request-notifcations">
{!request.lastStatus?.id && (
<AccountBlock
useAvatarStatic
showStats
account={request.account}
/>
)}
{request.lastStatus?.id && (
<div class="last-post">
<Link
class="status-link"
to={`/${instance}/s/${request.lastStatus.id}`}
>
<Status
status={request.lastStatus}
size="s"
readOnly
<div class="shazam-container">
<div class="shazam-container-inner">
<div class="filtered-notifications">
<details
onToggle={async (e) => {
const { open } = e.target;
if (open) {
const requests = await fetchNotificationsRequest();
setNotificationsRequests(requests);
console.log({ open, requests });
}
}}
>
<summary>
Filtered notifications from{' '}
{notificationsPolicy.summary.pendingRequestsCount} people
</summary>
{!notificationsRequests ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
notificationsRequests?.length > 0 && (
<ul>
{notificationsRequests.map((request) => (
<li key={request.id}>
<div class="request-notifcations">
{!request.lastStatus?.id && (
<AccountBlock
useAvatarStatic
showStats
account={request.account}
/>
</Link>
)}
{request.lastStatus?.id && (
<div class="last-post">
<Link
class="status-link"
to={`/${instance}/s/${request.lastStatus.id}`}
>
<Status
status={request.lastStatus}
size="s"
readOnly
/>
</Link>
</div>
)}
<NotificationRequestModalButton
request={request}
/>
</div>
)}
<NotificationRequestModalButton request={request} />
</div>
<NotificationRequestButtons
request={request}
onChange={() => {
loadNotifications(true);
}}
/>
</li>
))}
</ul>
)
)}
</details>
<NotificationRequestButtons
request={request}
onChange={() => {
loadNotifications(true);
}}
/>
</li>
))}
</ul>
)
)}
</details>
</div>
</div>
</div>
)}
<div id="mentions-option">

16
src/utils/open-osk.jsx Normal file
View file

@ -0,0 +1,16 @@
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755
export default function openOSK() {
if (isSafari) {
const fauxEl = document.createElement('input');
fauxEl.style.position = 'absolute';
fauxEl.style.top = '0';
fauxEl.style.left = '0';
fauxEl.style.opacity = '0';
document.body.appendChild(fauxEl);
fauxEl.focus();
setTimeout(() => {
document.body.removeChild(fauxEl);
}, 500);
}
}

View file

@ -2,6 +2,7 @@ import store from './store';
export function getAccount(id) {
const accounts = store.local.getJSON('accounts') || [];
if (!id) return accounts[0];
return accounts.find((a) => a.info.id === id) || accounts[0];
}