Skip to content

Commit

Permalink
Add support for hiding posts in the home screen with blocked words (#824
Browse files Browse the repository at this point in the history
)

* Add blocked words table to the database

* Add `isHidden` column to posts table

* Add trigger to posts table to hide post after insert if they contain a blocked word in title/description/rawContent

* Add trigger to posts table to hide post before update if they contain a blocked word in title/description/rawContent

* Add trigger to hide posts containing newly added blocked words

* Add trigger to unhide posts containing the removed blocked word

* Filter hidden posts when loading them in the home screen

* Add `isHidden` to `Post` model

* Add `BlockedWordsRepository`

* Add local model for `BlockedWord`

* Add `BlockedWordsPresenter`

* Add empty `BlockedWordsScreen` to navigation graph

* Add support for opening blocked words screen from settings screen

* Add support for trailing icon for text field in add screen

Eventually move this as a separate component

* Change before delete trigger to after delete for blocked words table

* Ignore hidden posts when counting unread posts

* Add blocked words screen

* Order blocked words list by rowid

* Replace blocked words upsert query with insert or ignore
  • Loading branch information
msasikanth authored Feb 26, 2025
1 parent d9846f9 commit 9177fa5
Show file tree
Hide file tree
Showing 26 changed files with 716 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,6 @@ interface DataComponent : SqlDriverPlatformComponent, DataStorePlatformComponent
@Provides fun providesFeedGroupQueries(database: ReaderDatabase) = database.feedGroupQueries

@Provides fun providesSourceQueries(database: ReaderDatabase) = database.sourceQueries

@Provides fun providesBlockedWordsQueries(database: ReaderDatabase) = database.blockedWordsQueries
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2025 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.sasikanth.rss.reader.data.repository

import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import dev.sasikanth.rss.reader.core.model.local.BlockedWord
import dev.sasikanth.rss.reader.data.database.BlockedWordsQueries
import dev.sasikanth.rss.reader.di.scopes.AppScope
import dev.sasikanth.rss.reader.util.DispatchersProvider
import dev.sasikanth.rss.reader.util.nameBasedUuidOf
import kotlinx.coroutines.withContext
import me.tatarka.inject.annotations.Inject

