Skip to content

Commit

Permalink
ui: Hide quoted or reposted notes from people whom the user has muted. (
Browse files Browse the repository at this point in the history
#1216)

Summary
-------

This patch fixes the issue where the user might see notes from users that they have muted, if such note has been reposted or quoted.

Furthermore, this patch introduces some improvements on some of the associated views, making them more reusable.

Testing of the fix
------------------

**PASS**

**Device:** iPhone 14 Pro simulator
**iOS:** 17.0
**Damus:** This commit
**Test steps:**

1. Create two test accounts (if not created already). We will use test account "A". Test account "B" is an external test account
2. Make some notes from test account "B" (if non existent)
3. Switch to account "A"
4. Under test account "A", follow account "B"
5. Repost a note from account "B", and quote another note from account "B"
6. Access "account B"'s timeline. Repost and quoted note should all be visible. Layout should look as usual
7. Click on the reposted note. Should appear and it should look normal
8. Click on the note with the quote. Should appear and it should look normal
9. Click on the quoted note. Should appear and it should look normal
10. Now mute account "B"
12. Go back to account "A"'s timeline
13. Repost should appear, but the reposted content should be hidden behind a mute box. Clicking on show/hide should show or hide muted content
14. Note with quoted content should appear, but the quoted content should be hidden behind a mute box. Clicking on show/hide should work as expected
15. Make sure that the layout in steps 13 and 14 look good.
16. Click on the repost to access the thread view. Should be muted as expected.
17. Add a comment to the repost. Comment should appear even if the mute box hides the main note
18. Click on the note with quote to open its thread view. Comments should appear, main note should appear, but quoted content should be behind the mute box
19. Under account "B", add a comment to the quoted notes
20. Under account "A", check in the thread view that "B"'s reply is behind a mute box
21. Reply to the note with the quote. Check that the note appears correctly and that quoted content is behind the mute box (in the post composer view)
22. Find on Nostr a post where one of the replies contains a quoted note. Mute the user of the quoted content, and check that quoted content is now in a mute box

Smoke sanity test
-----------------

**PASS**

**Device:** iPhone 14 pro simulator
**iOS:** 16.4
**Test steps:** Browse a timeline filled with real notes and comments. Go through different notes and threads, mute some users, just to make sure nothing else appears obsviously broken.

Other notes
-----------

I removed this code:

```
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
```

from `EventShell`, because it was causing the layout to break on "threaded" style event view with muted quoted content (e.g. in a reply with quoted content).

The line of code dates back to `495859e07f`, but I am not sure why this line existed in the first place, or if removing it has any negative impact.

Closes: #1216
Changelog-Fixed: Hide quoted or reposted notes from people whom the user has muted. (#1216)
Signed-off-by: Daniel D’Aquino <[email protected]>
Reviewed-by: William Casarin <[email protected]>
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
danieldaquino authored and jb55 committed Oct 1, 2023
1 parent 46382ae commit 66310a1
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 115 deletions.
16 changes: 12 additions & 4 deletions damus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE02981A83900D66079 /* MutelistView.swift */; };
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE22981BC7D00D66079 /* UserView.swift */; };
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE42981EE0C00D66079 /* EULAView.swift */; };
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE6298444FC00D66079 /* MutedEventView.swift */; };
4CF0ABE7298444FD00D66079 /* EventMutingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE6298444FC00D66079 /* EventMutingContainerView.swift */; };
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE829844AF100D66079 /* AnyCodable.swift */; };
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */; };
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
Expand Down Expand Up @@ -424,6 +424,8 @@
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; };
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; };
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; };
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
Expand Down Expand Up @@ -1044,7 +1046,7 @@
4CF0ABE02981A83900D66079 /* MutelistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistView.swift; sourceTree = "<group>"; };
4CF0ABE22981BC7D00D66079 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = "<group>"; };
4CF0ABE42981EE0C00D66079 /* EULAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULAView.swift; sourceTree = "<group>"; };
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedEventView.swift; sourceTree = "<group>"; };
4CF0ABE6298444FC00D66079 /* EventMutingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMutingContainerView.swift; sourceTree = "<group>"; };
4CF0ABE829844AF100D66079 /* AnyCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = "<group>"; };
4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; };
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1104,6 +1106,8 @@
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; };
D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; };
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; };
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1994,12 +1998,14 @@
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
4CF0ABE6298444FC00D66079 /* EventMutingContainerView.swift */,
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */,
4CA9275B2A28FF570098A105 /* Longform */,
4CA927602A290E340098A105 /* EventShell.swift */,
D7870BC02AC4750B0080BA88 /* MentionView.swift */,
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */,
);
path = Events;
sourceTree = "<group>";
Expand Down Expand Up @@ -2541,6 +2547,7 @@
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */,
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
Expand Down Expand Up @@ -2621,7 +2628,7 @@
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
4CF0ABE7298444FD00D66079 /* EventMutingContainerView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
4C1253502A76C5B20004F4B8 /* UnfollowedNotify.swift in Sources */,
4C86F7C62A76C51100EC0817 /* AttachedWalletNotify.swift in Sources */,
Expand Down Expand Up @@ -2671,6 +2678,7 @@
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4CA352AC2A76C07F003BB08B /* NewUnmutesNotify.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
Expand Down
69 changes: 15 additions & 54 deletions damus/Views/Events/BuilderEventView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,84 +10,45 @@ import SwiftUI
struct BuilderEventView: View {
let damus: DamusState
let event_id: NoteId
@State var event: NostrEvent?
@State var subscription_uuid: String = UUID().description
let event: NostrEvent?

init(damus: DamusState, event: NostrEvent) {
_event = State(initialValue: event)
self.event = event
self.damus = damus
self.event_id = event.id
}

init(damus: DamusState, event_id: NoteId) {
let event = damus.events.lookup(event_id)
self.event_id = event_id
self.damus = damus
_event = State(initialValue: event)
self.event = nil
}

func unsubscribe() {
damus.pool.unsubscribe(sub_id: subscription_uuid)
}

func subscribe(filters: [NostrFilter]) {
damus.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
damus.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
}

func handle_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nostr_response) = ev else {
return
}

guard case .event(let id, let nostr_event) = nostr_response else {
return
}

guard id == subscription_uuid else {
return
}

if event != nil {
return
}

event = nostr_event

unsubscribe()
}

func load() {
subscribe(filters: [
NostrFilter(ids: [self.event_id], limit: 1)
])
func Event(event: NostrEvent) -> some View {
return EventView(damus: damus, event: event, options: .embedded)
.padding([.top, .bottom], 8)
.onTapGesture {
let ev = event.get_inner_event(cache: damus.events) ?? event
let thread = ThreadModel(event: ev, damus_state: damus)
damus.nav.push(route: .Thread(thread: thread))
}
}

var body: some View {
VStack {
if let event {
EventView(damus: damus, event: event, options: .embedded)
.padding([.top, .bottom], 8)
.onTapGesture {
let ev = event.get_inner_event(cache: damus.events) ?? event
let thread = ThreadModel(event: ev, damus_state: damus)
damus.nav.push(route: .Thread(thread: thread))
}
self.Event(event: event)
} else {
ProgressView().padding()
EventLoaderView(damus_state: damus, event_id: self.event_id) { loaded_event in
self.Event(event: loaded_event)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1.0)
)
.onAppear {
guard event == nil else {
return
}
self.load()
}
}
}

Expand Down
87 changes: 87 additions & 0 deletions damus/Views/Events/EventLoaderView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// EventLoaderView.swift
// damus
//
// Created by Daniel D’Aquino on 2023-09-27.
//

import SwiftUI

/// This view handles the loading logic for Nostr events, so that you can easily use views that require `NostrEvent`, even if you only have a `NoteId`
struct EventLoaderView<Content: View>: View {
let damus_state: DamusState
let event_id: NoteId
@State var event: NostrEvent?
@State var subscription_uuid: String = UUID().description
let content: (NostrEvent) -> Content

init(damus_state: DamusState, event_id: NoteId, @ViewBuilder content: @escaping (NostrEvent) -> Content) {
self.damus_state = damus_state
self.event_id = event_id
self.content = content
let event = damus_state.events.lookup(event_id)
_event = State(initialValue: event)
}

func unsubscribe() {
damus_state.pool.unsubscribe(sub_id: subscription_uuid)
}

func subscribe(filters: [NostrFilter]) {
damus_state.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
damus_state.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
}

func handle_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nostr_response) = ev else {
return
}

guard case .event(let id, let nostr_event) = nostr_response else {
return
}

guard id == subscription_uuid else {
return
}

if event != nil {
return
}

event = nostr_event

unsubscribe()
}

func load() {
subscribe(filters: [
NostrFilter(ids: [self.event_id], limit: 1)
])
}

var body: some View {
VStack {
if let event {
self.content(event)
} else {
ProgressView().padding()
}
}
.onAppear {
guard event == nil else {
return
}
self.load()
}
}
}


struct EventLoaderView_Previews: PreviewProvider {
static var previews: some View {
EventLoaderView(damus_state: test_damus_state, event_id: test_note.id) { event in
EventView(damus: test_damus_state, event: event)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,63 +1,51 @@
//
// MutedEventView.swift
// EventMutingContainerView.swift
// damus
//
// Created by William Casarin on 2023-01-27.
//

import SwiftUI

struct MutedEventView: View {
/// A container view that shows or hides provided content based on whether the given event should be muted or not, with built-in user controls to show or hide content, and an option to customize the muted box
struct EventMutingContainerView<Content: View>: View {
typealias MuteBoxViewClosure = ((_ shown: Binding<Bool>) -> AnyView)

let damus_state: DamusState
let event: NostrEvent
let content: Content
var customMuteBox: MuteBoxViewClosure?

let selected: Bool
@State var shown: Bool

init(damus_state: DamusState, event: NostrEvent, selected: Bool) {
init(damus_state: DamusState, event: NostrEvent, @ViewBuilder content: () -> Content) {
self.damus_state = damus_state
self.event = event
self.selected = selected
self.content = content()
self._shown = State(initialValue: should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event))
}

var should_mute: Bool {
return !should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event)
}

var MutedBox: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(DamusColors.adaptableGrey)

HStack {
Text("Note from a user you've muted", comment: "Text to indicate that what is being shown is a note from a user who has been muted.")
Spacer()
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a note from a user who has been muted.")) {
shown.toggle()
}
}
.padding(10)
}
init(damus_state: DamusState, event: NostrEvent, muteBox: @escaping MuteBoxViewClosure, @ViewBuilder content: () -> Content) {
self.init(damus_state: damus_state, event: event, content: content)
self.customMuteBox = muteBox
}

var Event: some View {
Group {
if selected {
SelectedEventView(damus: damus_state, event: event, size: .selected)
} else {
EventView(damus: damus_state, event: event)
}
}
var should_mute: Bool {
return !should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event)
}

var body: some View {
Group {
if should_mute {
MutedBox
if let customMuteBox {
customMuteBox($shown)
}
else {
EventMutedBoxView(shown: $shown)
}
}
if shown {
Event
self.content
}
}
.onReceive(handle_notify(.new_mutes)) { mutes in
Expand All @@ -73,11 +61,34 @@ struct MutedEventView: View {
}
}

/// A box that instructs the user about a content that has been muted.
struct EventMutedBoxView: View {
@Binding var shown: Bool

var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(DamusColors.adaptableGrey)

HStack {
Text("Note from a user you've muted", comment: "Text to indicate that what is being shown is a note from a user who has been muted.")
Spacer()
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a note from a user who has been muted.")) {
shown.toggle()
}
}
.padding(10)
}
}
}

struct MutedEventView_Previews: PreviewProvider {

static var previews: some View {

MutedEventView(damus_state: test_damus_state, event: test_note, selected: false)
EventMutingContainerView(damus_state: test_damus_state, event: test_note) {
EventView(damus: test_damus_state, event: test_note)
}
.frame(width: .infinity, height: 50)
}
}
Loading

0 comments on commit 66310a1

Please sign in to comment.