Porównaj commity

...

27 Commity

Autor SHA1 Wiadomość Data
Helge 3adfcd7ac5
Merge 99e5c1036d into 015ed5e7eb 2024-04-05 12:10:20 +02:00
Lim Chee Aun 015ed5e7eb Further expand usage of SubMenu2 2024-04-04 17:03:30 +08:00
Lim Chee Aun 2ad9706304 Further utilize lazy shazam 2024-04-04 14:34:28 +08:00
Lim Chee Aun 30382d088b Possible fix for menus again 2024-04-04 14:34:04 +08:00
Lim Chee Aun 80196f83ca Revert "Test if this fixes submenu not opening"
This reverts commit 49fa48bd28.
2024-04-04 14:29:46 +08:00
Lim Chee Aun 419ad34250 Revert "Test another fix for submenus not opening"
This reverts commit a7cc0785f9.
2024-04-04 14:29:35 +08:00
Lim Chee Aun ed0d714cf2 Just a little spacing fix 2024-04-03 22:51:29 +08:00
Lim Chee Aun 708976a9e9 Anything Intl always need to extract out
and memoized
2024-04-03 19:48:18 +08:00
Lim Chee Aun d77ba19308 Handle another kind of emojiReaction response
Can't everyone just standardize the responses?
2024-04-03 17:58:37 +08:00
Lim Chee Aun b10e22a9a2 Better fallbacks 2024-04-03 17:57:15 +08:00
Lim Chee Aun 36d8b62e1e Height adjustments when switching between poll form and results 2024-04-03 16:14:59 +08:00
Lim Chee Aun 989e788d8e Slight delay is needed 2024-04-03 16:06:37 +08:00
Lim Chee Aun ebd9f05f69 Preload IntlSegmenter polyfill if needed 2024-04-03 14:33:53 +08:00
Lim Chee Aun 5246af4ae9 Undo lazy component experiment
Doesn't make much difference
2024-04-03 14:33:19 +08:00
Lim Chee Aun e6ba72f4c8 'Remove follower' menu item 2024-04-03 11:54:46 +08:00
Lim Chee Aun 960dff8b9e Make lazy shazam ignore top sticky header 2024-04-03 11:53:03 +08:00
Lim Chee Aun e3c25d25ee Add menus to view profile image and header 2024-04-03 09:29:23 +08:00
Lim Chee Aun 090320150a Select text too when pressing / 2024-04-03 09:28:59 +08:00
Lim Chee Aun 7100937e79 Higher gif picker sheet 2024-04-02 19:44:22 +08:00
Lim Chee Aun c18efef7b6 GIF picker 2024-04-02 17:51:48 +08:00
Lim Chee Aun ff336628f8 Fix media description not recognized if programmatically entered 2024-04-02 17:45:14 +08:00
Lim Chee Aun 28882d98d9 Add different UI state than default for start 2024-04-02 17:42:51 +08:00
Lim Chee Aun f6ad22e58f Fix bug: media attachments not updated when edited 2024-04-02 13:12:52 +08:00
Lim Chee Aun aa664e15f6 Convert all the punycodes
Surprising that this is still not built into browsers
2024-04-02 09:03:13 +08:00
Chee Aun f2f203c9d8
Merge pull request #478 from snail-coupe/doc/fix_build_example
Update README.md
2024-04-02 07:49:11 +08:00
snail-coupe ae0e4a0792
Update README.md
Build examples: PHANPY_APP_TITLE -> PHANPY_CLIENT_NAME
2024-04-01 23:26:15 +01:00
Helge 99e5c1036d Enable running using http
This means that by adding PHANPY_SCHEME=http to the file ".env"
phanpy will use http instead of https to connect to remote instances.

This is useful to test local versions of various Fediverse applications.
These can be created following the instructions on

https://funfedi.dev/quickstart/#running-an-application-from-the-fediverse-pasture
2024-03-09 17:31:36 +01:00
32 zmienionych plików z 741 dodań i 124 usunięć

Wyświetl plik

@ -138,7 +138,7 @@ Download or `git clone` this repository. Use `production` branch for *stable* re
Customization can be done by passing environment variables to the build command. Examples:
```bash
PHANPY_APP_TITLE="Phanpy Dev" \
PHANPY_CLIENT_NAME="Phanpy Dev" \
PHANPY_WEBSITE="https://dev.phanpy.social" \
npm run build
```
@ -179,6 +179,10 @@ Available variables:
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
- List of fallback instances hard-coded in `/.env`
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
- `PHANPY_GIPHY_API_KEY` (optional, no defaults):
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
- This is not self-hosted.
### Static site hosting

9
package-lock.json wygenerowano
Wyświetl plik

@ -30,6 +30,7 @@
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.20.1",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.0",
"react-intersection-observer": "~9.8.1",
"react-quick-pinch-zoom": "~5.1.0",
@ -7154,11 +7155,9 @@
"integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
"dev": true,
"license": "MIT",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}

Wyświetl plik

@ -32,6 +32,7 @@
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.20.1",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.0",
"react-intersection-observer": "~9.8.1",
"react-quick-pinch-zoom": "~5.1.0",

Wyświetl plik