@Inject
@AppScope
class BlockedWordsRepository(
private val blockedWordsQueries: BlockedWordsQueries,
dispatchersProvider: DispatchersProvider,
) {

private val ioDispatcher = dispatchersProvider.io

suspend fun addWord(word: String) {
withContext(ioDispatcher) {
val uuid = nameBasedUuidOf(word.lowercase())
blockedWordsQueries.insert(id = uuid.toString(), content = word)
}
}

suspend fun removeWord(id: Uuid) {
withContext(ioDispatcher) { blockedWordsQueries.remove(id.toString()) }
}

fun words() =
blockedWordsQueries
.words(mapper = { id, content -> BlockedWord(id = uuidFrom(id), content = content) })
.asFlow()
.mapToList(ioDispatcher)
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
CREATE TABLE blockedWord (
id TEXT NOT NULL PRIMARY KEY,
content TEXT NOT NULL
);

CREATE INDEX blocked_word_value_index ON blockedWord(content);

CREATE TRIGGER hide_posts_with_blocked_words_AFTER_INSERT
AFTER INSERT ON blockedWord
BEGIN
UPDATE OR IGNORE post
SET isHidden = 1
WHERE
(title LIKE '%' || new.content || '%' OR
description LIKE '%' || new.content || '%' OR
rawContent LIKE '%' || new.content || '%') AND
isHidden = 0;
END;

CREATE TRIGGER unhide_posts_with_blocked_words_AFTER_DELETE
AFTER DELETE ON blockedWord
BEGIN
UPDATE OR IGNORE post
SET isHidden = 0
WHERE
(title LIKE '%' || old.content || '%' OR
description LIKE '%' || old.content || '%' OR
rawContent LIKE '%' || old.content || '%') AND
isHidden = 1;
END;

insert:
INSERT OR IGNORE INTO blockedWord(id, content)
VALUES (?, ?);

remove:
DELETE FROM blockedWord WHERE id = :id;

words:
SELECT * FROM blockedWord ORDER BY rowid DESC;
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ SELECT
f.showFeedFavIcon
FROM feed f
LEFT JOIN post p ON f.id = p.sourceId AND p.date > :postsAfter
WHERE p.isHidden == 0
GROUP BY f.id
ORDER BY
CASE WHEN :orderBy = 'latest' THEN f.createdAt END DESC,
Expand Down Expand Up @@ -86,7 +87,7 @@ SELECT
f.showFeedFavIcon
FROM feed f
LEFT JOIN post p ON f.id = p.sourceId AND p.date > :postsAfter
WHERE f.id = :id
WHERE f.id = :id AND p.isHidden == 0
ORDER BY pinnedAt DESC, createdAt DESC LIMIT 1;

updateFeedName:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,44 @@ CREATE TABLE post(
commentsLink TEXT DEFAULT NULL,
bookmarked INTEGER AS Boolean NOT NULL DEFAULT 0,
read INTEGER AS Boolean NOT NULL DEFAULT 0,
isHidden INTEGER AS Boolean NOT NULL DEFAULT 0,
FOREIGN KEY(sourceId) REFERENCES feed(id) ON DELETE CASCADE
);

CREATE INDEX post_source_id_index ON post(sourceId);
CREATE INDEX post_date_desc_index ON post (date DESC);
CREATE INDEX post_is_hidden_index ON post(isHidden);

CREATE TRIGGER hide_post_if_blocked_word_is_present_AFTER_INSERT
AFTER INSERT ON post
FOR EACH ROW
WHEN (
SELECT 1
FROM blockedWord
WHERE
new.title LIKE '%' || blockedWord.content || '%' OR
new.description LIKE '%' || blockedWord.content || '%' OR
new.rawContent LIKE '%' || blockedWord.content || '%'
) IS NOT NULL
BEGIN
UPDATE post SET isHidden = 1 WHERE id = new.id;
END;

CREATE TRIGGER hide_post_if_blocked_word_is_present_BEFORE_UPDATE
BEFORE UPDATE ON post
FOR EACH ROW
WHEN (
SELECT 1
FROM blockedWord
WHERE
(new.title LIKE '%' || blockedWord.content || '%' OR
new.description LIKE '%' || blockedWord.content || '%' OR
new.rawContent LIKE '%' || blockedWord.content || '%') AND
new.isHidden == 0
) IS NOT NULL
BEGIN
UPDATE post SET isHidden = 1 WHERE id = new.id;
END;

upsert:
INSERT INTO post(id, sourceId, title, description, rawContent, imageUrl, date, link, commentsLink)
Expand All @@ -29,6 +62,7 @@ count:
SELECT COUNT(DISTINCT post.id) FROM post
LEFT JOIN feedGroup ON INSTR(feedGroup.feedIds, post.sourceId)
WHERE
post.isHidden == 0 AND
(:unreadOnly IS NULL OR post.read != :unreadOnly) AND
(
:sourceId IS NULL OR
Expand Down Expand Up @@ -57,6 +91,7 @@ FROM post
INNER JOIN feed ON post.sourceId == feed.id
LEFT JOIN feedGroup ON INSTR(feedGroup.feedIds, post.sourceId)
WHERE
post.isHidden == 0 AND
(:unreadOnly IS NULL OR post.read != :unreadOnly) AND
(
:sourceId IS NULL OR
Expand Down Expand Up @@ -85,6 +120,7 @@ FROM post
INNER JOIN feed ON post.sourceId == feed.id
LEFT JOIN feedGroup ON INSTR(feedGroup.feedIds, post.sourceId)
WHERE
post.isHidden == 0 AND
(:unreadOnly IS NULL OR post.read != :unreadOnly) AND
(
:sourceId IS NULL OR
Expand Down Expand Up @@ -123,6 +159,7 @@ unreadPostsCountInSource:
SELECT COUNT(*) FROM post
LEFT JOIN feedGroup ON INSTR(feedGroup.feedIds, post.sourceId)
WHERE
post.isHidden == 0 AND
read != 1 AND
date > :after AND
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ FROM (
f.pinnedPosition
FROM feed f
LEFT JOIN post p ON f.id = p.sourceId AND p.date > :postsAfter
WHERE p.isHidden == 0
GROUP BY f.id

UNION ALL
Expand Down Expand Up @@ -75,6 +76,7 @@ FROM (
fg.pinnedPosition
FROM feedGroup fg
LEFT JOIN post p ON INSTR(fg.feedIds, p.sourceId) AND p.date > :postsAfter
WHERE p.isHidden == 0
GROUP BY fg.id
)
WHERE pinnedAt IS NOT NULL
Expand Down Expand Up @@ -116,6 +118,7 @@ FROM (
NULL AS updatedAt
FROM feed f
LEFT JOIN post p ON f.id = p.sourceId AND p.date > :postsAfter
WHERE p.isHidden == 0
GROUP BY f.id

UNION ALL
Expand Down Expand Up @@ -144,6 +147,7 @@ FROM (
fg.updatedAt
FROM feedGroup fg
LEFT JOIN post p ON INSTR(fg.feedIds, p.sourceId) AND p.date > :postsAfter
WHERE p.isHidden == 0
GROUP BY fg.id
)
ORDER BY type DESC,
Expand Down
69 changes: 69 additions & 0 deletions core/data/src/commonMain/sqldelight/migrations/20.sqm
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
-- Post table
ALTER TABLE post ADD COLUMN isHidden INTEGER NOT NULL DEFAULT 0;

CREATE INDEX post_is_hidden_index ON post(isHidden);

-- Blocked words table
CREATE TABLE blockedWord (
id TEXT NOT NULL PRIMARY KEY,
content TEXT NOT NULL
);

CREATE INDEX blocked_word_value_index ON blockedWord(content);

-- Post table triggers
CREATE TRIGGER hide_post_if_blocked_word_is_present_AFTER_INSERT
AFTER INSERT ON post
FOR EACH ROW
WHEN (
SELECT 1
FROM blockedWord
WHERE
new.title LIKE '%' || blockedWord.content || '%' OR
new.description LIKE '%' || blockedWord.content || '%' OR
new.rawContent LIKE '%' || blockedWord.content || '%'
) IS NOT NULL
BEGIN
UPDATE post SET isHidden = 1 WHERE id = new.id;
END;

CREATE TRIGGER hide_post_if_blocked_word_is_present_BEFORE_UPDATE
BEFORE UPDATE ON post
FOR EACH ROW
WHEN (
SELECT 1
FROM blockedWord
WHERE
(new.title LIKE '%' || blockedWord.content || '%' OR
new.description LIKE '%' || blockedWord.content || '%' OR
new.rawContent LIKE '%' || blockedWord.content || '%') AND
new.isHidden == 0
) IS NOT NULL
BEGIN
UPDATE post SET isHidden = 1 WHERE id = new.id;
END;

-- Blocked words triggers
CREATE TRIGGER hide_posts_with_blocked_words_AFTER_INSERT
AFTER INSERT ON blockedWord
BEGIN
UPDATE OR IGNORE post
SET isHidden = 1
WHERE
(title LIKE '%' || new.content || '%' OR
description LIKE '%' || new.content || '%' OR
rawContent LIKE '%' || new.content || '%') AND
isHidden = 0;
END;

CREATE TRIGGER unhide_posts_with_blocked_words_AFTER_DELETE
AFTER DELETE ON blockedWord
BEGIN
UPDATE OR IGNORE post
SET isHidden = 0
WHERE
(title LIKE '%' || old.content || '%' OR
description LIKE '%' || old.content || '%' OR
rawContent LIKE '%' || old.content || '%') AND
isHidden = 1;
END;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2025 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.sasikanth.rss.reader.core.model.local

import com.benasher44.uuid.Uuid

data class BlockedWord(
val id: Uuid,
val content: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ data class Post(
val commentsLink: String?,
val bookmarked: Boolean,
val read: Boolean,
val isHidden: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,10 @@ val DeTwineStrings =
enableAutoSyncDesc = "When turned-on, feeds will be updated in the background",
showFeedFavIconTitle = "Show feed fav icon",
showFeedFavIconDesc =
"When turned-off, the feed icon will be displayed instead of the website's favicon"
"When turned-off, the feed icon will be displayed instead of the website's favicon",
blockedWords = "Blocked words",
blockedWordsHint = "Enter a word",
blockedWordsDesc =
"Post can be hidden from home screen based on their text. We recommend avoiding common words that appear in many posts, since it can result in no posts being shown or negatively impacting app performance. \n\nHidden posts will still be displayed in search & bookmarks.",
blockedWordsEmpty = "You haven't blocked any words yet",
)
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,10 @@ val EnTwineStrings =
enableAutoSyncDesc = "When turned-on, feeds will be updated in the background",
showFeedFavIconTitle = "Show feed fav icon",
showFeedFavIconDesc =
"When turned-off, the feed icon will be displayed instead of the website's favicon"
"When turned-off, the feed icon will be displayed instead of the website's favicon",
blockedWords = "Blocked words",
blockedWordsHint = "Enter a word",
blockedWordsDesc =
"Post can be hidden from the home screen based on their text. We recommend avoiding common words that appear in many posts, since it can result in no posts being shown or negatively impacting app performance. \n\nHidden posts will still be displayed in search & bookmarks.",
blockedWordsEmpty = "You haven't blocked any words yet",
)
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,10 @@ val TrTwineStrings =
enableAutoSyncDesc = "When turned-on, feeds will be updated in the background",
showFeedFavIconTitle = "Show feed fav icon",
showFeedFavIconDesc =
"When turned-off, the feed icon will be displayed instead of the website's favicon"
"When turned-off, the feed icon will be displayed instead of the website's favicon",
blockedWords = "Blocked words",
blockedWordsHint = "Enter a word",
blockedWordsDesc =
"Post can be hidden from the home screen based on their text. We recommend avoiding common words that appear in many posts, since it can result in no posts being shown or negatively impacting app performance. \n\nHidden posts will still be displayed in search & bookmarks.",
blockedWordsEmpty = "You haven't blocked any words yet",
)
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ data class TwineStrings(
val enableAutoSyncDesc: String,
val showFeedFavIconTitle: String,
val showFeedFavIconDesc: String,
val blockedWords: String,
val blockedWordsHint: String,
val blockedWordsDesc: String,
val blockedWordsEmpty: String,
)

object Locales {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,10 @@ val ZhTwineStrings =
enableAutoSyncDesc = "When turned-on, feeds will be updated in the background",
showFeedFavIconTitle = "Show feed fav icon",
showFeedFavIconDesc =
"When turned-off, the feed icon will be displayed instead of the website's favicon"
"When turned-off, the feed icon will be displayed instead of the website's favicon",
blockedWords = "Blocked words",
blockedWordsHint = "Enter a word",
blockedWordsDesc =
"Post can be hidden from the home screen based on their text. We recommend avoiding common words that appear in many posts, since it can result in no posts being shown or negatively impacting app performance. \n\nHidden posts will still be displayed in search & bookmarks.",
blockedWordsEmpty = "You haven't blocked any words yet",
)
Loading

0 comments on commit 9177fa5

Please sign in to comment.