Skip to content

Updated Conversation Settings - 1 #459

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

Merged
merged 73 commits into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
f2cf0de
update nickname setting for 1-1 convo
May 14, 2025
d190b06
implement notification settings view model
May 14, 2025
383dac9
WIP: 1-1 convo settings view model
May 14, 2025
00f7762
implement 1-1 conversation settings view model and a little bit refac…
May 15, 2025
d419a30
fix clear messages action
May 15, 2025
4f77927
implement note-to-self conversation setting view model
May 15, 2025
6315312
clean
May 15, 2025
80adea9
implement community conversation setting view model
May 15, 2025
1ff8ffc
implement non-admin group conversation settings view model
May 15, 2025
08aa8ef
WIP: implement admin group conversation settings view model
May 15, 2025
2bbcbdd
update conversation settings buttons
May 15, 2025
414e29a
clean up dual input modal for group name and description settings
May 20, 2025
37b3683
WIP: Error state for modals
May 20, 2025
50a88d7
Merge branch 'dev' into updated-convo-settings
May 20, 2025
50e6a53
add error state for modals
May 21, 2025
c14ffa9
update fallback group avatar
May 21, 2025
31e3f2c
WIP: expandable label
May 22, 2025
adaf63b
Merge branch 'dev' into updated-convo-settings
May 27, 2025
4e10431
implement view more/less for group descriptions
May 27, 2025
6fb9e48
add search functionality to SessionTableViewController
May 29, 2025
8b1e42f
Merge branch 'dev' into updated-convo-settings
Jun 1, 2025
2a2448f
clean
Jun 2, 2025
27bc079
update logic for navigation controller pop completion handler
Jun 2, 2025
fc65645
fix group convo avatar
Jun 2, 2025
1f671cf
refactor Interaction.markAsDeleted with options
Jun 2, 2025
f917fd8
fix compiling issue
Jun 2, 2025
b575bb3
Merge branch 'dev' into updated-convo-settings
Jun 3, 2025
853e7eb
Merge branch 'dev' into updated-convo-settings
Jun 3, 2025
7e82db5
update error state
Jun 3, 2025
3a834be
WIP: input error state
Jun 4, 2025
7527fe9
input error state update
Jun 4, 2025
ff4d387
update input error state for loading account
Jun 4, 2025
c7ff204
fix the UI issue on group description view more/less
Jun 5, 2025
b9bf3b2
clean
Jun 5, 2025
df9bbf4
Merge branch 'dev' into updated-convo-settings
Jun 11, 2025
7c9edd7
Merge branch 'dev' into updated-convo-settings
Jun 11, 2025
02811da
Merge branch 'dev' into updated-convo-settings
Jun 11, 2025
7bfe860
clean
Jun 11, 2025
250229a
clean up outdated test
Jun 11, 2025
75765d5
clean up and update unit test
Jun 11, 2025
f4cf2ed
update build and version
Jun 11, 2025
e6a7c40
Merge branch 'dev' into updated-convo-settings
Jun 16, 2025
7ba39b6
shorten the options in convo settings screen after blocking
Jun 16, 2025
fbb7105
fix convo settings screen for users who are kicked from groups
Jun 16, 2025
833b2b6
update disappearing messages options subtitle
Jun 16, 2025
fcf3bea
fix all media screen title when entering from convo settings screen
Jun 16, 2025
6d3ce2b
update swipe actions for group admins to be `delete` rather than `leave`
Jun 16, 2025
49e6f82
unwrap delete contact modal description
Jun 16, 2025
f99e6d8
fix an issue where updating nicknames won't trigger a UI refresh
Jun 17, 2025
454d40c
fix clear messages logic and toast
Jun 18, 2025
a64cb35
show plural for messages deleted all the time
Jun 18, 2025
9d8ba13
update ban/block icon
Jun 18, 2025
2deb16e
fix pinning nts when nts is hidden
Jun 18, 2025
d2642c8
fix exactly 100 bytes names
Jun 18, 2025
b286a33
update error mode for display name in settings screen
Jun 18, 2025
8e5248f
fix set button in thread notification settings screen
Jun 18, 2025
4c69245
fix community invite contacts list
Jun 18, 2025
e85d4bf
show unblock modal before actions like reacting and deleting if needed
Jun 18, 2025
38a3bdc
fix block contact behaviour and UI
Jun 19, 2025
c3a5d9c
update search bar icons
Jun 19, 2025
aef2fec
update character limit on group description
Jun 19, 2025
3073728
add clear button to modal text inputs
Jun 19, 2025
c36ace8
fix an issue that users can set empty display name during onboarding
Jun 19, 2025
07c7945
fix notification firing for mentions only
Jun 19, 2025
c19b15c
fix an issue where no feedback is displayed after sending invites for…
Jun 19, 2025
591c13b
update group member sorting logic
Jun 20, 2025
c3d833e
fix group member sorting
Jun 20, 2025
38cb095
make group description modal text view dynamic height
Jun 20, 2025
feb3cbf
update resource and remove scaled(to:)
Jun 26, 2025
e4fbf01
restore sorting for group member management
Jun 26, 2025
42efccc
fix icons on UIContextualAction
Jun 26, 2025
b01dd60
Fixed broken unit tests
mpretty-cyro Jun 26, 2025
fad413a
Updated version and build numbers
mpretty-cyro Jun 27, 2025
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
8 changes: 8 additions & 0 deletions Session.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@
945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */; };
946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; };
9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; };
9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */; };
9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; };
947D7FD42D509FC900E8E413 /* SessionNetworkAPI+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */; };
947D7FD62D509FC900E8E413 /* SessionNetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */; };
947D7FD72D509FC900E8E413 /* SessionNetworkAPI+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */; };
Expand Down Expand Up @@ -1479,7 +1481,9 @@
945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddJobUniqueHash.swift; sourceTree = "<group>"; };
9471CAA72CACFB4E00090FB7 /* GenerateLicenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateLicenses.swift; sourceTree = "<group>"; };
9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModel.swift; sourceTree = "<group>"; };
947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = "<group>"; };
947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkAPI.swift; sourceTree = "<group>"; };
947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Database.swift"; sourceTree = "<group>"; };
947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Models.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3020,6 +3024,7 @@
children = (
B84A89BB25DE328A0040017D /* ProfilePictureVC.swift */,
FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */,
9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */,
FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */,
);
path = Settings;
Expand Down Expand Up @@ -3181,6 +3186,7 @@
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */,
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */,
9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */,
FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */,
7B8914762A7CAAE200A4C627 /* SessionHostingViewController.swift */,
FDA335F42D911576007E0EB6 /* SessionImageView.swift */,
Expand Down Expand Up @@ -5861,6 +5867,7 @@
9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */,
FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */,
FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */,
9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */,
FDE754BA2C9B97B8002A2623 /* UIDevice+Utilities.swift in Sources */,
C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */,
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */,
Expand Down Expand Up @@ -6576,6 +6583,7 @@
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */,
C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */,
FDEF57242C3CF04700131302 /* (null) in Sources */,
9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Session/Closed Groups/EditGroupViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl
)
}

