kopia lustrzana https://github.com/cheeaun/phanpy
Porównaj commity
19 Commity
fd298051df
...
ba5e172440
Autor | SHA1 | Data |
---|---|---|
Alyx | ba5e172440 | |
Lim Chee Aun | 015ed5e7eb | |
Lim Chee Aun | 2ad9706304 | |
Lim Chee Aun | 30382d088b | |
Lim Chee Aun | 80196f83ca | |
Lim Chee Aun | 419ad34250 | |
Lim Chee Aun | ed0d714cf2 | |
Lim Chee Aun | 708976a9e9 | |
Lim Chee Aun | d77ba19308 | |
Lim Chee Aun | b10e22a9a2 | |
Lim Chee Aun | 36d8b62e1e | |
Lim Chee Aun | 989e788d8e | |
Lim Chee Aun | ebd9f05f69 | |
Lim Chee Aun | 5246af4ae9 | |
Lim Chee Aun | e6ba72f4c8 | |
Lim Chee Aun | 960dff8b9e | |
Lim Chee Aun | e3c25d25ee | |
Lim Chee Aun | 090320150a | |
Alyx | fa196a2c94 |
|
@ -0,0 +1,23 @@
|
|||
FROM busybox:1 AS build
|
||||
ARG PHANPY_RELEASE_VERSION
|
||||
|
||||
WORKDIR /root/phanpy_release
|
||||
|
||||
RUN wget "https://github.com/cheeaun/phanpy/releases/download/${PHANPY_RELEASE_VERSION}/phanpy-dist.tar.gz" && \
|
||||
tar -xvf "phanpy-dist.tar.gz" -C /root/phanpy_release && \
|
||||
rm "phanpy-dist.tar.gz"
|
||||
|
||||
# ---
|
||||
FROM busybox:1
|
||||
|
||||
# Create a non-root user to own the files and run our server
|
||||
RUN adduser -D static
|
||||
USER static
|
||||
WORKDIR /home/static
|
||||
|
||||
# Copy the static website
|
||||
# Use the .dockerignore file to control what ends up inside the image!
|
||||
COPY --chown=static:static --from=build /root/phanpy_release /home/static
|
||||
|
||||
# Run BusyBox httpd
|
||||
CMD ["httpd", "-f", "-v", "-p", "8080"]
|
19
src/app.jsx
19
src/app.jsx
|
@ -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>
|
||||
{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 />} />
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
@ -33,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 = [
|
||||
|
@ -582,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
|
||||
|
@ -660,6 +671,7 @@ function AccountInfo({
|
|||
// states.showAccount = false;
|
||||
setTimeout(() => {
|
||||
states.showGenericAccounts = {
|
||||
id: 'followers',
|
||||
heading: 'Followers',
|
||||
fetchAccounts: fetchFollowers,
|
||||
instance,
|
||||
|
@ -1273,7 +1285,7 @@ function RelatedActions({
|
|||
<span>Unmute @{username}</span>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<SubMenu
|
||||
<SubMenu2
|
||||
menuClassName="menu-blur"
|
||||
openTrigger="clickOnly"
|
||||
direction="bottom"
|
||||
|
@ -1327,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
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
export default function CustomEmoji({ staticUrl, alt, url }) {
|
||||
return (
|
||||
<picture>
|
||||
{staticUrl && (
|
||||
<source srcset={staticUrl} media="(prefers-reduced-motion: reduce)" />
|
||||
)}
|
||||
<img
|
||||
key={alt}
|
||||
key={alt || url}
|
||||
src={url}
|
||||
alt={alt}
|
||||
class="shortcode-emoji emoji"
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,12 +26,16 @@ export default function LazyShazam({ children }) {
|
|||
skip: visibleStart || visible,
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
if (rect.bottom > 0) {
|
||||
if (rect.bottom > TOP) {
|
||||
if (rect.top < window.innerHeight) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
setVisibleStart(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (visibleStart) return children;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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…
|
||||
</MenuItem>{' '}
|
||||
</SubMenu>
|
||||
</SubMenu2>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1943,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(
|
||||
|
@ -1962,7 +1979,7 @@ function Status({
|
|||
alt={name}
|
||||
url={emoji.url}
|
||||
staticUrl={emoji.staticUrl}
|
||||
/>
|
||||
/>{' '}
|
||||
{count}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -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?.();
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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,8 +143,7 @@ function TranslationBlock({
|
|||
detectedLang !== targetLangText
|
||||
) {
|
||||
return (
|
||||
<div class="shazam-container">
|
||||
<div class="shazam-container-inner">
|
||||
<LazyShazam>
|
||||
<div class="status-translation-block-mini">
|
||||
<Icon
|
||||
icon="translate"
|
||||
|
@ -157,8 +157,7 @@ function TranslationBlock({
|
|||
{translatedContent}
|
||||
</output>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyShazam>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -177,6 +177,7 @@ function Search({ columnMode, ...props }) {
|
|||
['/', 'Slash'],
|
||||
(e) => {
|
||||
searchFormRef.current?.focus?.();
|
||||
searchFormRef.current?.select?.();
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
export default function localeCode2Text(code) {
|
||||
try {
|
||||
return new Intl.DisplayNames(navigator.languages, {
|
||||
import mem from './mem';
|
||||
|
||||
const IntlDN = new Intl.DisplayNames(navigator.languages, {
|
||||
type: 'language',
|
||||
}).of(code);
|
||||
});
|
||||
|
||||
function _localeCode2Text(code) {
|
||||
try {
|
||||
return IntlDN.of(code);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default mem(_localeCode2Text);
|
||||
|
|
Ładowanie…
Reference in New Issue