Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keyword block #755

Merged
merged 3 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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