let searchable: Bool = false

let title: String = "groupEdit".localized()

var bannerInfo: AnyPublisher<InfoBanner.Info?, Never> {
Expand Down
10 changes: 5 additions & 5 deletions Session/Conversations/ConversationVC+Interaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2113,11 +2113,11 @@ extension ConversationVC:
explanation: NSAttributedString(string: deletionBehaviours.body),
warning: deletionBehaviours.warning.map { NSAttributedString(string: $0) },
options: deletionBehaviours.actions.map { action in
(
action.title,
action.state != .disabled,
action.state == .enabledAndDefaultSelected,
action.accessibility
ConfirmationModal.Info.Body.RadioOptionInfo(
title: action.title,
enabled: action.state != .disabled,
selected: action.state == .enabledAndDefaultSelected,
accessibility: action.accessibility
)
}
),
Expand Down
79 changes: 35 additions & 44 deletions Session/Conversations/ConversationVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
viewModel.threadData.threadIsBlocked != updatedThreadData.threadIsBlocked ||
viewModel.threadData.threadIsMessageRequest != updatedThreadData.threadIsMessageRequest ||
viewModel.threadData.threadRequiresApproval != updatedThreadData.threadRequiresApproval ||
viewModel.threadData.profile != updatedThreadData.profile
viewModel.threadData.profile != updatedThreadData.profile ||
viewModel.threadData.displayPictureFilename != updatedThreadData.displayPictureFilename
{
updateNavBarButtons(
threadData: updatedThreadData,
Expand Down Expand Up @@ -1371,51 +1372,41 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
return
}

switch threadData.threadVariant {
case .contact:
let profilePictureView = ProfilePictureView(
size: .navigation,
dataManager: viewModel.dependencies[singleton: .imageDataManager]
)
profilePictureView.update(
publicKey: threadData.threadId, // Contact thread uses the contactId
threadVariant: threadData.threadVariant,
displayPictureFilename: nil,
profile: threadData.profile,
additionalProfile: nil,
using: viewModel.dependencies
)
profilePictureView.customWidth = (44 - 16) // Width of the standard back button
let profilePictureView = ProfilePictureView(
size: .navigation,
dataManager: viewModel.dependencies[singleton: .imageDataManager]
)
profilePictureView.update(
publicKey: threadData.threadId, // Contact thread uses the contactId
threadVariant: threadData.threadVariant,
displayPictureFilename: threadData.displayPictureFilename,
profile: threadData.profile,
additionalProfile: nil,
using: viewModel.dependencies
)
profilePictureView.customWidth = (44 - 16) // Width of the standard back button

let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer)

let settingsButtonItem: UIBarButtonItem = UIBarButtonItem(customView: profilePictureView)
settingsButtonItem.accessibilityLabel = "More options"
settingsButtonItem.isAccessibilityElement = true

if shouldHaveCallButton {
let callButton = UIBarButtonItem(
image: UIImage(named: "Phone"),
style: .plain,
target: self,
action: #selector(startCall)
)
callButton.accessibilityLabel = "Call"
callButton.isAccessibilityElement = true

navigationItem.rightBarButtonItems = [settingsButtonItem, callButton]
}
else {
navigationItem.rightBarButtonItems = [settingsButtonItem]
}

default:
let rightBarButtonItem: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "Gear"), style: .plain, target: self, action: #selector(openSettings))
rightBarButtonItem.accessibilityLabel = "More options"
rightBarButtonItem.isAccessibilityElement = true

navigationItem.rightBarButtonItems = [rightBarButtonItem]
let settingsButtonItem: UIBarButtonItem = UIBarButtonItem(customView: profilePictureView)
settingsButtonItem.accessibilityLabel = "More options"
settingsButtonItem.isAccessibilityElement = true

if shouldHaveCallButton {
let callButton = UIBarButtonItem(
image: UIImage(named: "Phone"),
style: .plain,
target: self,
action: #selector(startCall)
)
callButton.accessibilityLabel = "Call"
callButton.isAccessibilityElement = true

navigationItem.rightBarButtonItems = [settingsButtonItem, callButton]
}
else {
navigationItem.rightBarButtonItems = [settingsButtonItem]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import SessionMessagingKit
import SessionUtilitiesKit
import SessionSnodeKit

class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, ObservableTableSource {
class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource {
typealias TableItem = String

public let dependencies: Dependencies
Expand Down Expand Up @@ -47,10 +47,6 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga

// MARK: - Config

enum NavItem: Equatable {
case save
}

public enum Section: SessionTableSection {
case type
case timerLegacy
Expand Down Expand Up @@ -84,6 +80,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga

// MARK: - Content

let searchable: Bool = false
let title: String = "disappearingMessages".localized()
lazy var subtitle: String? = {
switch (threadVariant, isNoteToSelf) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.

import Foundation
import Combine
import GRDB
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
import SessionSnodeKit

class ThreadNotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource {
public let dependencies: Dependencies
public let navigatableState: NavigatableState = NavigatableState()
public let state: TableDataState<Section, TableItem> = TableDataState()
public let observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState()

private let threadViewModel: SessionThreadViewModel
private var threadViewModelSubject: CurrentValueSubject<SessionThreadViewModel, Never>

// MARK: - Initialization

init(
threadViewModel: SessionThreadViewModel,
using dependencies: Dependencies
) {
self.dependencies = dependencies
self.threadViewModel = threadViewModel
self.threadViewModelSubject = CurrentValueSubject(threadViewModel)
}

// MARK: - Config

public enum Section: SessionTableSection {
case type
}

public enum TableItem: Differentiable {
case allMessages
case mentionsOnly
case mute
}

// MARK: - Content

let searchable: Bool = false
let title: String = "sessionNotifications".localized()

lazy var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> = threadViewModelSubject
.map { [threadViewModel] updatedThreadViewModel -> Bool in
// Need to explicitly compare values because 'lastChangeTimestampMs' will differ
return (
updatedThreadViewModel.threadOnlyNotifyForMentions != threadViewModel.threadOnlyNotifyForMentions ||
updatedThreadViewModel.threadMutedUntilTimestamp != threadViewModel.threadMutedUntilTimestamp
)
}
.removeDuplicates()
.map { [weak self] shouldShowConfirmButton -> SessionButton.Info? in
guard shouldShowConfirmButton else { return nil }

return SessionButton.Info(
style: .bordered,
title: "set".localized(),
isEnabled: true,
accessibility: Accessibility(
identifier: "Set button",
label: "Set button"
),
minWidth: 110,
onTap: {
self?.saveChanges()
self?.dismissScreen()
}
)
}
.eraseToAnyPublisher()

lazy var observation: TargetObservation = ObservationBuilder
.subject(threadViewModelSubject)
.compactMap { [weak self] threadViewModel -> [SectionModel]? in self?.content(threadViewModel) }

private func content(_ threadViewModel: SessionThreadViewModel) -> [SectionModel] {
let notificationTypeSection = SectionModel(
model: .type,
elements: [
SessionCell.Info(
id: .allMessages,
leadingAccessory: .icon(.volume2),
title: "notificationsAllMessages".localized(),
trailingAccessory: .radio(
isSelected: (threadViewModel.threadOnlyNotifyForMentions != true && threadViewModel.threadMutedUntilTimestamp == nil),
accessibility: Accessibility(
identifier: "All messages - Radio"
)
),
accessibility: Accessibility(
identifier: "All messages notification setting",
label: "All messages"
),
onTap: { [weak self] in
self?.threadViewModelSubject.send(
threadViewModel.with(
threadMutedUntilTimestamp: nil
).with(
threadOnlyNotifyForMentions: false
)
)
}
),

SessionCell.Info(
id: .mentionsOnly,
leadingAccessory: .icon(.atSign),
title: "notificationsMentionsOnly".localized(),
trailingAccessory: .radio(
isSelected: (threadViewModel.threadOnlyNotifyForMentions == true),
accessibility: Accessibility(
identifier: "Mentions only - Radio"
)
),
accessibility: Accessibility(
identifier: "Mentions only notification setting",
label: "Mentions only"
),
onTap: { [weak self] in
self?.threadViewModelSubject.send(
threadViewModel.with(
threadMutedUntilTimestamp: nil
).with(
threadOnlyNotifyForMentions: true
)
)
}
),

SessionCell.Info(
id: .mute,
leadingAccessory: .icon(.volumeOff),
title: "notificationsMute".localized(),
trailingAccessory: .radio(
isSelected: (threadViewModel.threadMutedUntilTimestamp != nil),
accessibility: Accessibility(
identifier: "Mute - Radio"
)
),
accessibility: Accessibility(
identifier: "\(ThreadSettingsViewModel.self).mute",
label: "Mute notifications"
),
onTap: { [weak self] in
self?.threadViewModelSubject.send(
threadViewModel.with(
threadMutedUntilTimestamp: Date.distantFuture.timeIntervalSince1970
).with(
threadOnlyNotifyForMentions: false
)
)
}
)
].compactMap { $0 }
)

return [notificationTypeSection]
}

// MARK: - Functions

private func saveChanges() {
let threadViewModel: SessionThreadViewModel = self.threadViewModelSubject.value

guard self.threadViewModel != threadViewModel else { return }

dependencies[singleton: .storage].writeAsync { db in
try SessionThread
.filter(id: threadViewModel.threadId)
.updateAll(
db,
SessionThread.Columns.onlyNotifyForMentions
.set(to: threadViewModel.threadOnlyNotifyForMentions),
SessionThread.Columns.mutedUntilTimestamp
.set(to: threadViewModel.threadMutedUntilTimestamp)
)
}
}
}
Loading