Skip to content

Commit

Permalink
Add keyword block (#755)
Browse files Browse the repository at this point in the history
* Add keyword block

* Only block on community/home/all feeds

* Add test suite
  • Loading branch information
aeharding authored Oct 10, 2023
1 parent 062df5f commit 3435166
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 4 deletions.
15 changes: 13 additions & 2 deletions src/features/feed/PostCommentFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { receivedComments } from "../comment/commentSlice";
import Post from "../post/inFeed/Post";
import CommentHr from "../comment/CommentHr";
import { FeedContext } from "./FeedContext";
import { postHasFilteredKeywords } from "../../helpers/lemmy";

const thickBorderCss = css`
border-bottom: 8px solid var(--thick-separator-color);
Expand All @@ -28,19 +29,24 @@ interface PostCommentFeed
extends Omit<FeedProps<PostCommentItem>, "renderItemContent"> {
communityName?: string;
filterHiddenPosts?: boolean;
filterKeywords?: boolean;
}

export default function PostCommentFeed({
communityName,
fetchFn: _fetchFn,
filterHiddenPosts = true,
filterKeywords = true,
...rest
}: PostCommentFeed) {
const dispatch = useAppDispatch();
const postAppearanceType = useAppSelector(
(state) => state.settings.appearance.posts.type,
);
const postHiddenById = useAppSelector(postHiddenByIdSelector);
const filteredKeywords = useAppSelector(
(state) => state.settings.blocks.keywords,
);

const itemsRef = useRef<PostCommentItem[]>();

Expand Down Expand Up @@ -107,8 +113,13 @@ export default function PostCommentFeed({
);

const filterFn = useCallback(
(item: PostCommentItem) => !postHiddenById[item.post.id],
[postHiddenById],
(item: PostCommentItem) =>
!postHiddenById[item.post.id] &&
!postHasFilteredKeywords(
item.post,
filterKeywords ? filteredKeywords : [],
),
[postHiddenById, filteredKeywords, filterKeywords],
);

const getIndex = useCallback(
Expand Down
82 changes: 82 additions & 0 deletions src/features/settings/blocks/FilteredKeywords.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
IonItemOption,
IonItemOptions,
IonItemSliding,
IonLabel,
IonList,
useIonAlert,
} from "@ionic/react";
import { InsetIonItem } from "../../../pages/profile/ProfileFeedItemsPage";
import { useAppDispatch, useAppSelector } from "../../../store";
import { ListHeader } from "../shared/formatting";
import { updateFilteredKeywords } from "../settingsSlice";
import { uniq, without } from "lodash";

export default function FilteredKeywords() {
const [presentAlert] = useIonAlert();
const dispatch = useAppDispatch();
const filteredKeywords = useAppSelector(
(state) => state.settings.blocks.keywords,
);

async function remove(keyword: string) {
dispatch(updateFilteredKeywords(without(filteredKeywords, keyword)));
}

async function add() {
presentAlert({
message: "Add Filtered Keyword",
buttons: [
{
text: "OK",
handler: ({ keyword }) => {
if (!keyword.trim()) return;

dispatch(
updateFilteredKeywords(
uniq([...filteredKeywords, keyword.trim()]),
),
);
},
},
"Cancel",
],
inputs: [
{
placeholder: "Keyword",
name: "keyword",
},
],
});
}

return (
<>
<ListHeader>
<IonLabel>Filtered Keywords</IonLabel>
</ListHeader>
<IonList inset>
{filteredKeywords.map((keyword) => (
<IonItemSliding key={keyword}>
<IonItemOptions side="end" onIonSwipe={() => remove(keyword)}>
<IonItemOption
color="danger"
expandable
onClick={() => remove(keyword)}
>
Unfilter
</IonItemOption>
</IonItemOptions>
<InsetIonItem>
<IonLabel>{keyword}</IonLabel>
</InsetIonItem>
</IonItemSliding>
))}

<InsetIonItem onClick={add}>
<IonLabel color="primary">Add Keyword</IonLabel>
</InsetIonItem>
</IonList>
</>
);
}
40 changes: 40 additions & 0 deletions src/features/settings/settingsSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ interface SettingsState {
enableHapticFeedback: boolean;
linkHandler: LinkHandlerType;
};
blocks: {
keywords: string[];
};
}

const LOCALSTORAGE_KEYS = {
Expand Down Expand Up @@ -153,6 +156,9 @@ const initialState: SettingsState = {
enableHapticFeedback: true,
linkHandler: OLinkHandlerType.InApp,
},
blocks: {
keywords: [],
},
};

// We continue using localstorage for specific items because indexeddb is slow
Expand Down Expand Up @@ -240,6 +246,10 @@ export const appearanceSlice = createSlice({
state.appearance.posts.blurNsfw = action.payload;
// Per user setting is updated in StoreProvider
},
setFilteredKeywords(state, action: PayloadAction<string[]>) {
state.blocks.keywords = action.payload;
// Per user setting is updated in StoreProvider
},
setShowVotingButtons(state, action: PayloadAction<boolean>) {
state.appearance.compact.showVotingButtons = action.payload;
db.setSetting("compact_show_voting_buttons", action.payload);
Expand Down Expand Up @@ -353,6 +363,31 @@ export const getBlurNsfw =
dispatch(setNsfwBlur(blurNsfw ?? initialState.appearance.posts.blurNsfw));
};

export const getFilteredKeywords =
() => async (dispatch: AppDispatch, getState: () => RootState) => {
const userHandle = getState().auth.accountData?.activeHandle;

const filteredKeywords = await db.getSetting("filtered_keywords", {
user_handle: userHandle,
});

dispatch(
setFilteredKeywords(filteredKeywords ?? initialState.blocks.keywords),
);
};

export const updateFilteredKeywords =
(filteredKeywords: string[]) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const userHandle = getState().auth.accountData?.activeHandle;

dispatch(setFilteredKeywords(filteredKeywords));

db.setSetting("filtered_keywords", filteredKeywords, {
user_handle: userHandle,
});
};

export const fetchSettingsFromDatabase = createAsyncThunk<SettingsState>(
"appearance/fetchSettingsFromDatabase",
async (_, thunkApi) => {
Expand Down Expand Up @@ -391,6 +426,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk<SettingsState>(
"enable_haptic_feedback",
);
const link_handler = await db.getSetting("link_handler");
const filtered_keywords = await db.getSetting("filtered_keywords");

return {
...state.settings,
Expand Down Expand Up @@ -452,6 +488,9 @@ export const fetchSettingsFromDatabase = createAsyncThunk<SettingsState>(
enableHapticFeedback:
enable_haptic_feedback ?? initialState.general.enableHapticFeedback,
},
blocks: {
keywords: filtered_keywords ?? initialState.blocks.keywords,
},
};
});

Expand All @@ -475,6 +514,7 @@ export const {
setShowJumpButton,
setJumpButtonPosition,
setNsfwBlur,
setFilteredKeywords,
setPostAppearance,
setThumbnailPosition,
setShowVotingButtons,
Expand Down
1 change: 1 addition & 0 deletions src/features/user/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export default function Profile({ person }: ProfileProps) {
fetchFn={fetchFn}
header={header}
filterHiddenPosts={false}
filterKeywords={false}
/>
);
}
Expand Down
59 changes: 59 additions & 0 deletions src/helpers/lemmy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { keywordFoundInSentence } from "./lemmy";

describe("keywordFoundInSentence", () => {
it("false when empty", () => {
expect(keywordFoundInSentence("", "")).toBe(false);
});

it("false with keyword in empty string", () => {
expect(keywordFoundInSentence("keyword", "")).toBe(false);
});

it("false with keyword in sentence", () => {
expect(keywordFoundInSentence("keyword", "Hello, this is Voyager!")).toBe(
false,
);
});

it("true with keyword in middle", () => {
expect(keywordFoundInSentence("this", "Hello, this is Voyager!")).toBe(
true,
);
});

it("false with partial keyword in middle", () => {
expect(keywordFoundInSentence("thi", "Hello, this is Voyager!")).toBe(
false,
);
});

it("true with multi word keyword", () => {
expect(keywordFoundInSentence("this is", "Hello, this is Voyager!")).toBe(
true,
);
});

it("true with keyword without comma", () => {
expect(keywordFoundInSentence("Hello", "Hello, this is Voyager!")).toBe(
true,
);
});

it("true with multi keyword with comma", () => {
expect(
keywordFoundInSentence("Hello, this", "Hello, this is Voyager!"),
).toBe(true);
});

it("true at beginning", () => {
expect(keywordFoundInSentence("Hello", "Hello, this is Voyager!")).toBe(
true,
);
});

it("true case insensitive", () => {
expect(keywordFoundInSentence("voyageR", "Hello, this is Voyager!")).toBe(
true,
);
});
});
22 changes: 22 additions & 0 deletions src/helpers/lemmy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,25 @@ export function isUrlVideo(url: string): boolean {
export function share(item: Post | Comment) {
return Share.share({ url: item.ap_id });
}

export function postHasFilteredKeywords(
post: Post,
keywords: string[],
): boolean {
for (const keyword of keywords) {
if (keywordFoundInSentence(keyword, post.name)) return true;
}

return false;
}

export function keywordFoundInSentence(
keyword: string,
sentence: string,
): boolean {
// Create a regular expression pattern to match the keyword as a whole word
const pattern = new RegExp(`\\b${keyword}\\b`, "i");

// Use the RegExp test method to check if the pattern is found in the sentence
return pattern.test(sentence);
}
3 changes: 2 additions & 1 deletion src/pages/profile/ProfileFeedHiddenPostsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ export default function ProfileFeedHiddenPostsPage() {
</IonHeader>
<FeedContent>
<PostCommentFeed
filterHiddenPosts={false}
fetchFn={fetchFn}
limit={LIMIT}
filterHiddenPosts={false}
filterKeywords={false}
/>
</FeedContent>
</IonPage>
Expand Down
6 changes: 5 additions & 1 deletion src/pages/profile/ProfileFeedItemsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ export default function ProfileFeedItemsPage({
</IonToolbar>
</IonHeader>
<FeedContent>
<PostCommentFeed fetchFn={fetchFn} filterHiddenPosts={false} />
<PostCommentFeed
fetchFn={fetchFn}
filterHiddenPosts={false}
filterKeywords={false}
/>
</FeedContent>
</IonPage>
);
Expand Down
2 changes: 2 additions & 0 deletions src/pages/settings/BlocksSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import FilterNsfw from "../../features/settings/blocks/FilterNsfw";
import BlockedCommunities from "../../features/settings/blocks/BlockedCommunities";
import { CenteredSpinner } from "../posts/PostPage";
import BlockedUsers from "../../features/settings/blocks/BlockedUsers";
import FilteredKeywords from "../../features/settings/blocks/FilteredKeywords";

export default function BlocksSettingsPage() {
const userHandle = useAppSelector(handleSelector);
Expand All @@ -48,6 +49,7 @@ export default function BlocksSettingsPage() {
{localUser ? (
<AppContent scrollY>
<FilterNsfw />
<FilteredKeywords />
<BlockedCommunities />
<BlockedUsers />
</AppContent>
Expand Down
1 change: 1 addition & 0 deletions src/services/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export type SettingValueTypes = {
link_handler: LinkHandlerType;
show_jump_button: boolean;
jump_button_position: JumpButtonPositionType;
filtered_keywords: string[];
};

export interface ISettingItem<T extends keyof SettingValueTypes> {
Expand Down
2 changes: 2 additions & 0 deletions src/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import inboxSlice from "./features/inbox/inboxSlice";
import settingsSlice, {
fetchSettingsFromDatabase,
getBlurNsfw,
getFilteredKeywords,
} from "./features/settings/settingsSlice";
import gestureSlice, {
fetchGesturesFromDatabase,
Expand Down Expand Up @@ -61,6 +62,7 @@ const activeHandleChange = () => {

store.dispatch(getFavoriteCommunities());
store.dispatch(getBlurNsfw());
store.dispatch(getFilteredKeywords());
};

export function StoreProvider({ children }: { children: ReactNode }) {
Expand Down

0 comments on commit 3435166

Please sign in to comment.