@ -1,7 +1,6 @@
import './app.css';
import debounce from 'just-debounce-it';
import { lazy, Suspense } from 'preact/compat';
import {
useEffect,
useLayoutEffect,
@ -18,14 +17,14 @@ import ComposeButton from './components/compose-button';
import { ICONS } from './components/ICONS';
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
import Loader from './components/loader';
// import Modals from './components/modals';
import Modals from './components/modals';
import NotificationService from './components/notification-service';
import SearchCommand from './components/search-command';
import Shortcuts from './components/shortcuts';
import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses';
import Bookmarks from './pages/bookmarks';
// import Catchup from './pages/catchup';
import Catchup from './pages/catchup';
import Favourites from './pages/favourites';
import Filters from './pages/filters';
import FollowedHashtags from './pages/followed-hashtags';
@ -57,9 +56,6 @@ import store from './utils/store';
import { getCurrentAccount } from './utils/store-utils';
import './utils/toast-alert';
const Catchup = lazy(() => import('./pages/catchup'));
const Modals = lazy(() => import('./components/modals'));
window.__STATES__ = states;
window.__STATES_STATS__ = () => {
const keys = [
@ -387,9 +383,7 @@ function App() {
)}
{isLoggedIn && <ComposeButton />}
{isLoggedIn && <Shortcuts />}
<Suspense>
<Modals />
</Suspense>
<Modals />
{isLoggedIn && <NotificationService />}
<BackgroundService isLoggedIn={isLoggedIn} />
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
@ -466,14 +460,7 @@ function SecondaryRoutes({ isLoggedIn }) {
</Route>
<Route path="/fh" element={<FollowedHashtags />} />
<Route path="/ft" element={<Filters />} />
<Route
path="/catchup"
element={
<Suspense>
<Catchup />
</Suspense>
}
/>
<Route path="/catchup" element={<Catchup />} />
</>
)}
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />

Wyświetl plik

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 641 223">
<path fill="#aaa" d="M86 214c-9-1-17-4-24-8l-6-3-5-5-5-4-4-6-4-6-3-8-2-8v-27l2-9 3-9 4-6 4-6 5-5 5-5 7-3 6-4 7-2 7-2 12-1h12l7 1 8 2 7 4 7 3 5 5 5 4-10 10-10 9-4-3-10-5-5-1H88l-5 2-6 3-3 4-4 4-2 5-2 6v6l-1 7 1 7 2 7 3 5 2 4 4 3 4 3 5 2 6 2h9l10-1 5-2 6-3v-16H91v-27h59v54l-1 3-2 3-5 4-4 4-5 3-5 2-8 2-8 2-10 1H92l-6-1zm266-62V91h34v46h44V91h34v121h-34v-46h-44v46h-34v-61zm-182-1V90h34v121h-34v-60zm59-1V90h35l36 1 5 2c3 0 8 2 10 4l5 2 4 5 5 4 3 7 3 7 1 13v13l-4 6-3 7-4 4-5 5-5 2-5 3-6 2-5 1-18 1h-18v32h-34v-61zm67-2 3-2 2-4 2-5v-5l-2-4-2-4-3-2-3-3h-30v31h30l3-2zm226 39v-24l-8-12-18-28a1751 1751 0 0 0-20-31v-2h39l7 12 12 21 6 9 13-21 13-21h38v2l-41 61-7 10v48h-34v-24zM109 66l-4-1-5-5-5-4-1-5-3-9v-5l1-5c2-7 3-10 8-15l4-4 7-2 7-2h7l6 1 5 2 5 2 3 4 4 3 2 6 2 5v13l-2 5-2 6-4 4-3 3-5 2-4 2-9 1h-9l-5-2zm22-11 4-2 3-4 2-5V34l-2-4-2-4-3-2-4-3-5-1h-6l-4 2-5 2-2 4-3 5-1 3v4l1 5 2 5 2 2 5 3 4 2h10l4-2zM37 39V11h33l3 1 3 2 4 3 3 3 1 5 1 4v5l-1 4-3 4-3 5-4 1-3 2-11 1H49v16H37V39zm31 0 3-2 1-2 1-2v-4l-1-3-3-2-2-2H49v18h15l4-1zm107 25a512 512 0 0 0-19-53h14l4 14 6 19 1 4 1-1 7-19 5-17h9l6 19 7 18v-1l2-6 5-17 4-13h14v1l-4 12-16 41v2h-5l-5-1-6-15-6-15-1 1-3 7-6 15-2 8h-11l-1-3zm74-25V11h42v11h-29v2l-1 5v4h29v11h-28v11h2l15 1h13v11h-43V39zm55 0V11h33l5 3 5 2 2 4 2 5v10l-2 3-1 4-5 3-5 3 5 5 8 10 3 4h-14l-7-9-8-10h-9v19h-12V39zm33-3 2-3v-6l-3-3-2-3h-18v16h1v1h17l2-2zm26 3V11h42v11h-29l-1 6v5h29v11h-28v5l-1 5 1 1v1h30v11h-43V39zm54 0V11h17l18 1 4 2 5 3 2 4 3 4 2 6 1 6v5c-1 6-3 12-6 15l-3 4-5 3-5 2-17 1h-16V39zm33 14 5-5 2-3v-6l-1-6-1-3-1-3-4-3-3-2h-5l-6-1-3 1h-3v34h9l8-1 3-2zm50-14V11h34l5 2 4 2 2 3 2 3v9l-2 2-3 4-1 1 3 3 3 4 1 3 1 4-1 4-1 4-3 3-3 3-5 1-5 1h-31V39zm34 15 2-1v-6l-2-2-2-2h-20v13h20l2-2zm-3-22 4-2v-6l-2-1-2-2h-19v12h16l4-1zm42 24V45l-6-9-11-17-5-8h15l4 8 7 11 2 3 7-11 7-11h14l-11 16-11 17v23h-12V56z"/>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Wyświetl plik

@ -107,4 +107,5 @@ export const ICONS = {
quote: () => import('@iconify-icons/mingcute/quote-left-line'),
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'),
};

Wyświetl plik

@ -1,6 +1,6 @@
import './account-info.css';
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
import {
useCallback,
useEffect,
@ -9,6 +9,7 @@ import {
useRef,
useState,
} from 'preact/hooks';
import punycode from 'punycode';
import { api } from '../utils/api';
import enhanceContent from '../utils/enhance-content';
@ -32,7 +33,9 @@ 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 Modal from './modal';
import SubMenu2 from './submenu2';
import TranslationBlock from './translation-block';
const MUTE_DURATIONS = [
@ -228,7 +231,7 @@ function AccountInfo({
const accountInstance = useMemo(() => {
if (!url) return null;
const domain = new URL(url).hostname;
const domain = punycode.toUnicode(new URL(url).hostname);
return domain;
}, [url]);
@ -581,6 +584,15 @@ function AccountInfo({
<Icon icon="external" />
<span>Go to original profile page</span>
</MenuItem>
<MenuDivider />
<MenuLink href={info.avatar} target="_blank">
<Icon icon="user" />
<span>View profile image</span>
</MenuLink>
<MenuLink href={info.header} target="_blank">
<Icon icon="media" />
<span>View profile header</span>
</MenuLink>
</Menu2>
) : (
<AccountBlock
@ -659,6 +671,7 @@ function AccountInfo({
// states.showAccount = false;
setTimeout(() => {
states.showGenericAccounts = {
id: 'followers',
heading: 'Followers',
fetchAccounts: fetchFollowers,
instance,
@ -1272,7 +1285,7 @@ function RelatedActions({
<span>Unmute @{username}</span>
</MenuItem>
) : (
<SubMenu
<SubMenu2
menuClassName="menu-blur"
openTrigger="clickOnly"
direction="bottom"
@ -1326,7 +1339,44 @@ function RelatedActions({
</MenuItem>
))}
</div>
</SubMenu>
</SubMenu2>
)}
{followedBy && (
<MenuConfirm
subMenu
menuItemClassName="danger"
confirmLabel={
<>
<Icon icon="user-x" />
<span>Remove @{username} from followers?</span>
</>
}
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
const newRelationship = await currentMasto.v1.accounts
.$select(currentInfo?.id || id)
.removeFromFollowers();
console.log(
'removing from followers',
newRelationship,
);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(`@${username} removed from followers`);
states.reloadGenericAccounts.id = 'followers';
states.reloadGenericAccounts.counter++;
} catch (e) {
console.error(e);
setRelationshipUIState('error');
}
})();
}}
>
<Icon icon="user-x" />
<span>Remove follower</span>
</MenuConfirm>
)}
<MenuConfirm
subMenu
@ -1598,7 +1648,7 @@ function niceAccountURL(url) {
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
return (
<>
<span class="more-insignificant">{host}/</span>
<span class="more-insignificant">{punycode.toUnicode(host)}/</span>
<wbr />
<span>{path}</span>
</>

Wyświetl plik

@ -727,3 +727,165 @@
}
}
}
@keyframes gif-shake {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(5deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(-5deg);
}
100% {
transform: rotate(0deg);
}
}
.gif-picker-button {
span {
font-weight: bold;
font-size: 11.5px;
display: block;
}
&:is(:hover, :focus) {
span {
animation: gif-shake 0.3s 3;
}
}
}
#gif-picker-sheet {
height: 50vh;
form {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
input[type='search'] {
flex-grow: 1;
min-width: 0;
}
}
main {
overflow-x: auto;
overflow-y: hidden;
mask-image: linear-gradient(
to right,
transparent 2px,
black 16px,
black calc(100% - 16px),
transparent calc(100% - 2px)
);
@media (min-height: 480px) {
overflow-y: auto;
max-height: 50vh;
}
&.loading {
opacity: 0.25;
}
.ui-state {
min-height: 100px;
}
ul {
min-height: 100px;
display: flex;
gap: 4px;
list-style: none;
padding: 8px 2px;
margin: 0;
@media (min-height: 480px) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
grid-auto-rows: 1fr;
}
li {
list-style: none;
padding: 0;
margin: 0;
max-width: 100%;
display: flex;
button {
padding: 4px;
margin: 0;
border: none;
background-color: transparent;
color: inherit;
cursor: pointer;
border-radius: 8px;
background-color: var(--bg-faded-color);
@media (min-height: 480px) {
width: 100%;
text-align: center;
}
&:is(:hover, :focus) {
background-color: var(--link-bg-color);
box-shadow: 0 0 0 2px var(--link-light-color);
filter: none;
}
}
figure {
margin: 0;
padding: 0;
width: var(--figure-width);
max-width: 100%;
@media (min-height: 480px) {
width: 100%;
text-align: center;
}
figcaption {
font-size: 0.8em;
padding: 2px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--text-insignificant-color);
}
}
img {
background-color: var(--img-bg-color);
border-radius: 4px;
vertical-align: top;
object-fit: contain;
}
}
}
.pagination {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 0;
margin: 0;
position: sticky;
bottom: 0;
left: 0;
right: 0;
@media (min-height: 480px) {
position: static;
}
}
}
}

Wyświetl plik

@ -11,6 +11,8 @@ import { uid } from 'uid/single';
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
import { useSnapshot } from 'valtio';
import poweredByGiphyURL from '../assets/powered-by-giphy.svg';
import Menu2 from '../components/menu2';
import supportedLanguages from '../data/status-supported-languages';
import urlRegex from '../data/url-regex';
@ -41,7 +43,10 @@ import Loader from './loader';
import Modal from './modal';
import Status from './status';
const { PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL } = import.meta.env;
const {
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
} = import.meta.env;
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
const [code, common, native] = l;
@ -610,6 +615,7 @@ function Compose({
}, [mediaAttachments]);
const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
const [showGIFPicker, setShowGIFPicker] = useState(false);
const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
const topLanguages = [];
@ -1235,6 +1241,18 @@ function Compose({
>
<Icon icon="emoji2" />
</button>
{!!states.settings.composerGIFPicker && (
<button
type="button"
class="toolbar-button gif-picker-button"
disabled={uiState === 'loading'}
onClick={() => {
setShowGIFPicker(true);
}}
>
<span>GIF</span>
</button>
)}
</span>
<div class="spacer" />
{uiState === 'loading' ? (
@ -1319,6 +1337,64 @@ function Compose({
/>
</Modal>
)}
{showGIFPicker && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowGIFPicker(false);
}
}}
>
<GIFPickerModal
onClose={() => setShowGIFPicker(false)}
onSelect={({ url, type, alt_text }) => {
console.log('GIF URL', url);
if (mediaAttachments.length >= maxMediaAttachments) {
alert(
`You can only attach up to ${maxMediaAttachments} files.`,
);
return;
}
// Download the GIF and insert it as media attachment
(async () => {
let theToast;
try {
theToast = showToast({
text: 'Downloading GIF…',
duration: -1,
});
const blob = await fetch(url, {
referrerPolicy: 'no-referrer',
}).then((res) => res.blob());
const file = new File(
[blob],
type === 'video/mp4' ? 'video.mp4' : 'image.gif',
{
type,
},
);
const newMediaAttachments = [
...mediaAttachments,
{
file,
type,
size: file.size,
id: null,
description: alt_text || '',
},
];
setMediaAttachments(newMediaAttachments);
theToast?.hideToast?.();
} catch (err) {
console.error(err);
theToast?.hideToast?.();
showToast('Failed to download GIF');
}
})();
}}
/>
</Modal>
)}
</div>
);
}
@ -1711,6 +1787,9 @@ function MediaAttachment({
onDescriptionChange,
250,
);
useEffect(() => {
debouncedOnDescriptionChange(description);
}, [description, debouncedOnDescriptionChange]);
const [showModal, setShowModal] = useState(false);
const textareaRef = useRef(null);
@ -1759,7 +1838,7 @@ function MediaAttachment({
onInput={(e) => {
const { value } = e.target;
setDescription(value);
debouncedOnDescriptionChange(value);
// debouncedOnDescriptionChange(value);
}}
></textarea>
)}
@ -2243,4 +2322,225 @@ function CustomEmojisModal({
);
}
const GIFS_PER_PAGE = 20;
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
const [uiState, setUIState] = useState('default');
const [results, setResults] = useState([]);
const formRef = useRef(null);
const qRef = useRef(null);
const currentOffset = useRef(0);
const scrollableRef = useRef(null);
function fetchGIFs({ offset }) {
console.log('fetchGIFs', { offset });
if (!qRef.current?.value) return;
setUIState('loading');
scrollableRef.current?.scrollTo?.({
top: 0,
left: 0,
behavior: 'smooth',
});
(async () => {
try {
const query = {
api_key: GIPHY_API_KEY,
q: qRef.current.value,
rating: 'g',
limit: GIFS_PER_PAGE,
bundle: 'messaging_non_clips',
offset,
};
const response = await fetch(
'https://api.giphy.com/v1/gifs/search?' + new URLSearchParams(query),
{
referrerPolicy: 'no-referrer',
},
).then((r) => r.json());
currentOffset.current = response.pagination?.offset || 0;
setResults(response);
setUIState('results');
} catch (e) {
setUIState('error');
console.error(e);
}
})();
}
useEffect(() => {
qRef.current?.focus();
}, []);
return (
<div id="gif-picker-sheet" class="sheet">
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header>
<form
ref={formRef}
onSubmit={(e) => {
e.preventDefault();
fetchGIFs({ offset: 0 });
}}
>
<input
ref={qRef}
type="search"
name="q"
placeholder="Search GIFs"
required
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellCheck="false"
dir="auto"
/>
<input
type="image"
class="powered-button"
src={poweredByGiphyURL}
width="86"
height="30"
/>
</form>
</header>
<main ref={scrollableRef} class={uiState === 'loading' ? 'loading' : ''}>
{uiState === 'default' && (
<div class="ui-state">
<p class="insignificant">Type to search GIFs</p>
</div>
)}
{uiState === 'loading' && !results?.data?.length && (
<div class="ui-state">
<Loader abrupt />
</div>
)}
{results?.data?.length > 0 ? (
<>
<ul>
{results.data.map((gif) => {
const { id, images, title, alt_text } = gif;
const {
fixed_height_small,
fixed_height_downsampled,
fixed_height,
original,
} = images;
const theImage = fixed_height_small?.url
? fixed_height_small
: fixed_height_downsampled?.url
? fixed_height_downsampled
: fixed_height;
let { url, webp, width, height } = theImage;
if (+height > 100) {
width = (width / height) * 100;
height = 100;
}
const urlObj = new URL(url);
const strippedURL = urlObj.origin + urlObj.pathname;
let strippedWebP;
if (webp) {
const webpObj = new URL(webp);
strippedWebP = webpObj.origin + webpObj.pathname;
}
return (
<li key={id}>
<button
type="button"
onClick={() => {
const { mp4, url } = original;
const theURL = mp4 || url;
const urlObj = new URL(theURL);
const strippedURL = urlObj.origin + urlObj.pathname;
onClose();
onSelect({
url: strippedURL,
type: mp4 ? 'video/mp4' : 'image/gif',
alt_text: alt_text || title,
});
}}
>
<figure
style={{
'--figure-width': width + 'px',
// width: width + 'px'
}}
>
<picture>
{strippedWebP && (
<source srcset={strippedWebP} type="image/webp" />
)}
<img
src={strippedURL}
width={width}
height={height}
loading="lazy"
decoding="async"
alt={alt_text}
referrerpolicy="no-referrer"
onLoad={(e) => {
e.target.style.backgroundColor = 'transparent';
}}
/>
</picture>
<figcaption>{alt_text || title}</figcaption>
</figure>
</button>
</li>
);
})}
</ul>
<p class="pagination">
{results.pagination?.offset > 0 && (
<button
type="button"
class="light small"
disabled={uiState === 'loading'}
onClick={() => {
fetchGIFs({
offset: results.pagination?.offset - GIFS_PER_PAGE,
});
}}
>
<Icon icon="chevron-left" />
<span>Previous</span>
</button>
)}
<span />
{results.pagination?.offset + results.pagination?.count <
results.pagination?.total_count && (
<button
type="button"
class="light small"
disabled={uiState === 'loading'}
onClick={() => {
fetchGIFs({
offset: results.pagination?.offset + GIFS_PER_PAGE,
});
}}
>
<span>Next</span> <Icon icon="chevron-right" />
</button>
)}
</p>
</>
) : (
uiState === 'results' && (
<div class="ui-state">
<p>No results</p>
</div>
)
)}
{uiState === 'error' && (
<div class="ui-state">
<p>Error loading GIFs</p>
</div>
)}
</main>
</div>
);
}
export default Compose;

