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 {