Skip to content

Commit

Permalink
Add filtered websites (#1578)
Browse files Browse the repository at this point in the history
Resolves #1236
  • Loading branch information
aeharding authored Aug 11, 2024
1 parent e47f027 commit 8660f7a
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 19 deletions.
26 changes: 14 additions & 12 deletions src/features/feed/PostCommentFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
isComment,
isPost,
postHasFilteredKeywords,
postHasFilteredWebsite,
} from "../../helpers/lemmy";
import { useAutohidePostIfNeeded } from "./PageTypeContext";

Expand All @@ -36,15 +37,15 @@ interface PostCommentFeed
extends Omit<FeedProps<PostCommentItem>, "renderItemContent"> {
communityName?: string;
filterHiddenPosts?: boolean;
filterKeywords?: boolean;
filterKeywordsAndWebsites?: boolean;

header?: ReactElement;
}

export default function PostCommentFeed({
fetchFn: _fetchFn,
filterHiddenPosts = true,
filterKeywords = true,
filterKeywordsAndWebsites = true,
filterOnRxFn: _filterOnRxFn,
filterFn: _filterFn,
...rest
Expand All @@ -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,
Expand Down Expand Up @@ -152,26 +156,24 @@ 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);

return true;
},
[
postHiddenById,
filteredKeywords,
filterKeywords,
filterHiddenPosts,
filterKeywordsAndWebsites,
_filterFn,
postDeletedById,
filteredKeywords,
filteredWebsites,
],
);

Expand Down
104 changes: 104 additions & 0 deletions src/features/settings/blocks/FilteredWebsites.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<ListHeader>
<IonLabel>Filtered Websites</IonLabel>
</ListHeader>
<IonList inset>
{filteredWebsites.map((website) => (
<IonItemSliding key={website}>
<IonItemOptions side="end" onIonSwipe={() => remove(website)}>
<IonItemOption
color="danger"
expandable
onClick={() => remove(website)}
>
Unfilter
</IonItemOption>
</IonItemOptions>
<IonItem>
<RemoveItemButton />
<IonLabel>{website}</IonLabel>
</IonItem>
</IonItemSliding>
))}

<IonItemSliding>
<IonItem onClick={add} button detail={false}>
<IonLabel color="primary">Add Website</IonLabel>
</IonItem>
</IonItemSliding>
</IonList>
</>
);
}
34 changes: 34 additions & 0 deletions src/features/settings/settingsSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ interface SettingsState {
};
blocks: {
keywords: string[];
websites: string[];
};
}

Expand Down Expand Up @@ -229,6 +230,7 @@ export const initialState: SettingsState = {
},
blocks: {
keywords: [],
websites: [],
},
};

Expand Down Expand Up @@ -372,6 +374,10 @@ export const appearanceSlice = createSlice({
state.blocks.keywords = action.payload;
// Per user setting is updated in StoreProvider
},
setFilteredWebsites(state, action: PayloadAction<string[]>) {
state.blocks.websites = action.payload;
// Per user setting is updated in StoreProvider
},
setDefaultFeed(state, action: PayloadAction<DefaultFeedType | undefined>) {
state.general.defaultFeed = action.payload;
// Per user setting is updated in StoreProvider
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<SettingsState>(
"appearance/fetchSettingsFromDatabase",
async (_, thunkApi) => {
Expand Down Expand Up @@ -711,6 +742,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk<SettingsState>(
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(
Expand Down Expand Up @@ -879,6 +911,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk<SettingsState>(
},
blocks: {
keywords: filtered_keywords ?? initialState.blocks.keywords,
websites: filtered_websites ?? initialState.blocks.websites,
},
};
});
Expand Down Expand Up @@ -918,6 +951,7 @@ export const {
setShowCommunityIcons,
setCommunityAtTop,
setFilteredKeywords,
setFilteredWebsites,
setPostAppearance,
setRememberPostAppearance,
setThumbnailPosition,
Expand Down
2 changes: 1 addition & 1 deletion src/features/user/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default function Profile({ person }: ProfileProps) {
fetchFn={fetchFn}
header={header}
filterHiddenPosts={false}
filterKeywords={false}
filterKeywordsAndWebsites={false}
/>
);
}
Expand Down
84 changes: 83 additions & 1 deletion src/helpers/lemmy.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 8660f7a

Please sign in to comment.