diff --git a/android/app/build.gradle b/android/app/build.gradle index f291b307a..af69ad9af 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 251 - versionName "2.12.1" + versionCode 254 + versionName "2.12.4" 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/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index a3b6dff7b..886a8a24c 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -48,7 +48,7 @@ include ':capacitor-plugin-safe-area' project(':capacitor-plugin-safe-area').projectDir = new File('../node_modules/.pnpm/capacitor-plugin-safe-area@3.0.1_@capacitor+core@6.0.0/node_modules/capacitor-plugin-safe-area/android') include ':capacitor-stash-media' -project(':capacitor-stash-media').projectDir = new File('../node_modules/.pnpm/capacitor-stash-media@2.0.0_@capacitor+core@6.0.0/node_modules/capacitor-stash-media/android') +project(':capacitor-stash-media').projectDir = new File('../node_modules/.pnpm/capacitor-stash-media@2.0.1_@capacitor+core@6.0.0/node_modules/capacitor-stash-media/android') include ':capacitor-tips' project(':capacitor-tips').projectDir = new File('../node_modules/.pnpm/capacitor-tips@1.0.0_@capacitor+core@6.0.0/node_modules/capacitor-tips/android') diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 3a5a45644..1353988e1 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.12.1 + 2.12.4 CFBundleURLTypes @@ -32,7 +32,7 @@ CFBundleVersion - 251 + 254 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/App/Podfile b/ios/App/Podfile index 03858de6a..cdad668ef 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -26,7 +26,7 @@ def capacitor_pods pod 'CapacitorClearCache', :path => '../../node_modules/.pnpm/capacitor-clear-cache@1.0.1_@capacitor+core@6.0.0/node_modules/capacitor-clear-cache' pod 'CapacitorLaunchNative', :path => '../../node_modules/.pnpm/capacitor-launch-native@1.0.0_@capacitor+core@6.0.0/node_modules/capacitor-launch-native' pod 'CapacitorPluginSafeArea', :path => '../../node_modules/.pnpm/capacitor-plugin-safe-area@3.0.1_@capacitor+core@6.0.0/node_modules/capacitor-plugin-safe-area' - pod 'CapacitorStashMedia', :path => '../../node_modules/.pnpm/capacitor-stash-media@2.0.0_@capacitor+core@6.0.0/node_modules/capacitor-stash-media' + pod 'CapacitorStashMedia', :path => '../../node_modules/.pnpm/capacitor-stash-media@2.0.1_@capacitor+core@6.0.0/node_modules/capacitor-stash-media' pod 'CapacitorTips', :path => '../../node_modules/.pnpm/capacitor-tips@1.0.0_@capacitor+core@6.0.0/node_modules/capacitor-tips' end diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index cb046e747..3b22a9b25 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -30,7 +30,7 @@ PODS: - Capacitor - CapacitorShare (6.0.0): - Capacitor - - CapacitorStashMedia (2.0.0): + - CapacitorStashMedia (2.0.1): - Capacitor - SDWebImage - CapacitorStatusBar (6.0.0): @@ -58,7 +58,7 @@ DEPENDENCIES: - "CapacitorNetwork (from `../../node_modules/.pnpm/@capacitor+network@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/network`)" - "CapacitorPluginSafeArea (from `../../node_modules/.pnpm/capacitor-plugin-safe-area@3.0.1_@capacitor+core@6.0.0/node_modules/capacitor-plugin-safe-area`)" - "CapacitorShare (from `../../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share`)" - - "CapacitorStashMedia (from `../../node_modules/.pnpm/capacitor-stash-media@2.0.0_@capacitor+core@6.0.0/node_modules/capacitor-stash-media`)" + - "CapacitorStashMedia (from `../../node_modules/.pnpm/capacitor-stash-media@2.0.1_@capacitor+core@6.0.0/node_modules/capacitor-stash-media`)" - "CapacitorStatusBar (from `../../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar`)" - "CapacitorTips (from `../../node_modules/.pnpm/capacitor-tips@1.0.0_@capacitor+core@6.0.0/node_modules/capacitor-tips`)" @@ -100,7 +100,7 @@ EXTERNAL SOURCES: CapacitorShare: :path: "../../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share" CapacitorStashMedia: - :path: "../../node_modules/.pnpm/capacitor-stash-media@2.0.0_@capacitor+core@6.0.0/node_modules/capacitor-stash-media" + :path: "../../node_modules/.pnpm/capacitor-stash-media@2.0.1_@capacitor+core@6.0.0/node_modules/capacitor-stash-media" CapacitorStatusBar: :path: "../../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar" CapacitorTips: @@ -123,11 +123,11 @@ SPEC CHECKSUMS: CapacitorNetwork: f15a94c16a33cba7c47a17814cb6bcfe3ea34ded CapacitorPluginSafeArea: b99681e1d9986ae1c059ca0e1b5e7025916e5e04 CapacitorShare: a771200d3b924a5d7ad9d9fecbac517e4c0aa74f - CapacitorStashMedia: a9038a69ab186f5d172536791887ef67fb2a39f8 + CapacitorStashMedia: 10aa96dc5f874c4c27642528a4c327c46792abf2 CapacitorStatusBar: 2e4369f99166125435641b1908d05f561eaba6f6 CapacitorTips: 2087733aea06ec041b210085395ca934c8554907 SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 -PODFILE CHECKSUM: 91325504efbeb1b038121b8724e8025b9f2fa11f +PODFILE CHECKSUM: 28718eb4e9f122216fc11cbe2c95ad57524fee30 COCOAPODS: 1.12.1 diff --git a/package.json b/package.json index 1ab20c732..4e573fee0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "voyager", "description": "A progressive webapp Lemmy client", "private": true, - "version": "2.12.1", + "version": "2.12.4", "type": "module", "packageManager": "pnpm@9.1.4+sha512.9df9cf27c91715646c7d675d1c9c8e41f6fce88246f1318c1aa6a1ed1aeb3c4f032fcdf4ba63cc69c4fe6d634279176b5358727d8f2cc1e65b65f43ce2f8bfb0", "scripts": { @@ -59,7 +59,7 @@ "capacitor-launch-native": "^1.0.0", "capacitor-plugin-safe-area": "^3.0.1", "capacitor-set-version": "^2.2.0", - "capacitor-stash-media": "^2.0.0", + "capacitor-stash-media": "^2.0.1", "capacitor-tips": "^1.0.0", "compare-versions": "^6.1.0", "date-fns": "^3.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbfeae06f..058a69c76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,8 +104,8 @@ importers: specifier: ^2.2.0 version: 2.2.0(@types/node@20.12.13)(typescript@5.4.5) capacitor-stash-media: - specifier: ^2.0.0 - version: 2.0.0(@capacitor/core@6.0.0) + specifier: ^2.0.1 + version: 2.0.1(@capacitor/core@6.0.0) capacitor-tips: specifier: ^1.0.0 version: 1.0.0(@capacitor/core@6.0.0) @@ -370,7 +370,7 @@ importers: version: 0.20.0(vite@5.2.12(@types/node@20.12.13)(terser@5.31.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(rollup@4.18.0)(typescript@5.4.5)(vite@5.2.12(@types/node@20.12.13)(terser@5.31.0)) + version: 4.2.0(rollup@2.79.1)(typescript@5.4.5)(vite@5.2.12(@types/node@20.12.13)(terser@5.31.0)) vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@20.12.13)(happy-dom@14.12.0)(jsdom@24.1.0)(terser@5.31.0) @@ -2525,8 +2525,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - capacitor-stash-media@2.0.0: - resolution: {integrity: sha512-UrX3fHpOddKdukXW1be5SBH5iKbSyWJPjXRpNnwPNl/AduJ9jRH+ZFQ9YSp3uLCVTdeGdJ7AAgJ1dL++WL63HQ==} + capacitor-stash-media@2.0.1: + resolution: {integrity: sha512-sDJsQJVk0DCM+rNfEpe51u3TRE0fHF7Los5F6IyLcbB+VmAn4RtTTuF6BRM4YWsLOC9REnZsDl2k6IMlOLNAZw==} peerDependencies: '@capacitor/core': ^6.0.0 @@ -8576,14 +8576,6 @@ snapshots: optionalDependencies: rollup: 2.79.1 - '@rollup/pluginutils@5.1.0(rollup@4.18.0)': - dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 4.18.0 - '@rollup/rollup-android-arm-eabi@4.18.0': optional: true @@ -9614,7 +9606,7 @@ snapshots: - supports-color - typescript - capacitor-stash-media@2.0.0(@capacitor/core@6.0.0): + capacitor-stash-media@2.0.1(@capacitor/core@6.0.0): dependencies: '@capacitor/core': 6.0.0 @@ -14419,9 +14411,9 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-svgr@4.2.0(rollup@4.18.0)(typescript@5.4.5)(vite@5.2.12(@types/node@20.12.13)(terser@5.31.0)): + vite-plugin-svgr@4.2.0(rollup@2.79.1)(typescript@5.4.5)(vite@5.2.12(@types/node@20.12.13)(terser@5.31.0)): dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.18.0) + '@rollup/pluginutils': 5.1.0(rollup@2.79.1) '@svgr/core': 8.1.0(typescript@5.4.5) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.4.5)) vite: 5.2.12(@types/node@20.12.13)(terser@5.31.0) diff --git a/src/core/Auth.tsx b/src/core/Auth.tsx index 74464c2a6..6da2d9b9e 100644 --- a/src/core/Auth.tsx +++ b/src/core/Auth.tsx @@ -6,8 +6,6 @@ import { getInboxCounts, syncMessages } from "../features/inbox/inboxSlice"; import { useInterval } from "usehooks-ts"; import usePageVisibility from "../helpers/usePageVisibility"; import { getDefaultServer } from "../services/app"; -import { isLemmyError } from "../helpers/lemmyErrors"; -import useAppToast from "../helpers/useAppToast"; import BackgroundReportSync from "../features/moderation/BackgroundReportSync"; import { getSiteIfNeeded, isAdminSelector } from "../features/auth/siteSlice"; import { instanceSelector, jwtSelector } from "../features/auth/authSelectors"; @@ -42,7 +40,6 @@ function AuthLocation() { const location = useLocation(); const dispatch = useAppDispatch(); - const presentToast = useAppToast(); const pageVisibility = usePageVisibility(); const jwt = useAppSelector(jwtSelector); @@ -82,26 +79,6 @@ function AuthLocation() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]); - const getInboxCountsAndErrorIfNeeded = useCallback(async () => { - try { - await dispatch(getInboxCounts()); - } catch (error) { - if ( - isLemmyError(error, "not_logged_in") || - isLemmyError(error, "incorrect_login") - ) { - presentToast({ - message: "Logged out by Lemmy instance. Please try logging in again.", - color: "warning", - duration: 4_000, - }); - } - - throw error; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dispatch]); - useInterval( () => { if (!pageVisibility) return; @@ -116,14 +93,14 @@ function AuthLocation() { if (!pageVisibility) return; if (!jwt) return; - getInboxCountsAndErrorIfNeeded(); + dispatch(getInboxCounts()); }, 1_000 * 60); useEffect(() => { if (!pageVisibility) return; - getInboxCountsAndErrorIfNeeded(); - }, [pageVisibility, jwt, getInboxCountsAndErrorIfNeeded]); + dispatch(getInboxCounts()); + }, [pageVisibility, jwt, dispatch]); useEffect(() => { if (!pageVisibility) return; diff --git a/src/features/auth/login/login/PickLoginServer.tsx b/src/features/auth/login/login/PickLoginServer.tsx index d28b588e9..06a036a49 100644 --- a/src/features/auth/login/login/PickLoginServer.tsx +++ b/src/features/auth/login/login/PickLoginServer.tsx @@ -77,22 +77,22 @@ export default function PickLoginServer() { async function submit() { if (loading) return; - setLoading(true); - const potentialServer = searchHostname.toLowerCase(); + // Dirty input with candidate + if (instances[0] && search !== potentialServer) { + setDirty(false); + setSearch(instances[0]); + return; + } + + setLoading(true); + let site: GetSiteResponse; try { site = await getClient(potentialServer).getSite(); } catch (error) { - // Dirty input with candidate - if (instances[0]) { - setDirty(false); - setSearch(instances[0]); - return; - } - presentToast({ message: `Problem connecting to ${potentialServer}. Please try again`, color: "danger", diff --git a/src/features/comment/useCommentActions.ts b/src/features/comment/useCommentActions.ts index 27a0d3238..609b98d51 100644 --- a/src/features/comment/useCommentActions.ts +++ b/src/features/comment/useCommentActions.ts @@ -232,7 +232,7 @@ export default function useCommentActions({ handler: () => { (async () => { if (presentLoginIfNeeded()) return; - if (commentView.post.locked) { + if (commentView.post.locked && !canModerate) { presentToast(postLocked); return; } diff --git a/src/features/inbox/inboxSlice.ts b/src/features/inbox/inboxSlice.ts index 2058842f5..067b38968 100644 --- a/src/features/inbox/inboxSlice.ts +++ b/src/features/inbox/inboxSlice.ts @@ -1,16 +1,10 @@ import { PayloadAction, createSelector, createSlice } from "@reduxjs/toolkit"; import { GetUnreadCountResponse, PrivateMessageView } from "lemmy-js-client"; import { AppDispatch, RootState } from "../../store"; -import { logoutAccount } from "../auth/authSlice"; import { InboxItemView } from "./InboxItem"; import { differenceBy, groupBy, sortBy, uniqBy } from "lodash"; import { receivedUsers } from "../user/userSlice"; -import { isLemmyError } from "../../helpers/lemmyErrors"; -import { - clientSelector, - userHandleSelector, - jwtSelector, -} from "../auth/authSelectors"; +import { clientSelector, jwtSelector } from "../auth/authSelectors"; interface PostState { counts: { @@ -118,31 +112,7 @@ export const getInboxCounts = if (Date.now() - lastUpdatedCounts < 3_000) return; - let result; - const initialHandle = userHandleSelector(getState()); - - try { - result = await clientSelector(getState()).getUnreadCount(); - } catch (error) { - // Get inbox counts is a good place to check if token is valid, - // because it runs quite often (when returning from background, - // every 60 seconds, etc) - // - // If API rejects jwt, check if initial handle used to make the request - // is the same as the handle at this moment (e.g. something else didn't - // log the user out). If match, then proceed to log the user out - if ( - isLemmyError(error, "not_logged_in") || - isLemmyError(error, "incorrect_login") - ) { - const handle = userHandleSelector(getState()); - if (handle && handle === initialHandle) { - dispatch(logoutAccount(handle)); - } - } - - throw error; - } + const result = await clientSelector(getState()).getUnreadCount(); if (result) dispatch(receivedInboxCounts(result)); }; diff --git a/src/features/post/crosspost/create/CreateCrosspostDialog.tsx b/src/features/post/crosspost/create/CreateCrosspostDialog.tsx index 16ab3f4ea..b3322cd2e 100644 --- a/src/features/post/crosspost/create/CreateCrosspostDialog.tsx +++ b/src/features/post/crosspost/create/CreateCrosspostDialog.tsx @@ -115,7 +115,7 @@ export default function CreateCrosspostDialog({ name: title, url: post.post.url, nsfw: post.post.nsfw, - body: buildCrosspostBody(post.post), + body: buildCrosspostBody(post.post, title !== post.post.name), community_id: community.community.id, }); } catch (error) { diff --git a/src/features/post/detail/PostHeader.tsx b/src/features/post/detail/PostHeader.tsx index 27f1b3bf1..ecf994594 100644 --- a/src/features/post/detail/PostHeader.tsx +++ b/src/features/post/detail/PostHeader.tsx @@ -30,6 +30,7 @@ import AnimateHeight from "react-animate-height"; import useIsPostUrlMedia from "../useIsPostUrlMedia"; import { findIonContentScrollView } from "../../../helpers/ionic"; import PostLink from "../link/PostLink"; +import { getCanModerate } from "../../moderation/useCanModerate"; const BorderlessIonItem = styled(IonItem)` --padding-start: 0; @@ -283,7 +284,10 @@ function PostHeader({ post={post} onReply={async () => { if (presentLoginIfNeeded()) return; - if (post.post.locked) { + + const canModerate = getCanModerate(post.community); + + if (post.post.locked && !canModerate) { presentToast(postLocked); return; } diff --git a/src/features/post/shared/usePostActions.tsx b/src/features/post/shared/usePostActions.tsx index c4fbce016..67d94fa83 100644 --- a/src/features/post/shared/usePostActions.tsx +++ b/src/features/post/shared/usePostActions.tsx @@ -198,7 +198,7 @@ export default function usePostActions(post: PostView) { icon: arrowUndoOutline, handler: () => { if (presentLoginIfNeeded()) return; - if (post.post.locked) { + if (post.post.locked && !canModerate) { presentToast(postLocked); return; } diff --git a/src/features/shared/sliding/BaseSlidingVote.tsx b/src/features/shared/sliding/BaseSlidingVote.tsx index f4463bce4..b877bb714 100644 --- a/src/features/shared/sliding/BaseSlidingVote.tsx +++ b/src/features/shared/sliding/BaseSlidingVote.tsx @@ -172,12 +172,14 @@ function BaseSlidingVoteInternal({ if (isInboxItem(item)) dispatch(markRead(item, true)); + const canModerate = getCanModerate(item.community); + // Prevent replying to a comment that's been deleted, or removed by mod (if you're not a mod) if (!isPost) { const comment = store.getState().comment.commentById[item.comment.id] ?? item.comment; - const stub = isStubComment(comment, getCanModerate(item.community)); + const stub = isStubComment(comment, canModerate); if (stub) { presentToast(replyStubError); @@ -185,7 +187,7 @@ function BaseSlidingVoteInternal({ } } - if (item.post.locked) { + if (item.post.locked && !canModerate) { presentToast(postLocked); return; } diff --git a/src/helpers/lemmy.test.ts b/src/helpers/lemmy.test.ts index 894b1cfd3..9b1330e4c 100644 --- a/src/helpers/lemmy.test.ts +++ b/src/helpers/lemmy.test.ts @@ -1,4 +1,5 @@ -import { keywordFoundInSentence } from "./lemmy"; +import { Post } from "lemmy-js-client"; +import { buildCrosspostBody, keywordFoundInSentence } from "./lemmy"; describe("keywordFoundInSentence", () => { it("false when empty", () => { @@ -57,3 +58,84 @@ describe("keywordFoundInSentence", () => { ); }); }); + +describe("buildCrosspostBody", () => { + it("url only", () => { + expect( + buildCrosspostBody( + { name: "Hi there", ap_id: "https://a.com/post/123" } as Post, + false, + ), + ).toBe("cross-posted from: https://a.com/post/123"); + }); + + it("with title", () => { + expect( + buildCrosspostBody( + { name: "Hi there", ap_id: "https://a.com/post/123" } as Post, + true, + ), + ).toBe( + ` +cross-posted from: https://a.com/post/123 + +> Hi there`.trim(), + ); + }); + + it("with title and body", () => { + expect( + buildCrosspostBody( + { + name: "Hi there", + ap_id: "https://a.com/post/123", + body: "Test", + } as Post, + true, + ), + ).toBe( + ` +cross-posted from: https://a.com/post/123 + +> Hi there +> +> Test`.trim(), + ); + }); + + it("with body only", () => { + expect( + buildCrosspostBody( + { + name: "Hi there", + ap_id: "https://a.com/post/123", + body: "Test", + } as Post, + false, + ), + ).toBe( + ` +cross-posted from: https://a.com/post/123 + +> Test`.trim(), + ); + }); + + it("trims body", () => { + expect( + buildCrosspostBody( + { + name: "Hi there", + ap_id: "https://a.com/post/123", + body: "Test\n", + } as Post, + false, + ), + ).toBe( + ` +cross-posted from: https://a.com/post/123 + +> Test`.trim(), + ); + }); +}); diff --git a/src/helpers/lemmy.ts b/src/helpers/lemmy.ts index 346fe8b71..335f7f518 100644 --- a/src/helpers/lemmy.ts +++ b/src/helpers/lemmy.ts @@ -291,12 +291,20 @@ export function getCrosspostUrl(post: Post): string | undefined { return matches?.[1]; } -export function buildCrosspostBody(post: Post): string { - const header = `cross-posted from: ${post.ap_id}\n\n${quote(post.name)}`; +export function buildCrosspostBody(post: Post, includeTitle = true): string { + let header = `cross-posted from: ${post.ap_id}`; + + if (includeTitle) { + header += `\n\n${quote(post.name)}`; + } if (!post.body) return header; - return `${header}\n>\n${quote(post.body)}`; + header += `\n${includeTitle ? ">" : ""}\n`; + + header += quote(post.body.trim()); + + return header; } export function isPost(item: PostView | CommentView): item is PostView {