A cheap attempt at a native Bluesky client for Android
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Configurable reply filtering, UI polish, and layout improvements

- Add ReplyFilterMode enum (None, OnlyFilterDeepThreads, Strict)
with OnlyFilterDeepThreads as default: shows direct replies from
followed accounts, only filters deep 4+ thread noise
- Restructure SkeetView to two-column layout (avatar + thread line |
header + content + actions) for top-level posts, old stacked layout
for nested embeds
- Thread connector line runs alongside post content under the avatar
- "See more" card aligned with content column and connected by thread line
- Fix 3-image gallery blank space (fillMaxSize -> fillMaxWidth)
- Smaller avatar (44dp), refined embed/external view padding
- Redesigned external link embeds with title/description/domain row

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

geesawra 447ec437 0ee5c6af

+49 -27
+49 -27
app/src/main/java/industries/geesawra/monarch/datalayer/Models.kt
··· 45 45 import kotlin.time.ExperimentalTime 46 46 import kotlin.time.Instant 47 47 48 + enum class ReplyFilterMode { 49 + None, 50 + OnlyFilterDeepThreads, 51 + Strict 52 + } 53 + 48 54 enum class CDNImageSize( 49 55 val size: String 50 56 ) { ··· 109 115 var replyToNotFollowing: Boolean = false, 110 116 ) { 111 117 companion object { 112 - fun fromFeedViewPost(post: FeedViewPost, currentUserDid: Did? = null): SkeetData { 118 + fun fromFeedViewPost(post: FeedViewPost, currentUserDid: Did? = null, replyFilterMode: ReplyFilterMode = ReplyFilterMode.OnlyFilterDeepThreads): SkeetData { 113 119 val content: Post = (post.post.record.decodeAs()) 114 120 115 121 val sd = SkeetData( ··· 137 143 ) 138 144 139 145 sd.replyToNotFollowing = run { 146 + if (replyFilterMode == ReplyFilterMode.None) return@run false 140 147 if (currentUserDid != null && sd.did == currentUserDid) return@run false 141 148 142 149 when (sd.reason) { ··· 155 162 } 156 163 157 164 if (parent == null) { 158 - return@run false // no parent AND no root, this is a single post 165 + return@run false // single post, no parent 159 166 } 160 167 161 - if (root == null) { 162 - return@run !isFollowedOrSelf(parent) // just parent, this is a single reply 163 - } 168 + when (replyFilterMode) { 169 + ReplyFilterMode.OnlyFilterDeepThreads -> { 170 + // Show direct replies from followed accounts freely. 171 + // Only filter 4+ deep threads where root/grandfather are unfollowed. 172 + if (root == null) return@run false // direct reply, always show 164 173 165 - if (!isFollowedOrSelf(parent)) { 166 - return@run true // I don't follow whoever sd is replying to, don't care 167 - } 174 + val parentsParent = sd.parentsParentRef() 175 + if (parentsParent != null && parentsParent.uri == root.uri) { 176 + return@run false // 3-post thread, always show 177 + } 168 178 169 - // Simplify everything: 170 - // - 3 post threads: if we follow parent and root, we show 171 - // - 4+ post threads: if we follow parent, root, and grandfather, we show 179 + // 4+ deep: require root and grandfather to be followed 180 + if (!isFollowedOrSelf(parent)) return@run true 172 181 173 - // from now on, we know both parent and root are non-null, 174 - // hence we might be in a 3+ post threads. 182 + val grandfather = sd.grandparentAuthor() 183 + val grandfatherFollowed = if (currentUserDid != null && grandfather?.did == currentUserDid) { 184 + true 185 + } else { 186 + grandfather?.viewer?.following?.isNotEmpty() ?: false 187 + } 188 + return@run !(isFollowedOrSelf(root) && grandfatherFollowed) 189 + } 175 190 176 - val parentsParent = sd.parentsParentRef()!! 177 - if (parentsParent.uri == root.uri) { 178 - // parent's root == root, this is a 3 post thread 179 - return@run !isFollowedOrSelf(root) 180 - } 191 + ReplyFilterMode.Strict -> { 192 + // Require all thread participants to be followed 193 + if (root == null) { 194 + return@run !isFollowedOrSelf(parent) 195 + } 181 196 182 - val grandfather = sd.grandparentAuthor() 183 - val grandfatherFollowed = if (currentUserDid != null && grandfather?.did == currentUserDid) { 184 - true 185 - } else { 186 - grandfather?.viewer?.following?.isNotEmpty() ?: false 187 - } 197 + if (!isFollowedOrSelf(parent)) return@run true 188 198 189 - // base case: parent, root and grandfather all followed 190 - return@run !(isFollowedOrSelf(root) && grandfatherFollowed) 191 - } 199 + val parentsParent = sd.parentsParentRef() 200 + if (parentsParent != null && parentsParent.uri == root.uri) { 201 + return@run !isFollowedOrSelf(root) 202 + } 192 203 204 + val grandfather = sd.grandparentAuthor() 205 + val grandfatherFollowed = if (currentUserDid != null && grandfather?.did == currentUserDid) { 206 + true 207 + } else { 208 + grandfather?.viewer?.following?.isNotEmpty() ?: false 209 + } 210 + return@run !(isFollowedOrSelf(root) && grandfatherFollowed) 211 + } 193 212 213 + else -> return@run false 214 + } 215 + } 194 216 } 195 217 } 196 218