Wyświetl plik

@ -1,9 +1,11 @@
export default function CustomEmoji({ staticUrl, alt, url }) {
return (
<picture>
<source srcset={staticUrl} media="(prefers-reduced-motion: reduce)" />
{staticUrl && (
<source srcset={staticUrl} media="(prefers-reduced-motion: reduce)" />
)}
<img
key={alt}
key={alt || url}
src={url}
alt={alt}
class="shortcode-emoji emoji"

Wyświetl plik

@ -6,6 +6,15 @@ import Loader from './loader';
const supportsIntlSegmenter = !shouldPolyfill();
// Preload IntlSegmenter
setTimeout(() => {
queueMicrotask(() => {
if (!supportsIntlSegmenter) {
import('@formatjs/intl-segmenter/polyfill-force').catch(() => {});
}
});
}, 1000);
export default function IntlSegmenterSuspense({ children }) {
if (supportsIntlSegmenter) {
return <Suspense fallback={<Loader />}>{children}</Suspense>;

Wyświetl plik

@ -1,9 +1,12 @@
/*
Rendered but hidden. Only show when visible
*/
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useInView } from 'react-intersection-observer';
// The sticky header, usually at the top
const TOP = 48;
export default function LazyShazam({ children }) {
const containerRef = useRef();
const [visible, setVisible] = useState(false);
@ -11,6 +14,7 @@ export default function LazyShazam({ children }) {
const { ref } = useInView({
root: null,
rootMargin: `-${TOP}px 0px 0px 0px`,
trackVisibility: true,
delay: 1000,
onChange: (inView) => {
@ -22,11 +26,15 @@ export default function LazyShazam({ children }) {
skip: visibleStart || visible,
});
useLayoutEffect(() => {
useEffect(() => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
if (rect.bottom > 0) {
setVisibleStart(true);
if (rect.bottom > TOP) {
if (rect.top < window.innerHeight) {
setVisible(true);
} else {
setVisibleStart(true);
}
}
}, []);

Wyświetl plik

@ -1,8 +1,8 @@
import { MenuItem, SubMenu } from '@szhsin/react-menu';
import { MenuItem } from '@szhsin/react-menu';
import { cloneElement } from 'preact';
import { useRef } from 'preact/hooks';
import Menu2 from './menu2';
import SubMenu2 from './submenu2';
function MenuConfirm({
subMenu = false,
@ -23,11 +23,9 @@ function MenuConfirm({
}
return children;
}
const Parent = subMenu ? SubMenu : Menu2;
const menuRef = useRef();
const Parent = subMenu ? SubMenu2 : Menu2;
return (
<Parent
instanceRef={menuRef}
openTrigger="clickOnly"
direction="bottom"
overflow="auto"
@ -37,19 +35,6 @@ function MenuConfirm({
{...restProps}
menuButton={subMenu ? undefined : children}
label={subMenu ? children : undefined}
// Test fix for bug; submenus not opening on Android
itemProps={{
onPointerMove: (e) => {
if (e.pointerType === 'touch') {
menuRef.current?.openMenu?.();
}
},
onPointerLeave: (e) => {
if (e.pointerType === 'touch') {
menuRef.current?.openMenu?.();
}
},
}}
>
<MenuItem className={menuItemClassName} onClick={onClick}>
{confirmLabel}

Wyświetl plik

@ -1,11 +1,6 @@
import './nav-menu.css';
import {
ControlledMenu,
MenuDivider,
MenuItem,
SubMenu,
} from '@szhsin/react-menu';
import { ControlledMenu, 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';
@ -20,8 +15,7 @@ import store from '../utils/store';
import Avatar from './avatar';
import Icon from './icon';
import MenuLink from './menu-link';
const supportsTouch = 'ontouchstart' in window;
import SubMenu2 from './submenu2';
function NavMenu(props) {
const snapStates = useSnapshot(states);
@ -150,7 +144,7 @@ function NavMenu(props) {
}}
{...props}
overflow="auto"
// viewScroll="close"
viewScroll="close"
position="anchor"
align="center"
boundingBoxPadding={boundingBoxPadding}
@ -211,8 +205,7 @@ function NavMenu(props) {
</MenuLink>
)}
{lists?.length > 0 ? (
<SubMenu
openTrigger={supportsTouch ? 'clickOnly' : undefined}
<SubMenu2
menuClassName="nav-submenu"
overflow="auto"
gap={-8}
@ -237,7 +230,7 @@ function NavMenu(props) {
))}
</>
)}
</SubMenu>
</SubMenu2>
) : (
<MenuLink to="/l">
<Icon icon="list" size="l" />
@ -247,8 +240,7 @@ function NavMenu(props) {
<MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
</MenuLink>
<SubMenu
openTrigger={supportsTouch ? 'clickOnly' : undefined}
<SubMenu2
menuClassName="nav-submenu"
overflow="auto"
gap={-8}
@ -297,7 +289,7 @@ function NavMenu(props) {
<Icon icon="block" size="l" />
Blocked users&hellip;
</MenuItem>{' '}
</SubMenu>
</SubMenu2>
<MenuDivider />
<MenuItem
onClick={() => {

Wyświetl plik

@ -1,6 +1,6 @@
import './shortcuts.css';
import { MenuDivider, SubMenu } from '@szhsin/react-menu';
import { MenuDivider } from '@szhsin/react-menu';
import { memo } from 'preact/compat';
import { useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
@ -17,6 +17,7 @@ import Icon from './icon';
import Link from './link';
import Menu2 from './menu2';
import MenuLink from './menu-link';
import SubMenu2 from './submenu2';
function Shortcuts() {
const { instance } = api();
@ -182,7 +183,7 @@ function Shortcuts() {
{formattedShortcuts.map(({ id, path, title, subtitle, icon }, i) => {
if (id === 'lists') {
return (
<SubMenu
<SubMenu2
menuClassName="glass-menu"
overflow="auto"
gap={-8}
@ -205,7 +206,7 @@ function Shortcuts() {
<span>{list.title}</span>
</MenuLink>
))}
</SubMenu>
</SubMenu2>
);
}

Wyświetl plik

@ -1695,13 +1695,14 @@ a.card:is(:hover, :focus):visited {
}
.poll-label input:is([type='radio'], [type='checkbox']) {
flex-shrink: 0;
margin: 3px;
min-height: 1em;
margin: 0 3px;
min-height: 0.9em;
}
.poll-option-votes {
flex-shrink: 0;
font-size: 90%;
opacity: 0.75;
line-height: 1;
}
.poll-option-leading .poll-option-votes {
font-weight: bold;

Wyświetl plik

@ -20,6 +20,7 @@ import {
useRef,
useState,
} from 'preact/hooks';
import punycode from 'punycode';
import { useHotkeys } from 'react-hotkeys-hook';
import { useLongPress } from 'use-long-press';
import { useSnapshot } from 'valtio';
@ -1942,7 +1943,24 @@ function Status({
{!!emojiReactions?.length && (
<div class="emoji-reactions">
{emojiReactions.map((emojiReaction) => {
const { name, count, me } = emojiReaction;
const { name, count, me, url, staticUrl } = emojiReaction;
if (url) {
// Some servers return url and staticUrl
return (
<span
class={`emoji-reaction tag ${
me ? '' : 'insignificant'
}`}
>
<CustomEmoji
alt={name}
url={url}
staticUrl={staticUrl}
/>{' '}
{count}
</span>
);
}
const isShortCode = /^:.+?:$/.test(name);
if (isShortCode) {
const emoji = emojis.find(
@ -1961,7 +1979,7 @@ function Status({
alt={name}
url={emoji.url}
staticUrl={emoji.staticUrl}
/>
/>{' '}
{count}
</span>
);
@ -2231,9 +2249,9 @@ function Card({ card, selfReferential, instance }) {
);
if (hasText && (image || (type === 'photo' && blurhash))) {
const domain = new URL(url).hostname
.replace(/^www\./, '')
.replace(/\/$/, '');
const domain = punycode.toUnicode(
new URL(url).hostname.replace(/^www\./, '').replace(/\/$/, ''),
);
let blurhashImage;
const rgbAverageColor =
image && blurhash ? getBlurHashAverageColor(blurhash) : null;
@ -2349,7 +2367,9 @@ function Card({ card, selfReferential, instance }) {
// );
}
if (hasText && !image) {
const domain = new URL(url).hostname.replace(/^www\./, '');
const domain = punycode.toUnicode(
new URL(url).hostname.replace(/^www\./, ''),
);
return (
<a
href={cardStatusURL || url}
@ -2881,7 +2901,7 @@ function nicePostURL(url) {
const [_, username, restPath] = path.match(/\/(@[^\/]+)\/(.*)/) || [];
return (
<>
{host}
{punycode.toUnicode(host)}
{username ? (
<>
/{username}

Wyświetl plik

@ -0,0 +1,25 @@
import { SubMenu } from '@szhsin/react-menu';
import { useRef } from 'preact/hooks';
export default function SubMenu2(props) {
const menuRef = useRef();
return (
<SubMenu
{...props}
instanceRef={menuRef}
// Test fix for bug; submenus not opening on Android
itemProps={{
onPointerMove: (e) => {
if (e.pointerType === 'touch') {
menuRef.current?.openMenu?.();
}
},
onPointerLeave: (e) => {
if (e.pointerType === 'touch') {
menuRef.current?.openMenu?.();
}
},
}}
/>
);
}

Wyświetl plik

@ -51,7 +51,7 @@ function Timeline({
}) {
const snapStates = useSnapshot(states);
const [items, setItems] = useState([]);
const [uiState, setUIState] = useState('default');
const [uiState, setUIState] = useState('start');
const [showMore, setShowMore] = useState(false);
const [showNew, setShowNew] = useState(false);
const [visible, setVisible] = useState(true);
@ -496,7 +496,8 @@ function Timeline({
)}
</ul>
) : (
uiState !== 'error' && <p class="ui-state">{emptyText}</p>
uiState !== 'error' &&
uiState !== 'start' && <p class="ui-state">{emptyText}</p>
)}
{uiState === 'error' && (
<p class="ui-state">

Wyświetl plik

@ -10,6 +10,7 @@ import localeCode2Text from '../utils/localeCode2Text';
import pmem from '../utils/pmem';
import Icon from './icon';
import LazyShazam from './lazy-shazam';
import Loader from './loader';
const { PHANPY_LINGVA_INSTANCES } = import.meta.env;
@ -142,23 +143,21 @@ function TranslationBlock({
detectedLang !== targetLangText
) {
return (
<div class="shazam-container">
<div class="shazam-container-inner">
<div class="status-translation-block-mini">
<Icon
icon="translate"
alt={`Auto-translated from ${sourceLangText}`}
/>
<output
lang={targetLang}
dir="auto"
title={pronunciationContent || ''}
>
{translatedContent}
</output>
</div>
<LazyShazam>
<div class="status-translation-block-mini">
<Icon
icon="translate"
alt={`Auto-translated from ${sourceLangText}`}
/>
<output
lang={targetLang}
dir="auto"
title={pronunciationContent || ''}
>
{translatedContent}
</output>
</div>
</div>
</LazyShazam>
);
}
return null;

Wyświetl plik

@ -347,6 +347,7 @@ button[hidden] {
}
input[type='text'],
input[type='search'],
textarea,
select {
color: var(--text-color);
@ -356,6 +357,7 @@ select {
border-radius: 4px;
}
input[type='text']:focus,
input[type='search']:focus,
textarea:focus,
select:focus {
border-color: var(--outline-color);
@ -371,7 +373,7 @@ textarea:disabled {
background-color: var(--bg-faded-color);
}
:is(input[type='text'], textarea, select).block {
:is(input[type='text'], input[type='search'], textarea, select).block {
display: block;
width: 100%;
}

Wyświetl plik

@ -6,6 +6,7 @@ import {
useRef,
useState,
} from 'preact/hooks';
import punycode from 'punycode';
import { useParams, useSearchParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
@ -516,7 +517,13 @@ function AccountStatuses() {
>
<Icon icon="transfer" />{' '}
<small class="menu-double-lines">
Switch to account's instance (<b>{accountInstance}</b>)
Switch to account's instance{' '}
{accountInstance ? (
<>
{' '}
(<b>{punycode.toUnicode(accountInstance)}</b>)
</>
) : null}
</small>
</MenuItem>
{!sameCurrentInstance && (

Wyświetl plik

@ -13,6 +13,7 @@ import {
useRef,
useState,
} from 'preact/hooks';
import punycode from 'punycode';
import { useHotkeys } from 'react-hotkeys-hook';
import { useSearchParams } from 'react-router-dom';
import { uid } from 'uid/single';
@ -1099,9 +1100,11 @@ function Catchup() {
height,
publishedAt,
} = card;
const domain = new URL(url).hostname
.replace(/^www\./, '')
.replace(/\/$/, '');
const domain = punycode.toUnicode(
new URL(url).hostname
.replace(/^www\./, '')
.replace(/\/$/, ''),
);
let accentColor;
if (blurhash) {
const averageColor = getBlurHashAverageColor(blurhash);

Wyświetl plik

@ -12,7 +12,7 @@ import { getAuthorizationURL, registerApplication } from '../utils/auth';
import store from '../utils/store';
import useTitle from '../utils/useTitle';
const { PHANPY_DEFAULT_INSTANCE: DEFAULT_INSTANCE } = import.meta.env;
const { PHANPY_DEFAULT_INSTANCE: DEFAULT_INSTANCE, PHANPY_SCHEME: SCHEME = 'https' } = import.meta.env;
function Login() {
useTitle('Log in');
@ -85,9 +85,11 @@ function Login() {
.replace(/^@?[^@]+@/, '') // Remove @?acct@
.trim()
: null;
const instanceTextLooksLikeDomain =
/[^\s\r\n\t\/\\]+\.[^\s\r\n\t\/\\]+/.test(cleanInstanceText) &&
!/[\s\/\\@]/.test(cleanInstanceText);
const instanceTextLooksLikeDomain =
(/[^\s\r\n\t\/\\]+\.[^\s\r\n\t\/\\]+/.test(cleanInstanceText) &&
!/[\s\/\\@]/.test(cleanInstanceText)) || SCHEME === "http";
console.log(SCHEME)
const instancesSuggestions = cleanInstanceText
? instancesList

Wyświetl plik

@ -177,6 +177,7 @@ function Search({ columnMode, ...props }) {
['/', 'Slash'],
(e) => {
searchFormRef.current?.focus?.();
searchFormRef.current?.select?.();
},
{
preventDefault: true,

Wyświetl plik

@ -28,6 +28,7 @@ const {
PHANPY_WEBSITE: WEBSITE,
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
} = import.meta.env;
function Settings({ onClose }) {
@ -433,6 +434,37 @@ function Settings({ onClose }) {
</div>
</div>
</li>
{!!GIPHY_API_KEY && authenticated && (
<li>
<label>
<input
type="checkbox"
checked={snapStates.settings.composerGIFPicker}
onChange={(e) => {
states.settings.composerGIFPicker = e.target.checked;
}}
/>{' '}
GIF Picker for composer
</label>
<div class="sub-section insignificant">
<small>
Note: This feature uses external GIF search service, powered
by{' '}
<a
href="https://developers.giphy.com/"
target="_blank"
rel="noopener noreferrer"
>
GIPHY
</a>
. G-rated (suitable for viewing by all ages), tracking
parameters are stripped, referrer information is omitted
from requests, but search queries and IP address information
will still reach their servers.
</small>
</div>
</li>
)}
{!!IMG_ALT_API_URL && authenticated && (
<li>
<label>

Wyświetl plik

@ -12,10 +12,10 @@ import {
useRef,
useState,
} from 'preact/hooks';
import punycode from 'punycode';
import { useHotkeys } from 'react-hotkeys-hook';
import { InView } from 'react-intersection-observer';
import { matchPath, useSearchParams } from 'react-router-dom';
import { useDebouncedCallback } from 'use-debounce';
import { useSnapshot } from 'valtio';
import Avatar from '../components/avatar';
@ -122,7 +122,7 @@ function StatusPage(params) {
}, [showMedia]);
const mediaAttachments = mediaStatusID
? mediaStatus?.mediaAttachments
? snapStates.statuses[statusKey(mediaStatusID, instance)]?.mediaAttachments
: heroStatus?.mediaAttachments;
const handleMediaClose = useCallback(() => {
@ -1208,7 +1208,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
{postInstance ? (
<>
{' '}
(<b>{postInstance}</b>)
(<b>{punycode.toUnicode(postInstance)}</b>)
</>
) : (
''

Wyświetl plik

@ -3,6 +3,7 @@ import '../components/links-bar.css';
import { MenuItem } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
import { useMemo, useRef, useState } from 'preact/hooks';
import punycode from 'punycode';
import { useNavigate, useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
@ -161,9 +162,9 @@ function Trending({ columnMode, ...props }) {
url,
width,
} = link;
const domain = new URL(url).hostname
.replace(/^www\./, '')
.replace(/\/$/, '');
const domain = punycode.toUnicode(
new URL(url).hostname.replace(/^www\./, '').replace(/\/$/, ''),
);
let accentColor;
if (blurhash) {
const averageColor = getBlurHashAverageColor(blurhash);

Wyświetl plik

@ -9,6 +9,8 @@ import {
saveAccount,
} from './store-utils';
const { PHANPY_SCHEME: SCHEME = 'https' } = import.meta.env;
// Default *fallback* instance
const DEFAULT_INSTANCE = 'mastodon.social';
@ -36,7 +38,9 @@ export function initClient({ instance, accessToken }) {
.replace(/\/+$/, '')
.toLowerCase();
}
const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`;
const url = instance
? `${SCHEME}://${instance}`
: `${SCHEME}://${DEFAULT_INSTANCE}`;
const masto = createRestAPIClient({
url,

Wyświetl plik

@ -1,5 +1,8 @@
const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
.env;
const {
PHANPY_CLIENT_NAME: CLIENT_NAME,
PHANPY_WEBSITE: WEBSITE,
PHANPY_SCHEME: SCHEME = 'https',
} = import.meta.env;
const SCOPES = 'read write follow push';
@ -11,7 +14,7 @@ export async function registerApplication({ instanceURL }) {
website: WEBSITE,
});
const registrationResponse = await fetch(
`https://${instanceURL}/api/v1/apps`,
`${SCHEME}://${instanceURL}/api/v1/apps`,
{
method: 'POST',
headers: {
@ -33,7 +36,7 @@ export async function getAuthorizationURL({ instanceURL, client_id }) {
// redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
response_type: 'code',
});
const authorizationURL = `https://${instanceURL}/oauth/authorize?${authorizationParams.toString()}`;
const authorizationURL = `${SCHEME}://${instanceURL}/oauth/authorize?${authorizationParams.toString()}`;
return authorizationURL;
}
@ -51,7 +54,7 @@ export async function getAccessToken({
code,
scope: SCOPES,
});
const tokenResponse = await fetch(`https://${instanceURL}/oauth/token`, {
const tokenResponse = await fetch(`${SCHEME}://${instanceURL}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',

Wyświetl plik

@ -1,10 +1,16 @@
export default function localeCode2Text(code) {
import mem from './mem';
const IntlDN = new Intl.DisplayNames(navigator.languages, {
type: 'language',
});
function _localeCode2Text(code) {
try {
return new Intl.DisplayNames(navigator.languages, {
type: 'language',
}).of(code);
return IntlDN.of(code);
} catch (e) {
console.error(e);
return null;
}
}
export default mem(_localeCode2Text);

Wyświetl plik

@ -67,6 +67,7 @@ const states = proxy({
contentTranslationAutoInline: false,
shortcutSettingsCloudImportExport: false,
mediaAltGenerator: false,
composerGIFPicker: false,
cloakMode: false,
},
});
@ -99,6 +100,8 @@ export function initStates() {
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
states.settings.mediaAltGenerator =
store.account.get('settings-mediaAltGenerator') ?? false;
states.settings.composerGIFPicker =
store.account.get('settings-composerGIFPicker') ?? false;
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
}
@ -140,6 +143,9 @@ subscribe(states, (changes) => {
if (path.join('.') === 'settings.mediaAltGenerator') {
store.account.set('settings-mediaAltGenerator', !!value);
}
if (path.join('.') === 'settings.composerGIFPicker') {
store.account.set('settings-composerGIFPicker', !!value);
}
if (path?.[0] === 'shortcuts') {
store.account.set('shortcuts', states.shortcuts);
}