From 36060918ecb6e4a31a8a68bc3194fca75e4e7c19 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:00:04 -0500 Subject: [PATCH 01/12] Fix private message feed showing sent messages when switching accounts (#1574) --- src/routes/pages/inbox/InboxPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/pages/inbox/InboxPage.tsx b/src/routes/pages/inbox/InboxPage.tsx index dcb2b728ee..7eb0b14012 100644 --- a/src/routes/pages/inbox/InboxPage.tsx +++ b/src/routes/pages/inbox/InboxPage.tsx @@ -40,6 +40,8 @@ export default function InboxPage({ showRead }: InboxPageProps) { const fetchFn: FetchFn = useCallback( async (pageData) => { + if (!myUserId) return []; + const params = { limit: 50, ...pageData, From 77be83771274cf7d41b7e57537df6c061aa9a3fc Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:55:30 -0500 Subject: [PATCH 02/12] Add subscribed icon (#1573) Resolves #504 --- src/features/labels/links/CommunityLink.tsx | 52 +++++++++++++++++-- src/features/labels/links/PersonLink.tsx | 1 + src/features/settings/general/other/Other.tsx | 2 + .../settings/general/other/SubscribedIcon.tsx | 24 +++++++++ src/features/settings/settingsSlice.tsx | 13 +++++ .../settings/shared/SettingSelector.tsx | 1 + src/routes/pages/shared/SpecialFeedPage.tsx | 21 ++++---- src/services/db.ts | 10 ++++ 8 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/features/settings/general/other/SubscribedIcon.tsx diff --git a/src/features/labels/links/CommunityLink.tsx b/src/features/labels/links/CommunityLink.tsx index 4ba9f41c4c..7b63a98ead 100644 --- a/src/features/labels/links/CommunityLink.tsx +++ b/src/features/labels/links/CommunityLink.tsx @@ -4,16 +4,17 @@ import { Community, SubscribedType } from "lemmy-js-client"; import { renderHandle } from "../Handle"; import { LinkContainer, StyledLink, hideCss } from "./shared"; import ItemIcon from "../img/ItemIcon"; -import { useIonActionSheet } from "@ionic/react"; +import { IonIcon, useIonActionSheet } from "@ionic/react"; import { LongPressOptions, useLongPress } from "use-long-press"; import { + heart, heartDislikeOutline, heartOutline, removeCircleOutline, tabletPortraitOutline, } from "ionicons/icons"; import useCommunityActions from "../../community/useCommunityActions"; -import { useCallback, useContext } from "react"; +import { createContext, useCallback, useContext } from "react"; import { ShareImageContext } from "../../share/asImage/ShareAsImage"; import { preventOnClickNavigationBug, @@ -22,12 +23,28 @@ import { import { styled } from "@linaria/react"; import { cx } from "@linaria/core"; import { useAppSelector } from "../../../store"; +import { OShowSubscribedIcon } from "../../../services/db"; const StyledItemIcon = styled(ItemIcon)` margin-right: 0.4rem; vertical-align: middle; `; +const SubscribedIcon = styled(IonIcon)` + color: var(--ion-color-danger); + vertical-align: middle; + font-size: 0.85em; + + margin-bottom: 1px; + margin-left: 2px; + + opacity: 0.4; + + .ion-palette-dark & { + opacity: 0.5; + } +`; + interface CommunityLinkProps { community: Community; showInstanceWhenRemote?: boolean; @@ -55,6 +72,7 @@ export default function CommunityLink({ const showCommunityIcons = useAppSelector( (state) => state.settings.appearance.posts.showCommunityIcons, ); + const showSubscribed = useShowSubscribedIcon(); const { isSubscribed, isBlocked, subscribe, block, sidebar } = useCommunityActions(community, subscribed); @@ -106,6 +124,13 @@ export default function CommunityLink({ showInstanceWhenRemote, }); + const end = ( + <> + {instance} + {showSubscribed && isSubscribed && } + + ); + return ( {showCommunityIcons && !hideCommunity && !hideIcon && ( )} {name} - {!disableInstanceClick && instance} + {!disableInstanceClick && end} - {disableInstanceClick && instance} + {disableInstanceClick && end} ); } @@ -133,3 +159,21 @@ export default function CommunityLink({ const onStart: LongPressOptions["onStart"] = (e) => { e.stopPropagation(); }; + +function useShowSubscribedIcon() { + const feedEnabled = useContext(ShowSubscribedIconContext); + const subscribedIcon = useAppSelector( + (state) => state.settings.general.subscribedIcon, + ); + + switch (subscribedIcon) { + case OShowSubscribedIcon.OnlyAllLocal: + return feedEnabled; + case OShowSubscribedIcon.Never: + return false; + case OShowSubscribedIcon.Everywhere: + return true; + } +} + +export const ShowSubscribedIconContext = createContext(false); diff --git a/src/features/labels/links/PersonLink.tsx b/src/features/labels/links/PersonLink.tsx index be72e08e5a..6c52ce0ee3 100644 --- a/src/features/labels/links/PersonLink.tsx +++ b/src/features/labels/links/PersonLink.tsx @@ -148,6 +148,7 @@ export default function PersonLink({ e.stopPropagation(); preventOnClickNavigationBug(e); }} + draggable={false} > {prefix ? ( <> diff --git a/src/features/settings/general/other/Other.tsx b/src/features/settings/general/other/Other.tsx index 1664099bc6..a2acf6c6f6 100644 --- a/src/features/settings/general/other/Other.tsx +++ b/src/features/settings/general/other/Other.tsx @@ -9,6 +9,7 @@ import OpenNativeApps from "./OpenNativeApps"; import ClearCache from "./ClearCache"; import BackupSettings from "./backup/BackupSettings"; import Thumbnailinator from "./Thumbnailinator"; +import SubscribedIcon from "./SubscribedIcon"; export default function Other() { return ( @@ -24,6 +25,7 @@ export default function Other() { + diff --git a/src/features/settings/general/other/SubscribedIcon.tsx b/src/features/settings/general/other/SubscribedIcon.tsx new file mode 100644 index 0000000000..8bd3d7733b --- /dev/null +++ b/src/features/settings/general/other/SubscribedIcon.tsx @@ -0,0 +1,24 @@ +import { startCase } from "lodash"; +import { OShowSubscribedIcon } from "../../../../services/db"; +import { useAppSelector } from "../../../../store"; +import { setSubscribedIcon } from "../../settingsSlice"; +import SettingSelector from "../../shared/SettingSelector"; + +export default function SubscribedIcon() { + const subscribedIcon = useAppSelector( + (state) => state.settings.general.subscribedIcon, + ); + + return ( + { + if (option === OShowSubscribedIcon.OnlyAllLocal) return "All/Local"; + return startCase(option); + }} + /> + ); +} diff --git a/src/features/settings/settingsSlice.tsx b/src/features/settings/settingsSlice.tsx index 4bcde086af..ac1664fcf3 100644 --- a/src/features/settings/settingsSlice.tsx +++ b/src/features/settings/settingsSlice.tsx @@ -40,6 +40,8 @@ import { OAutoplayMediaType, CommentsThemeType, VotesThemeType, + ShowSubscribedIcon, + OShowSubscribedIcon, } from "../../services/db"; import { LOCALSTORAGE_KEYS, get, set } from "./syncStorage"; import { Mode } from "@ionic/core"; @@ -136,6 +138,7 @@ interface SettingsState { defaultFeed: DefaultFeedType | undefined; noSubscribedInFeed: boolean; thumbnailinatorEnabled: boolean; + subscribedIcon: ShowSubscribedIcon; }; blocks: { keywords: string[]; @@ -222,6 +225,7 @@ export const initialState: SettingsState = { defaultFeed: undefined, noSubscribedInFeed: false, thumbnailinatorEnabled: true, + subscribedIcon: OShowSubscribedIcon.Never, }, blocks: { keywords: [], @@ -532,6 +536,11 @@ export const appearanceSlice = createSlice({ db.setSetting("prefer_native_apps", action.payload); }, + setSubscribedIcon(state, action: PayloadAction) { + state.general.subscribedIcon = action.payload; + + db.setSetting("subscribed_icon", action.payload); + }, resetSettings: () => ({ ...initialState, @@ -722,6 +731,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk( const quick_switch_dark_mode = await db.getSetting( "quick_switch_dark_mode", ); + const subscribed_icon = await db.getSetting("subscribed_icon"); return { ...state.settings, @@ -864,6 +874,8 @@ export const fetchSettingsFromDatabase = createAsyncThunk( thumbnailinatorEnabled: thumbnailinator_enabled ?? initialState.general.thumbnailinatorEnabled, + subscribedIcon: + subscribed_icon ?? initialState.general.subscribedIcon, }, blocks: { keywords: filtered_keywords ?? initialState.blocks.keywords, @@ -944,6 +956,7 @@ export const { setAlwaysUseReaderMode, setShowCollapsedComment, setQuickSwitchDarkMode, + setSubscribedIcon, } = appearanceSlice.actions; export default appearanceSlice.reducer; diff --git a/src/features/settings/shared/SettingSelector.tsx b/src/features/settings/shared/SettingSelector.tsx index 0df01239e0..645009ed84 100644 --- a/src/features/settings/shared/SettingSelector.tsx +++ b/src/features/settings/shared/SettingSelector.tsx @@ -83,6 +83,7 @@ export default function SettingSelector< {title} {getSelectedLabel?.(selected) ?? + getOptionLabel?.(selected) ?? (typeof selected === "string" ? startCase(selected) : selected)} ; return ( - - - - - + + + + + + + ); })(); diff --git a/src/services/db.ts b/src/services/db.ts index ce287c5c6e..87b9a48765 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -172,6 +172,15 @@ export const OLinkHandlerType = { InApp: "in-app", } as const; +export type ShowSubscribedIcon = + (typeof OShowSubscribedIcon)[keyof typeof OShowSubscribedIcon]; + +export const OShowSubscribedIcon = { + Never: "never", + OnlyAllLocal: "all-local", + Everywhere: "everywhere", +} as const; + export type DefaultFeedType = | { type: @@ -351,6 +360,7 @@ export type SettingValueTypes = { autoplay_media: AutoplayMediaType; show_collapsed_comment: boolean; quick_switch_dark_mode: boolean; + subscribed_icon: ShowSubscribedIcon; }; export interface ISettingItem { From 493cc7c0b65d478504b2fc2731af3f3578c46da7 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:59:39 -0500 Subject: [PATCH 03/12] Fix subscribed icon showing in mod feed (#1575) --- src/routes/pages/shared/SpecialFeedPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/pages/shared/SpecialFeedPage.tsx b/src/routes/pages/shared/SpecialFeedPage.tsx index d59e015c54..c90582ae52 100644 --- a/src/routes/pages/shared/SpecialFeedPage.tsx +++ b/src/routes/pages/shared/SpecialFeedPage.tsx @@ -94,7 +94,9 @@ export default function SpecialFeedPage({ type }: SpecialFeedProps) { if (!sort) return ; return ( - + Date: Sat, 10 Aug 2024 18:48:08 -0500 Subject: [PATCH 04/12] Improve support for testing (http, custom ports, env variables) (#1577) Resolves #1576 Now you can set the following (testing purposes only!): - `VITE_FORCE_LEMMY_INSECURE=1` to force all lemmy instance connections to http instead of https. - `VITE_CUSTOM_LEMMY_SERVERS=localhost:3001,localhost:3002` a comma separated list of default lemmy servers. If _config is found it will be preferred. --- src/features/auth/login/login/Login.tsx | 4 ++-- src/features/comment/CommentLinks.tsx | 6 ++++-- src/features/feed/SpecialFeedMoreActions.tsx | 5 +++-- src/features/shared/markdown/LinkInterceptor.tsx | 9 +++++---- src/features/shared/useLemmyUrlHandler.ts | 6 ++++-- src/routes/common/ActorRedirect.tsx | 11 ++++++++++- src/services/app.ts | 10 +++++++++- src/services/lemmy.ts | 8 ++++++-- 8 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/features/auth/login/login/Login.tsx b/src/features/auth/login/login/Login.tsx index 33354da266..722c8680e0 100644 --- a/src/features/auth/login/login/Login.tsx +++ b/src/features/auth/login/login/Login.tsx @@ -26,7 +26,7 @@ import Totp from "./Totp"; import { DynamicDismissableModalContext } from "../../../shared/DynamicDismissableModal"; import InAppExternalLink from "../../../shared/InAppExternalLink"; import { HelperText } from "../../../settings/shared/formatting"; -import { getImageSrc } from "../../../../services/lemmy"; +import { buildBaseLemmyUrl, getImageSrc } from "../../../../services/lemmy"; import { loginSuccess } from "../../../../helpers/toastMessages"; import lemmyLogo from "../lemmyLogo.svg"; import { styled } from "@linaria/react"; @@ -162,7 +162,7 @@ export default function Login({ url, siteIcon }: LoginProps) {
You are logging in to{" "} diff --git a/src/features/comment/CommentLinks.tsx b/src/features/comment/CommentLinks.tsx index 7d2d2e758b..7636265389 100644 --- a/src/features/comment/CommentLinks.tsx +++ b/src/features/comment/CommentLinks.tsx @@ -10,6 +10,7 @@ import { Text } from "mdast"; import { uniqBy } from "lodash"; import { isValidUrl } from "../../helpers/url"; import spoiler from "@aeharding/remark-lemmy-spoiler"; +import { buildBaseLemmyUrl } from "../../services/lemmy"; const Container = styled.div` display: flex; @@ -34,6 +35,7 @@ export default function CommentLinks({ markdown }: CommentLinksProps) { const connectedInstance = useAppSelector( (state) => state.auth.connectedInstance, ); + const connectedInstanceUrl = buildBaseLemmyUrl(connectedInstance); const links = useMemo(() => { // Initialize a unified processor with the remark-parse parser @@ -56,7 +58,7 @@ export default function CommentLinks({ markdown }: CommentLinksProps) { links.push({ type: node.type, // normalize relative links - url: new URL(node.url, `https://${connectedInstance}`).href, + url: new URL(node.url, connectedInstanceUrl).href, text: "children" in node ? (node.children[0] as Text)?.value : undefined, }); @@ -72,7 +74,7 @@ export default function CommentLinks({ markdown }: CommentLinksProps) { links = links.slice(0, 4); return links; - }, [markdown, showCommentImages, connectedInstance]); + }, [connectedInstance, markdown, showCommentImages, connectedInstanceUrl]); if (!links.length) return; diff --git a/src/features/feed/SpecialFeedMoreActions.tsx b/src/features/feed/SpecialFeedMoreActions.tsx index 9c1f11f25e..1db5b89de1 100644 --- a/src/features/feed/SpecialFeedMoreActions.tsx +++ b/src/features/feed/SpecialFeedMoreActions.tsx @@ -12,6 +12,7 @@ import { useSetPostAppearance, } from "../post/appearance/PostAppearanceProvider"; import { getShareIcon } from "../../helpers/device"; +import { buildBaseLemmyUrl } from "../../services/lemmy"; interface SpecialFeedMoreActionsProps { type: ListingType; @@ -40,10 +41,10 @@ export default function SpecialFeedMoreActions({ text: "Share", icon: getShareIcon(), handler: () => { - const url = urlSelector(store.getState()); + const url = buildBaseLemmyUrl(urlSelector(store.getState())); Share.share({ - url: `https://${url}?dataType=Post&listingType=${type}`, + url: `${url}?dataType=Post&listingType=${type}`, }); }, }, diff --git a/src/features/shared/markdown/LinkInterceptor.tsx b/src/features/shared/markdown/LinkInterceptor.tsx index 03e298a1df..928cacc1c1 100644 --- a/src/features/shared/markdown/LinkInterceptor.tsx +++ b/src/features/shared/markdown/LinkInterceptor.tsx @@ -3,6 +3,7 @@ import InAppExternalLink from "../InAppExternalLink"; import useLemmyUrlHandler from "../useLemmyUrlHandler"; import { useAppSelector } from "../../../store"; import { styled } from "@linaria/react"; +import { buildBaseLemmyUrl } from "../../../services/lemmy"; const LinkInterceptor = styled(LinkInterceptorUnstyled)` -webkit-touch-callout: default; @@ -23,8 +24,8 @@ function LinkInterceptorUnstyled({ forceResolveObject, ...props }: LinkInterceptorUnstyledProps) { - const connectedInstance = useAppSelector( - (state) => state.auth.connectedInstance, + const connectedInstanceUrl = useAppSelector((state) => + buildBaseLemmyUrl(state.auth.connectedInstance), ); const { redirectToLemmyObjectIfNeeded } = useLemmyUrlHandler(); @@ -32,11 +33,11 @@ function LinkInterceptorUnstyled({ if (!props.href) return; try { - return new URL(props.href, `https://${connectedInstance}`).href; + return new URL(props.href, connectedInstanceUrl).href; } catch (_) { return; } - }, [connectedInstance, props.href]); + }, [connectedInstanceUrl, props.href]); const onClick = useCallback( async (e: React.MouseEvent) => { diff --git a/src/features/shared/useLemmyUrlHandler.ts b/src/features/shared/useLemmyUrlHandler.ts index 387f1b2066..0664e6286c 100644 --- a/src/features/shared/useLemmyUrlHandler.ts +++ b/src/features/shared/useLemmyUrlHandler.ts @@ -8,6 +8,7 @@ import { MouseEvent } from "react"; import useAppToast from "../../helpers/useAppToast"; import { isLemmyError } from "../../helpers/lemmyErrors"; import { useOptimizedIonRouter } from "../../helpers/useOptimizedIonRouter"; +import { buildBaseLemmyUrl } from "../../services/lemmy"; export const POST_PATH = /^\/post\/(\d+)$/; @@ -43,6 +44,7 @@ export default function useLemmyUrlHandler() { const connectedInstance = useAppSelector( (state) => state.auth.connectedInstance, ); + const connectedInstanceUrl = buildBaseLemmyUrl(connectedInstance); const objectByUrl = useAppSelector((state) => state.resolve.objectByUrl); const { navigateToComment, @@ -162,12 +164,12 @@ export default function useLemmyUrlHandler() { const getUrl = useCallback( (link: string) => { try { - return new URL(link, `https://${connectedInstance}`); + return new URL(link, connectedInstanceUrl); } catch (error) { console.error("Error parsing url", error); } }, - [connectedInstance], + [connectedInstanceUrl], ); const determineObjectTypeFromUrl = useCallback( diff --git a/src/routes/common/ActorRedirect.tsx b/src/routes/common/ActorRedirect.tsx index 42a3d3eff4..ac82f52adb 100644 --- a/src/routes/common/ActorRedirect.tsx +++ b/src/routes/common/ActorRedirect.tsx @@ -33,7 +33,7 @@ function ActorRedirectEnabled({ children }: ActorRedirectProps) { const [first, second, _wrongActor, ...urlEnd] = location.pathname.split("/"); // no need to redirect if url doesn't have actor - if (!_wrongActor || !_wrongActor.includes(".")) return page; + if (!_wrongActor || !isPotentialActor(_wrongActor)) return page; return ( ); } + +function isPotentialActor(host: string) { + if (host.includes(".")) return true; + if (host.includes(":")) return true; + + if (host.startsWith("localhost")) return true; + + return false; +} diff --git a/src/services/app.ts b/src/services/app.ts index d958709b5e..65fdba8b16 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { isNative } from "../helpers/device"; import { isEqual } from "lodash"; -const DEFAULT_LEMMY_SERVERS = ["lemmy.world"]; +const DEFAULT_LEMMY_SERVERS = getCustomDefaultServers() ?? ["lemmy.world"]; let _customServers = DEFAULT_LEMMY_SERVERS; @@ -53,3 +53,11 @@ export default function ConfigProvider({ children }: ConfigProviderProps) { if (configLoaded) return children; } + +function getCustomDefaultServers() { + const serversList = import.meta.env.VITE_CUSTOM_LEMMY_SERVERS; + + if (!serversList) return; + + return serversList.split(","); +} diff --git a/src/services/lemmy.ts b/src/services/lemmy.ts index c5ab1addcd..98ed8c9b86 100644 --- a/src/services/lemmy.ts +++ b/src/services/lemmy.ts @@ -3,12 +3,16 @@ import { reduceFileSize } from "../helpers/imageCompress"; import { isNative, supportsWebp } from "../helpers/device"; import nativeFetch from "./nativeFetch"; -function buildBaseUrl(url: string): string { +export function buildBaseLemmyUrl(url: string): string { + if (import.meta.env.VITE_FORCE_LEMMY_INSECURE) { + return `http://${url}`; + } + return `https://${url}`; } export function getClient(url: string, jwt?: string): LemmyHttp { - return new LemmyHttp(buildBaseUrl(url), { + return new LemmyHttp(buildBaseLemmyUrl(url), { fetchFunction: isNative() ? nativeFetch : fetch.bind(globalThis), headers: jwt ? { From 8660f7af3cdfb8d971aedffe5327c273ed5420ce Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sat, 10 Aug 2024 19:50:02 -0500 Subject: [PATCH 05/12] Add filtered websites (#1578) Resolves #1236 --- src/features/feed/PostCommentFeed.tsx | 26 +++-- .../settings/blocks/FilteredWebsites.tsx | 104 ++++++++++++++++++ src/features/settings/settingsSlice.tsx | 34 ++++++ src/features/user/Profile.tsx | 2 +- src/helpers/lemmy.test.ts | 84 +++++++++++++- src/helpers/lemmy.ts | 21 ++++ .../profile/ProfileFeedHiddenPostsPage.tsx | 2 +- .../pages/profile/ProfileFeedItemsPage.tsx | 2 +- .../search/results/SearchFeedResultsPage.tsx | 2 +- .../pages/settings/BlocksSettingsPage.tsx | 4 +- src/routes/pages/shared/ModqueuePage.tsx | 2 +- src/services/db.ts | 1 + src/store.tsx | 2 + 13 files changed, 267 insertions(+), 19 deletions(-) create mode 100644 src/features/settings/blocks/FilteredWebsites.tsx diff --git a/src/features/feed/PostCommentFeed.tsx b/src/features/feed/PostCommentFeed.tsx index 08bf1b3e9c..2b808acc90 100644 --- a/src/features/feed/PostCommentFeed.tsx +++ b/src/features/feed/PostCommentFeed.tsx @@ -23,6 +23,7 @@ import { isComment, isPost, postHasFilteredKeywords, + postHasFilteredWebsite, } from "../../helpers/lemmy"; import { useAutohidePostIfNeeded } from "./PageTypeContext"; @@ -36,7 +37,7 @@ interface PostCommentFeed extends Omit, "renderItemContent"> { communityName?: string; filterHiddenPosts?: boolean; - filterKeywords?: boolean; + filterKeywordsAndWebsites?: boolean; header?: ReactElement; } @@ -44,7 +45,7 @@ interface PostCommentFeed export default function PostCommentFeed({ fetchFn: _fetchFn, filterHiddenPosts = true, - filterKeywords = true, + filterKeywordsAndWebsites = true, filterOnRxFn: _filterOnRxFn, filterFn: _filterFn, ...rest @@ -58,6 +59,9 @@ export default function PostCommentFeed({ const filteredKeywords = useAppSelector( (state) => state.settings.blocks.keywords, ); + const filteredWebsites = useAppSelector( + (state) => state.settings.blocks.websites, + ); const disableMarkingRead = useAppSelector( (state) => state.settings.general.posts.disableMarkingRead, @@ -152,14 +156,11 @@ export default function PostCommentFeed({ postHidden.hidden ) return false; - if ( - filterKeywords && - postHasFilteredKeywords( - item.post, - filterKeywords ? filteredKeywords : [], - ) - ) - return false; + + if (filterKeywordsAndWebsites) { + if (postHasFilteredKeywords(item.post, filteredKeywords)) return false; + if (postHasFilteredWebsite(item.post, filteredWebsites)) return false; + } if (_filterFn) return _filterFn(item); @@ -167,11 +168,12 @@ export default function PostCommentFeed({ }, [ postHiddenById, - filteredKeywords, - filterKeywords, filterHiddenPosts, + filterKeywordsAndWebsites, _filterFn, postDeletedById, + filteredKeywords, + filteredWebsites, ], ); diff --git a/src/features/settings/blocks/FilteredWebsites.tsx b/src/features/settings/blocks/FilteredWebsites.tsx new file mode 100644 index 0000000000..4bf3096d49 --- /dev/null +++ b/src/features/settings/blocks/FilteredWebsites.tsx @@ -0,0 +1,104 @@ +import { + IonItem, + IonItemOption, + IonItemOptions, + IonItemSliding, + IonLabel, + IonList, + useIonAlert, +} from "@ionic/react"; +import { useAppDispatch, useAppSelector } from "../../../store"; +import { ListHeader } from "../shared/formatting"; +import { updateFilteredWebsites } from "../settingsSlice"; +import { uniq, without } from "lodash"; +import { RemoveItemButton } from "../../shared/ListEditor"; +import { parseUrl } from "../../../helpers/url"; +import useAppToast from "../../../helpers/useAppToast"; +import { close } from "ionicons/icons"; + +export default function FilteredWebsites() { + const [presentAlert] = useIonAlert(); + const presentToast = useAppToast(); + const dispatch = useAppDispatch(); + const filteredWebsites = useAppSelector( + (state) => state.settings.blocks.websites, + ); + + async function remove(website: string) { + dispatch(updateFilteredWebsites(without(filteredWebsites, website))); + } + + async function add() { + presentAlert({ + message: "Add Filtered Website", + buttons: [ + { + text: "OK", + handler: ({ website }) => { + const cleanedWebsite = website.trim().toLowerCase(); + + if (!cleanedWebsite) return; + + const hasProtocol = /^https?:\/\//.test(cleanedWebsite); + const host = parseUrl( + `${hasProtocol ? "" : "https://"}${cleanedWebsite}`, + )?.host; + + if (!host || !host.includes(".")) { + presentToast({ + message: "Invalid website", + color: "danger", + centerText: true, + icon: close, + }); + return false; + } + + dispatch(updateFilteredWebsites(uniq([...filteredWebsites, host]))); + }, + }, + "Cancel", + ], + inputs: [ + { + placeholder: "example.org", + name: "website", + type: "url", + }, + ], + }); + } + + return ( + <> + + Filtered Websites + + + {filteredWebsites.map((website) => ( + + remove(website)}> + remove(website)} + > + Unfilter + + + + + {website} + + + ))} + + + + Add Website + + + + + ); +} diff --git a/src/features/settings/settingsSlice.tsx b/src/features/settings/settingsSlice.tsx index ac1664fcf3..d2f4d40912 100644 --- a/src/features/settings/settingsSlice.tsx +++ b/src/features/settings/settingsSlice.tsx @@ -142,6 +142,7 @@ interface SettingsState { }; blocks: { keywords: string[]; + websites: string[]; }; } @@ -229,6 +230,7 @@ export const initialState: SettingsState = { }, blocks: { keywords: [], + websites: [], }, }; @@ -372,6 +374,10 @@ export const appearanceSlice = createSlice({ state.blocks.keywords = action.payload; // Per user setting is updated in StoreProvider }, + setFilteredWebsites(state, action: PayloadAction) { + state.blocks.websites = action.payload; + // Per user setting is updated in StoreProvider + }, setDefaultFeed(state, action: PayloadAction) { state.general.defaultFeed = action.payload; // Per user setting is updated in StoreProvider @@ -589,6 +595,19 @@ export const getFilteredKeywords = ); }; +export const getFilteredWebsites = + () => async (dispatch: AppDispatch, getState: () => RootState) => { + const userHandle = getState().auth.accountData?.activeHandle; + + const filteredWebsites = await db.getSetting("filtered_websites", { + user_handle: userHandle, + }); + + dispatch( + setFilteredWebsites(filteredWebsites ?? initialState.blocks.websites), + ); + }; + export const getDefaultFeed = () => async (dispatch: AppDispatch, getState: () => RootState) => { const userHandle = getState().auth.accountData?.activeHandle; @@ -637,6 +656,18 @@ export const updateFilteredKeywords = }); }; +export const updateFilteredWebsites = + (filteredWebsites: string[]) => + async (dispatch: AppDispatch, getState: () => RootState) => { + const userHandle = getState().auth.accountData?.activeHandle; + + dispatch(setFilteredWebsites(filteredWebsites)); + + db.setSetting("filtered_websites", filteredWebsites, { + user_handle: userHandle, + }); + }; + export const fetchSettingsFromDatabase = createAsyncThunk( "appearance/fetchSettingsFromDatabase", async (_, thunkApi) => { @@ -711,6 +742,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk( const link_handler = await db.getSetting("link_handler"); const prefer_native_apps = await db.getSetting("prefer_native_apps"); const filtered_keywords = await db.getSetting("filtered_keywords"); + const filtered_websites = await db.getSetting("filtered_websites"); const touch_friendly_links = await db.getSetting("touch_friendly_links"); const show_comment_images = await db.getSetting("show_comment_images"); const show_collapsed_comment = await db.getSetting( @@ -879,6 +911,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk( }, blocks: { keywords: filtered_keywords ?? initialState.blocks.keywords, + websites: filtered_websites ?? initialState.blocks.websites, }, }; }); @@ -918,6 +951,7 @@ export const { setShowCommunityIcons, setCommunityAtTop, setFilteredKeywords, + setFilteredWebsites, setPostAppearance, setRememberPostAppearance, setThumbnailPosition, diff --git a/src/features/user/Profile.tsx b/src/features/user/Profile.tsx index c6025f67a9..faee4de541 100644 --- a/src/features/user/Profile.tsx +++ b/src/features/user/Profile.tsx @@ -137,7 +137,7 @@ export default function Profile({ person }: ProfileProps) { fetchFn={fetchFn} header={header} filterHiddenPosts={false} - filterKeywords={false} + filterKeywordsAndWebsites={false} /> ); } diff --git a/src/helpers/lemmy.test.ts b/src/helpers/lemmy.test.ts index 9b1330e4c0..1e08ca8607 100644 --- a/src/helpers/lemmy.test.ts +++ b/src/helpers/lemmy.test.ts @@ -1,5 +1,9 @@ import { Post } from "lemmy-js-client"; -import { buildCrosspostBody, keywordFoundInSentence } from "./lemmy"; +import { + buildCrosspostBody, + keywordFoundInSentence, + postHasFilteredWebsite, +} from "./lemmy"; describe("keywordFoundInSentence", () => { it("false when empty", () => { @@ -59,6 +63,84 @@ describe("keywordFoundInSentence", () => { }); }); +describe("postHasFilteredWebsite", () => { + it("false when empty", () => { + expect( + postHasFilteredWebsite({ url: "https://google.com" } as Post, []), + ).toBe(false); + }); + + it("false when no url", () => { + expect(postHasFilteredWebsite({} as Post, [])).toBe(false); + }); + + it("true when match", () => { + expect( + postHasFilteredWebsite({ url: "https://google.com" } as Post, [ + "google.com", + ]), + ).toBe(true); + }); + + it("true when match with path", () => { + expect( + postHasFilteredWebsite( + { url: "https://google.com/foo/bar?baz#test" } as Post, + ["google.com"], + ), + ).toBe(true); + }); + + it("true when match with multiple websites", () => { + expect( + postHasFilteredWebsite( + { url: "https://google.com/foo/bar?baz#test" } as Post, + ["test.com", "google.com"], + ), + ).toBe(true); + }); + + it("true with subdomain", () => { + expect( + postHasFilteredWebsite({ url: "https://www.google.com" } as Post, [ + "google.com", + ]), + ).toBe(true); + }); + + it("false when starts with", () => { + expect( + postHasFilteredWebsite({ url: "https://ggoogle.com" } as Post, [ + "google.com", + ]), + ).toBe(false); + }); + + it("true with multiple subdomains", () => { + expect( + postHasFilteredWebsite({ url: "https://www1.www2.google.com" } as Post, [ + "google.com", + ]), + ).toBe(true); + }); + + it("false on domain when filtering subdomain", () => { + expect( + postHasFilteredWebsite({ url: "https://google.com" } as Post, [ + "www.google.com", + ]), + ).toBe(false); + }); + + it("true on exact subdomain", () => { + expect( + postHasFilteredWebsite({ url: "https://hi.google.com" } as Post, [ + "hi.google.com", + ]), + ).toBe(true); + }); +}); + describe("buildCrosspostBody", () => { it("url only", () => { expect( diff --git a/src/helpers/lemmy.ts b/src/helpers/lemmy.ts index fe9a9735c8..6293f48ede 100644 --- a/src/helpers/lemmy.ts +++ b/src/helpers/lemmy.ts @@ -12,6 +12,7 @@ import { escapeStringForRegex } from "./regex"; import { quote } from "./markdown"; import { compare } from "compare-versions"; import { parseJWT } from "./jwt"; +import { parseUrl } from "./url"; export interface LemmyJWT { sub: number; @@ -255,6 +256,26 @@ export function postHasFilteredKeywords( return false; } +export function postHasFilteredWebsite( + post: Post, + websites: string[], +): boolean { + if (!post.url) return false; + + for (const website of websites) { + const postUrl = parseUrl(post.url); + if (!postUrl) continue; + + if ( + postUrl.hostname === website || + postUrl.hostname.endsWith(`.${website}`) // match subdomains + ) + return true; + } + + return false; +} + export function keywordFoundInSentence( keyword: string, sentence: string, diff --git a/src/routes/pages/profile/ProfileFeedHiddenPostsPage.tsx b/src/routes/pages/profile/ProfileFeedHiddenPostsPage.tsx index f9a96933b5..e6a73824c9 100644 --- a/src/routes/pages/profile/ProfileFeedHiddenPostsPage.tsx +++ b/src/routes/pages/profile/ProfileFeedHiddenPostsPage.tsx @@ -112,7 +112,7 @@ export default function ProfileFeedHiddenPostsPage() { fetchFn={fetchFn} limit={LIMIT} filterHiddenPosts={false} - filterKeywords={false} + filterKeywordsAndWebsites={false} /> diff --git a/src/routes/pages/profile/ProfileFeedItemsPage.tsx b/src/routes/pages/profile/ProfileFeedItemsPage.tsx index f4a22bea68..18cbd68687 100644 --- a/src/routes/pages/profile/ProfileFeedItemsPage.tsx +++ b/src/routes/pages/profile/ProfileFeedItemsPage.tsx @@ -88,7 +88,7 @@ export default function ProfileFeedItemsPage({ diff --git a/src/routes/pages/search/results/SearchFeedResultsPage.tsx b/src/routes/pages/search/results/SearchFeedResultsPage.tsx index ff07d77402..bf1f2beeb7 100644 --- a/src/routes/pages/search/results/SearchFeedResultsPage.tsx +++ b/src/routes/pages/search/results/SearchFeedResultsPage.tsx @@ -81,7 +81,7 @@ export default function SearchFeedResultsPage({ fetchFn={fetchFn} sortDuration={getSortDuration(sort)} filterHiddenPosts={false} - filterKeywords={false} + filterKeywordsAndWebsites={false} /> diff --git a/src/routes/pages/settings/BlocksSettingsPage.tsx b/src/routes/pages/settings/BlocksSettingsPage.tsx index 6f54995602..e6f7bb0296 100644 --- a/src/routes/pages/settings/BlocksSettingsPage.tsx +++ b/src/routes/pages/settings/BlocksSettingsPage.tsx @@ -28,6 +28,7 @@ import { TitleContainer, UsernameIonText, } from "../../../features/shared/markdown/editing/modal/contents/CommentReplyPage"; +import FilteredWebsites from "../../../features/settings/blocks/FilteredWebsites"; export default function BlocksSettingsPage() { const pageRef = useRef(null); @@ -56,10 +57,11 @@ export default function BlocksSettingsPage() { return ( - + + ); })(); diff --git a/src/routes/pages/shared/ModqueuePage.tsx b/src/routes/pages/shared/ModqueuePage.tsx index f68edad964..d3388da3fb 100644 --- a/src/routes/pages/shared/ModqueuePage.tsx +++ b/src/routes/pages/shared/ModqueuePage.tsx @@ -140,7 +140,7 @@ function ModqueueByCommunity({ community }: { community?: Community }) { diff --git a/src/services/db.ts b/src/services/db.ts index 87b9a48765..43c8d49a0e 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -337,6 +337,7 @@ export type SettingValueTypes = { jump_button_position: JumpButtonPositionType; tap_to_collapse: TapToCollapseType; filtered_keywords: string[]; + filtered_websites: string[]; highlight_new_account: boolean; default_feed: DefaultFeedType; touch_friendly_links: boolean; diff --git a/src/store.tsx b/src/store.tsx index 2885e1834e..6b6aab02de 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -19,6 +19,7 @@ import settingsSlice, { getBlurNsfw, getDefaultFeed, getFilteredKeywords, + getFilteredWebsites, } from "./features/settings/settingsSlice"; import gestureSlice, { fetchGesturesFromDatabase, @@ -104,6 +105,7 @@ const activeHandleChange = () => { store.dispatch(getFavoriteCommunities()); store.dispatch(getBlurNsfw()); store.dispatch(getFilteredKeywords()); + store.dispatch(getFilteredWebsites()); store.dispatch(getDefaultFeed()); store.dispatch(getInstances()); }; From 5e927bc77af9fea49fba069a012c8ed1166f1bda Mon Sep 17 00:00:00 2001 From: Aashu Anshuman Date: Sun, 11 Aug 2024 03:03:02 +0100 Subject: [PATCH 06/12] (chore) Add .editorconfig (#1579) --- .editorconfig | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..db5f973596 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.{js,ts,jsx,tsx,yaml,json}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true From b8f305ccfc68d66c4907a49df244d67087ec192f Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sat, 10 Aug 2024 22:03:24 -0500 Subject: [PATCH 07/12] Add persisted search sort (#1580) --- src/features/feed/helpers.ts | 11 +++++++++++ src/features/post/appearance/appearanceSlice.ts | 15 +++------------ .../pages/search/CommunitiesResultsPage.tsx | 6 +++++- .../search/results/SearchFeedResultsPage.tsx | 4 +++- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/features/feed/helpers.ts b/src/features/feed/helpers.ts index 63f9807822..eaebe79249 100644 --- a/src/features/feed/helpers.ts +++ b/src/features/feed/helpers.ts @@ -1,12 +1,21 @@ import { ListingType } from "lemmy-js-client"; import { getFeedUrlName } from "../community/mod/ModActions"; +type InternalFeedType = + | "PostsSearch" + | "CommentsSearch" + | "CommunitiesSearch" + | "CommunitiesExplore"; + export type AnyFeed = | { remoteCommunityHandle: string; } | { listingType: ListingType; + } + | { + internal: InternalFeedType; }; export function serializeFeedName(feed: AnyFeed): string { @@ -15,6 +24,8 @@ export function serializeFeedName(feed: AnyFeed): string { return feed.remoteCommunityHandle; // always contains @ - will never overlap with getFeedUrlName case "listingType" in feed: return getFeedUrlName(feed.listingType); + case "internal" in feed: + return `@@voyager_${feed.internal}`; default: return feed; } diff --git a/src/features/post/appearance/appearanceSlice.ts b/src/features/post/appearance/appearanceSlice.ts index 52337880e3..e3185dd89a 100644 --- a/src/features/post/appearance/appearanceSlice.ts +++ b/src/features/post/appearance/appearanceSlice.ts @@ -1,7 +1,6 @@ import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { ListingType } from "lemmy-js-client"; import { PostAppearanceType, db } from "../../../services/db"; -import { serializeFeedName } from "../../feed/helpers"; +import { AnyFeed, serializeFeedName } from "../../feed/helpers"; interface PostAppearanceState { /** @@ -21,7 +20,7 @@ export const postAppearanceSlice = createSlice({ setPostAppeartance: ( state, action: PayloadAction<{ - feed: FeedSortFeed; + feed: AnyFeed; postAppearance: PostAppearanceType; }>, ) => { @@ -47,17 +46,9 @@ export const { setPostAppeartance } = postAppearanceSlice.actions; export default postAppearanceSlice.reducer; -export type FeedSortFeed = - | { - remoteCommunityHandle: string; - } - | { - listingType: ListingType; - }; - export const getPostAppearance = createAsyncThunk( "postAppearance/getPostAppearance", - async (feed: FeedSortFeed) => { + async (feed: AnyFeed) => { const feedName = serializeFeedName(feed); const postAppearance = (await db.getSetting("post_appearance_type", { diff --git a/src/routes/pages/search/CommunitiesResultsPage.tsx b/src/routes/pages/search/CommunitiesResultsPage.tsx index ffc1311914..2d1d6eb76f 100644 --- a/src/routes/pages/search/CommunitiesResultsPage.tsx +++ b/src/routes/pages/search/CommunitiesResultsPage.tsx @@ -29,7 +29,11 @@ export default function CommunitiesResultsPage({ }: CommunitiesResultsPageProps) { const buildGeneralBrowseLink = useBuildGeneralBrowseLink(); const client = useClient(); - const [sort, setSort] = useFeedSort("posts", undefined, "TopAll"); + const [sort, setSort] = useFeedSort( + "posts", + { internal: search ? "CommunitiesSearch" : "CommunitiesExplore" }, + "TopAll", + ); const [listingType, setListingType] = useState("All"); const fetchFn: FetchFn = useCallback( diff --git a/src/routes/pages/search/results/SearchFeedResultsPage.tsx b/src/routes/pages/search/results/SearchFeedResultsPage.tsx index bf1f2beeb7..459057340d 100644 --- a/src/routes/pages/search/results/SearchFeedResultsPage.tsx +++ b/src/routes/pages/search/results/SearchFeedResultsPage.tsx @@ -37,7 +37,9 @@ export default function SearchFeedResultsPage({ }>(); const buildGeneralBrowseLink = useBuildGeneralBrowseLink(); const client = useClient(); - const [sort, setSort] = useFeedSort("posts"); + const [sort, setSort] = useFeedSort("posts", { + internal: `${type}Search`, + }); const search = decodeURIComponent(_encodedSearch); From 352c6701e60c89259b8877dcf783f156d197c035 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:37:34 -0500 Subject: [PATCH 08/12] Move subscribed icon setting to Appearance, increase icons size (#1582) --- src/features/labels/links/CommunityLink.tsx | 3 +-- src/features/settings/appearance/posts/Posts.tsx | 4 +++- .../other => appearance/posts}/SubscribedIcon.tsx | 2 +- src/features/settings/general/other/Other.tsx | 2 -- src/features/settings/settingsSlice.tsx | 10 +++++----- 5 files changed, 10 insertions(+), 11 deletions(-) rename src/features/settings/{general/other => appearance/posts}/SubscribedIcon.tsx (91%) diff --git a/src/features/labels/links/CommunityLink.tsx b/src/features/labels/links/CommunityLink.tsx index 7b63a98ead..27d3fa551b 100644 --- a/src/features/labels/links/CommunityLink.tsx +++ b/src/features/labels/links/CommunityLink.tsx @@ -33,7 +33,6 @@ const StyledItemIcon = styled(ItemIcon)` const SubscribedIcon = styled(IonIcon)` color: var(--ion-color-danger); vertical-align: middle; - font-size: 0.85em; margin-bottom: 1px; margin-left: 2px; @@ -163,7 +162,7 @@ const onStart: LongPressOptions["onStart"] = (e) => { function useShowSubscribedIcon() { const feedEnabled = useContext(ShowSubscribedIconContext); const subscribedIcon = useAppSelector( - (state) => state.settings.general.subscribedIcon, + (state) => state.settings.appearance.posts.subscribedIcon, ); switch (subscribedIcon) { diff --git a/src/features/settings/appearance/posts/Posts.tsx b/src/features/settings/appearance/posts/Posts.tsx index bad0289c24..e0a6245745 100644 --- a/src/features/settings/appearance/posts/Posts.tsx +++ b/src/features/settings/appearance/posts/Posts.tsx @@ -8,6 +8,7 @@ import EmbedExternalMedia from "./EmbedExternalMedia"; import AlwaysShowAuthor from "./AlwaysShowAuthor"; import RememberType from "./RememberType"; import CommunityAtTop from "./CommunityAtTop"; +import SubscribedIcon from "./SubscribedIcon"; export default function Posts() { return ( @@ -20,10 +21,11 @@ export default function Posts() { - + + ); diff --git a/src/features/settings/general/other/SubscribedIcon.tsx b/src/features/settings/appearance/posts/SubscribedIcon.tsx similarity index 91% rename from src/features/settings/general/other/SubscribedIcon.tsx rename to src/features/settings/appearance/posts/SubscribedIcon.tsx index 8bd3d7733b..417495f0cb 100644 --- a/src/features/settings/general/other/SubscribedIcon.tsx +++ b/src/features/settings/appearance/posts/SubscribedIcon.tsx @@ -6,7 +6,7 @@ import SettingSelector from "../../shared/SettingSelector"; export default function SubscribedIcon() { const subscribedIcon = useAppSelector( - (state) => state.settings.general.subscribedIcon, + (state) => state.settings.appearance.posts.subscribedIcon, ); return ( diff --git a/src/features/settings/general/other/Other.tsx b/src/features/settings/general/other/Other.tsx index a2acf6c6f6..1664099bc6 100644 --- a/src/features/settings/general/other/Other.tsx +++ b/src/features/settings/general/other/Other.tsx @@ -9,7 +9,6 @@ import OpenNativeApps from "./OpenNativeApps"; import ClearCache from "./ClearCache"; import BackupSettings from "./backup/BackupSettings"; import Thumbnailinator from "./Thumbnailinator"; -import SubscribedIcon from "./SubscribedIcon"; export default function Other() { return ( @@ -25,7 +24,6 @@ export default function Other() { - diff --git a/src/features/settings/settingsSlice.tsx b/src/features/settings/settingsSlice.tsx index d2f4d40912..0f6e3cd009 100644 --- a/src/features/settings/settingsSlice.tsx +++ b/src/features/settings/settingsSlice.tsx @@ -79,6 +79,7 @@ interface SettingsState { embedExternalMedia: boolean; alwaysShowAuthor: boolean; communityAtTop: boolean; + subscribedIcon: ShowSubscribedIcon; }; large: { showVotingButtons: boolean; @@ -138,7 +139,6 @@ interface SettingsState { defaultFeed: DefaultFeedType | undefined; noSubscribedInFeed: boolean; thumbnailinatorEnabled: boolean; - subscribedIcon: ShowSubscribedIcon; }; blocks: { keywords: string[]; @@ -167,6 +167,7 @@ export const initialState: SettingsState = { embedExternalMedia: true, alwaysShowAuthor: false, communityAtTop: false, + subscribedIcon: OShowSubscribedIcon.Never, }, large: { showVotingButtons: true, @@ -226,7 +227,6 @@ export const initialState: SettingsState = { defaultFeed: undefined, noSubscribedInFeed: false, thumbnailinatorEnabled: true, - subscribedIcon: OShowSubscribedIcon.Never, }, blocks: { keywords: [], @@ -543,7 +543,7 @@ export const appearanceSlice = createSlice({ db.setSetting("prefer_native_apps", action.payload); }, setSubscribedIcon(state, action: PayloadAction) { - state.general.subscribedIcon = action.payload; + state.appearance.posts.subscribedIcon = action.payload; db.setSetting("subscribed_icon", action.payload); }, @@ -805,6 +805,8 @@ export const fetchSettingsFromDatabase = createAsyncThunk( initialState.appearance.posts.alwaysShowAuthor, communityAtTop: community_at_top ?? initialState.appearance.posts.communityAtTop, + subscribedIcon: + subscribed_icon ?? initialState.appearance.posts.subscribedIcon, }, large: { showVotingButtons: @@ -906,8 +908,6 @@ export const fetchSettingsFromDatabase = createAsyncThunk( thumbnailinatorEnabled: thumbnailinator_enabled ?? initialState.general.thumbnailinatorEnabled, - subscribedIcon: - subscribed_icon ?? initialState.general.subscribedIcon, }, blocks: { keywords: filtered_keywords ?? initialState.blocks.keywords, From 381cba7ac50beecb869491728222de3e92316e32 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:13:04 -0500 Subject: [PATCH 09/12] (chore) Update README instances list (#1583) --- README.md | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8b17d7fcaa..58633cd963 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,32 @@ Native apps can be great, but we believe in the strengths of the web. Why use a ## Deployment -### Official Deployment +### Ecosystem + +**For most users,** we recommend the native app available in the stores: + +   +   -The Voyager team maintains a deployment at: +However, if you prefer using the Progressive Web App, there are a number of folks that host it: + +| Country | URL | Description | Contact/Privacy | +| ---------------- | -------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------- | +| 🇺🇸 Virginia, USA | [vger.app](https://vger.app) | ✅ Official Voyager PWA instance | [➡️](https://vger.social/u/aeharding) | +| 🇫🇮 Finland | [m.lemmy.world](https://m.lemmy.world) | Voyager hosted by the mastodon.world team. | [➡️](https://mastodon.world/about) | +| 🇸🇬 Singapore | [v.opnxng.com](https://v.opnxng.com) | Voyager hosted by Opnxng in Singapore. | [➡️](https://about.opnxng.com) | +| 🇲🇽 Mexico | [voyager.nohost.network](https://voyager.nohost.network) | Voyager hosted by Nohost in Mexico. | [➡️](https://nohost.network) | +| 🇺🇸 USA | [vger.thesanewriter.com](https://vger.thesanewriter.com) | Voyager hosted by the lemmy.thesanewriter.com team. | [➡️](https://lemmy.thesanewriter.com/legal) | +| 🇺🇸 Oregon, USA | [m.lemmy.today](https://m.lemmy.today) | A fast, general purpose instance located in Oregon, USA | [➡️](https://lemmy.today) | +| 🇫🇷 France | [v.lemmy.dbzer0.com](https://v.lemmy.dbzer0.com) | Be Weird, Download a Car, Generate Art, Screw Copyrights | [➡️](https://lemmy.dbzer0.com) | +| ? | [v.programming.dev](https://v.programming.dev) | A collection of programming communities | [➡️](https://legal.programming.dev) | +| 🇺🇸 Oregon, USA | [v.lemmy.eco.br](https://v.lemmy.eco.br) | A fast, general purpose instance located in Oregon, USA | [➡️](https://lemmy.eco.br) | +| 🇩🇪 Germany | [v.lemy.lol](https://v.lemy.lol) | Long-term, general purpose Lemmy instance. | [➡️](https://lemy.lol) | +| 🇵🇹 Portugal | [voyager.slrpnk.net](https://voyager.slrpnk.net) | Sustainable future interconnected with nature and community. | [➡️](https://slrpnk.net) | +| ? | [app.thelemmy.club](https://app.thelemmy.club) | A general Lemmy instance for all! | [➡️](https://thelemmy.club) | +| ? | [m.lemdro.id](https://m.lemdro.id) | Fully open source instance with incredible transparency. | [➡️](https://lemdro.id) | -- 🐭 Production: [vger.app](https://vger.app) +> **Note**: Community deployments are **NOT** maintained by the Voyager team. They may not be synced with Voyager's source code. Please do your own research about the host servers before using them. ### Self-Host @@ -132,15 +153,6 @@ Optionally, you can serve a custom list of instance(s) in the `/_config` endpoin For production, serve `index.html` with `Cache-Control: no-cache` and `/assets` with a long cache period (files in assets are immutable) -### Ecosystem - -- 🇫🇮 [m.lemmy.world](https://m.lemmy.world) - Voyager hosted by the mastodon.world team. [Contact/privacy](https://mastodon.world/about) -- 🇸🇬 [v.opnxng.com](https://v.opnxng.com) - Voyager hosted by Opnxng in Singapore. [Contact/privacy](https://about.opnxng.com) -- 🇲🇽 [voyager.nohost.network](https://voyager.nohost.network) - Voyager hosted by Nohost in Mexico. [Contact/privacy](https://nohost.network) -- 🇺🇸 [vger.thesanewriter.com](https://vger.thesanewriter.com) - Voyager hosted by the lemmy.thesanewriter.com team. [Contact/privacy](https://lemmy.thesanewriter.com/legal) - -> **Note**: Community deployments are **NOT** maintained by the Voyager team. They may not be synced with Voyager's source code. Please do your own research about the host servers before using them. - ## 💖 Sponsors If you're enjoying Voyager, you can sponsor it: From 3a25bf55aac41a29fe4557713f73da5b4e9d0973 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sun, 11 Aug 2024 17:07:33 -0500 Subject: [PATCH 10/12] Hide subscribed icon on community results feed, use primary color (#1584) --- src/features/community/CommunitySummary.tsx | 1 + src/features/labels/links/CommunityLink.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/features/community/CommunitySummary.tsx b/src/features/community/CommunitySummary.tsx index 7383ee7ade..764724dc3c 100644 --- a/src/features/community/CommunitySummary.tsx +++ b/src/features/community/CommunitySummary.tsx @@ -83,6 +83,7 @@ export default function CommunitySummary({ community }: CommunitySummaryProps) { community={community.community} showInstanceWhenRemote subscribed={community.subscribed} + hideSubscribed /> {instance} - {showSubscribed && isSubscribed && } + {showSubscribed && !hideSubscribed && isSubscribed && ( + + )} ); From 7e364469ade21ed284ec147505951dfe5808582f Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:47:31 -0500 Subject: [PATCH 11/12] Fix error message on lemm.ee image upload (#1586) --- src/services/lemmy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/lemmy.ts b/src/services/lemmy.ts index 98ed8c9b86..65ed6e202b 100644 --- a/src/services/lemmy.ts +++ b/src/services/lemmy.ts @@ -50,7 +50,9 @@ export async function _uploadImage(client: LemmyHttp, image: File) { image: compressedImageIfNeeded as File, }); - if (!response.url) throw new Error(response.msg); + // lemm.ee uses response.message for error messages (e.g. account too new) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!response.url) throw new Error(response.msg ?? (response as any).message); return response; } From 6556c0126aa25993a3718d055214fea9b3e21093 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:16:12 -0500 Subject: [PATCH 12/12] Release 2.17.0 --- android/app/build.gradle | 4 ++-- ios/App/App/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c23c413a09..9f3d8129d3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "app.vger.voyager" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 259 - versionName "2.16.0" + versionCode 260 + versionName "2.17.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 4a30bf92d3..5aad4b9235 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.16.0 + 2.17.0 CFBundleURLTypes @@ -32,7 +32,7 @@ CFBundleVersion - 259 + 260 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/package.json b/package.json index e3e49c088e..f2e0b5e92f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "voyager", "description": "A progressive webapp Lemmy client", "private": true, - "version": "2.16.0", + "version": "2.17.0", "type": "module", "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e", "scripts": {