Skip to content
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

Adds Polls UI Components + Introducing View Container Builder #3435

Merged
merged 35 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
721d153
[Polls] Add `PollAttachmentView` + `ViewContainerBuilder` (#3374)
nuno-vieira Aug 21, 2024
caae109
[Polls] Add `PollResultsVC` (#3381)
nuno-vieira Aug 22, 2024
ac53bd0
[Polls] Comments + Suggestions + Anonymous Polls + LLC Fixes (#3398)
nuno-vieira Aug 28, 2024
c4a0c62
[Polls] Add Poll Creation Flow (#3433)
nuno-vieira Sep 26, 2024
be43620
Add `padding()` to ViewContainerBuilder + Remove ViewContainerBuilder…
nuno-vieira Sep 26, 2024
c376300
Fix votes impacting latest answers
nuno-vieira Sep 26, 2024
a65e413
Fix poll.ownVotes and poll.latestAnwers impacting each other by movin…
nuno-vieira Sep 26, 2024
1933c07
Fix Poll Repository Tests
nuno-vieira Sep 26, 2024
67f57e6
Fix reorder icon showing before adding a new option
nuno-vieira Sep 26, 2024
d0e0439
Fix anonymous comments logic
nuno-vieira Sep 26, 2024
fbf292e
Fix channel list message preview incorrect latest voter
nuno-vieira Sep 27, 2024
e55d9b1
Fix edit message appearing in poll attachment
nuno-vieira Sep 27, 2024
45c52be
Fix being able to tap on options in a closed poll
nuno-vieira Sep 27, 2024
4dc078e
Change min votes to 2 when maximum votes enabled since 1 maximum is n…
nuno-vieira Sep 27, 2024
9be494a
Fix trophy not showing for poll results winner
nuno-vieira Sep 27, 2024
0c8966a
Fix snapshot UI Tests
nuno-vieira Sep 27, 2024
42fff74
Fix section header tests
nuno-vieira Sep 27, 2024
1ff1e00
Remove unnecessary disabling of swiftlint rules
nuno-vieira Sep 27, 2024
f698986
PR Feedback
nuno-vieira Sep 27, 2024
12aa799
Change the order of the latest votes so that the first one is the mos…
nuno-vieira Sep 29, 2024
a209051
Adds the `PollAllOptionsListVC` component
nuno-vieira Sep 30, 2024
b857035
Add missing test coverage to `PollAllOptionsListVC`
nuno-vieira Sep 30, 2024
533dfd0
Fix poll question tappable on the All Options view
nuno-vieira Sep 30, 2024
80a8783
Fix not allow creating poll with only 1 option
nuno-vieira Sep 30, 2024
29f02b4
Fix snapshots of poll all options view
nuno-vieira Sep 30, 2024
c2cc39b
Fix Message Actions not scrollable when multiple options
nuno-vieira Sep 30, 2024
3958905
Update CHANGELOG.md
nuno-vieira Sep 30, 2024
05a231b
Add documentation to Polls UIKit
nuno-vieira Oct 1, 2024
70bde59
Merge branch 'develop' into add/polls-ui-base
nuno-vieira Oct 1, 2024
952a29d
Update Poll Configuration docs
nuno-vieira Oct 1, 2024
18707f9
Better consistency in Polls documentation titles
nuno-vieira Oct 1, 2024
8082dcf
Improve Poll State documentation structure
nuno-vieira Oct 1, 2024
d8806bc
Update Poll Vote List Controller documentation with new inits
nuno-vieira Oct 1, 2024
da009e4
[CI] Snapshots (#3441)
Stream-SDK-Bot Oct 1, 2024
723dd37
Remove unwanted file
nuno-vieira Oct 1, 2024
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
4 changes: 3 additions & 1 deletion .styles/config/vocabularies/Base/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ swappable
telehealth
subclassing
[Ss]ubview
scrollable
scrollable
HContainer
VContainer
2 changes: 2 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ disabled_rules:
- type_body_length
- opening_brace
- line_length
- switch_case_alignment
- notification_center_detachment

# TODO: https://github.com/GetStream/ios-issues-tracking/issues/538
- attributes # it should be included in `opt_in_rules`
Expand Down
31 changes: 30 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## StreamChat
### ✅ Added
- Add `ChannelMemberListSortingKey.userId` for sorting channel members by id [#3423](https://github.com/GetStream/stream-chat-swift/pull/3423)
- Add helper functions to `Poll` that extracts common domain logic [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
### 🐞 Fixed
- Fix old channel updates not being added to the channel list automatically [#3430](https://github.com/GetStream/stream-chat-swift/pull/3430)
- Keep consistent order in channel and member lists when sorting by key with many equal values [#3423](https://github.com/GetStream/stream-chat-swift/pull/3423)
- Recommendation: Always add at least one unique key to the query's sort
- Fix `PollOption.latestVotes` sorting [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
- Fix `Poll.latestAnswers` sorting [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
- Fix `Poll` updates not triggering message updates in `ChannelController` [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
- Fix `Poll.latestAnswers` being reset on events, causing "Add a comment" button to not update in the UI SDKs [#3398](https://github.com/GetStream/stream-chat-swift/pull/3398)
- Fix `PollVoteListController` resetting the first page when loading a new page [#3398](https://github.com/GetStream/stream-chat-swift/pull/3398)
- Fix `PollVoteListController` default sorting being from oldest to newest from the server response [#3398](https://github.com/GetStream/stream-chat-swift/pull/3398)
- Fix `PollVoteListQuery.pollId` not limiting the votes query to the given poll id [#3398](https://github.com/GetStream/stream-chat-swift/pull/3398)
### 🔄 Changed
- Deprecates `PollVoteListQuery(pollId:optionId:pagination:filter:)` initializer in favor of `(pollId:filter:pagination:)` [#3381](https://github.com/GetStream/stream-chat-swift/pull/3381)

## StreamChatUI
### ✅ Added
- ✨ Introducing `ViewContainerBuilder`, a new, easier way to customize views [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374) (Learn more by reading the docs [here](https://getstream.io/chat/docs/sdk/ios/uikit/custom-components/))
- Add `PollAttachmentView` component to render polls in the message list [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
- Add `PollResultsVC` component to show the results of a poll [#3381](https://github.com/GetStream/stream-chat-swift/pull/3381)
- Add `PollCommentListVC` component to show the comments of a poll [#3398](https://github.com/GetStream/stream-chat-swift/pull/3398)
- Add `PollCreationVC` component to create a poll in a channel [#3433](https://github.com/GetStream/stream-chat-swift/pull/3433)
- Add `PollAllOptionsListVC` component to show all the options of a poll [#3435](https://github.com/GetStream/stream-chat-swift/pull/3435)
- Add `ChatUserAvatarView.shouldShowOnlineIndicator` to disable the online indicator easily [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
### 🐞 Fixed
- Fix a crash with thematic breaks in markdown [#3437](https://github.com/GetStream/stream-chat-swift/pull/3437)
- In some cases channel was not added to the channel list automatically [#3430](https://github.com/GetStream/stream-chat-swift/pull/3430)
- Fix Message Actions Alert view not scrollable when the view has the exact same height as the screen [#3435](https://github.com/GetStream/stream-chat-swift/pull/3435)
### 🎭 New Localizations
Multiple localizations were added to Polls, for more details please check the strings file.
- `polls.subtitle.*`
- `polls.button.*`
- `polls.*`
- `alert.poll.*`
- `message.preview.poll-*`

# [4.63.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.63.0)
_September 12, 2024_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ struct PollPayload: Decodable {
var voteCount: Int
var latestAnswers: [PollVotePayload?]?
var options: [PollOptionPayload?]
var ownVotes: [PollVotePayload?]
var ownVotes: [PollVotePayload?]?
var custom: [String: RawJSON]?
var latestVotesByOption: [String: [PollVotePayload]]?
var voteCountsByOption: [String: Int]?
Expand All @@ -183,6 +183,11 @@ struct PollPayload: Decodable {
var votingVisibility: String?
var createdBy: UserPayload?

// Workaround for handling events. The backend always returns the `ownVotes` as an empty array.
// This would reset the ownVotes of a Poll, so we need to understand that this payload is from an event
// and ignore the `ownVotes` property.
var fromEvent: Bool = false

init(
allowAnswers: Bool,
allowUserSuggestedOptions: Bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,10 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
eventsController = client.eventsController()
ownVotesQuery = PollVoteListQuery(
pollId: pollId,
optionId: nil,
pagination: .init(pageSize: 25, cursor: nil),
filter: .and(
[.equal(.userId, to: client.currentUserId ?? ""), .equal(.pollId, to: pollId)]
)
),
pagination: .init(pageSize: 25, cursor: nil)
)
pollsRepository = client.pollsRepository
super.init()
Expand Down
66 changes: 53 additions & 13 deletions Sources/StreamChat/Database/DTOs/PollDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,20 @@ class PollDTO: NSManagedObject {
@NSManaged var maxVotesAllowed: NSNumber?
@NSManaged var votingVisibility: String?
@NSManaged var createdBy: UserDTO?
@NSManaged var latestAnswers: Set<PollVoteDTO>
nuno-vieira marked this conversation as resolved.
Show resolved Hide resolved
@NSManaged var latestVotes: Set<PollVoteDTO>
@NSManaged var message: MessageDTO?
@NSManaged var options: NSOrderedSet
@NSManaged var latestVotesByOption: Set<PollOptionDTO>


override func willSave() {
super.willSave()

// When the poll is updated, trigger message FRC update.
if let message = self.message, hasPersistentChangedValues, !message.hasChanges, !message.isDeleted {
message.id = message.id
}
}

static func loadOrCreate(
pollId: String,
context: NSManagedObjectContext,
Expand Down Expand Up @@ -70,7 +79,8 @@ extension PollDTO {
}

let optionsArray = (options.array as? [PollOptionDTO]) ?? []

let currentUserId = managedObjectContext?.currentUser?.user.id

return try Poll(
allowAnswers: allowAnswers,
allowUserSuggestedOptions: allowUserSuggestedOptions,
Expand All @@ -88,9 +98,20 @@ extension PollDTO {
maxVotesAllowed: maxVotesAllowed?.intValue,
votingVisibility: votingVisibility(from: votingVisibility),
createdBy: createdBy?.asModel(),
latestAnswers: latestAnswers.map { try $0.asModel() },
latestAnswers: latestVotes
.filter { $0.isAnswer }
.map { try $0.asModel() }
.sorted(by: { $0.createdAt > $1.createdAt }),
options: optionsArray.map { try $0.asModel() },
latestVotesByOption: latestVotesByOption.map { try $0.asModel() }
latestVotesByOption: latestVotesByOption.map { try $0.asModel() },
latestVotes: latestVotesByOption
.map(\.latestVotes)
.joined()
.map { try $0.asModel() }
.sorted(by: { $0.createdAt > $1.createdAt }),
ownVotes: latestVotes
.filter { !$0.isAnswer && $0.user?.id == currentUserId }
.map { try $0.asModel() }
)
}

Expand Down Expand Up @@ -163,18 +184,37 @@ extension NSManagedObjectContext {
return optionDto
} ?? []
)
pollDto.latestAnswers = try Set(
payload.latestAnswers?.compactMap { payload in

if let latestAnswers = payload.latestAnswers {
pollDto.latestVotes
.filter { $0.isAnswer }
.forEach {
pollDto.latestVotes.remove($0)
}

try latestAnswers.forEach { payload in
if let payload {
let answerDto = try savePollVote(payload: payload, query: nil, cache: cache)
answerDto.poll = pollDto
return answerDto
} else {
return nil
}
} ?? []
)

}
}

if let payloadOwnVotes = payload.ownVotes, !payload.fromEvent {
pollDto.latestVotes
.filter { !$0.isAnswer }
.forEach {
pollDto.latestVotes.remove($0)
}

try payloadOwnVotes.forEach { payload in
if let payload {
let voteDto = try savePollVote(payload: payload, query: nil, cache: cache)
voteDto.poll = pollDto
}
}
}

return pollDto
}

Expand Down
8 changes: 7 additions & 1 deletion Sources/StreamChat/Database/DTOs/PollOptionDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class PollOptionDTO: NSManagedObject {
@NSManaged var text: String
@NSManaged var custom: Data?
@NSManaged var poll: PollDTO?

// It contains both latestAnswers and ownVotes, plus every other vote.
// We can't have separate properties unless they have different entities.
// So the only way it would work would be to add a new PollAnswerDTO entity.
@NSManaged var latestVotes: Set<PollVoteDTO>

static func loadOrCreate(
Expand Down Expand Up @@ -56,7 +60,9 @@ extension PollOptionDTO {
return PollOption(
id: id,
text: text,
latestVotes: try latestVotes.map { try $0.asModel() },
latestVotes: try latestVotes
.map { try $0.asModel() }
.sorted(by: { $0.createdAt > $1.createdAt }),
extraData: extraData
)
}
Expand Down
28 changes: 14 additions & 14 deletions Sources/StreamChat/Database/DTOs/PollVoteDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ class PollVoteDTO: NSManagedObject {
@NSManaged var poll: PollDTO?
@NSManaged var user: UserDTO?
@NSManaged var queries: Set<PollVoteListQueryDTO>?


override func willSave() {
super.willSave()

// When the poll is updated, trigger message FRC update.
if let message = poll?.message, hasPersistentChangedValues, !message.hasChanges, !message.isDeleted {
message.id = message.id
}
}

static func loadOrCreate(
voteId: String,
poll: PollDTO,
Expand Down Expand Up @@ -75,7 +84,7 @@ extension PollVoteDTO {
extension NSManagedObjectContext {
@discardableResult
func savePollVotes(payload: PollVoteListResponse, query: PollVoteListQuery?, cache: PreWarmedCache?) -> [PollVoteDTO] {
let isFirstPage = query?.pagination.offset == 0
let isFirstPage = query?.pagination.cursor == nil && query?.pagination.offset == 0
if let filterHash = query?.queryHash, isFirstPage {
let queryDTO = PollVoteListQueryDTO.load(filterHash: filterHash, context: self)
queryDTO?.votes = []
Expand Down Expand Up @@ -131,7 +140,7 @@ extension NSManagedObjectContext {
let queryDTO = try saveQuery(query: query)
queryDTO?.votes.insert(dto)
}

return dto
}

Expand Down Expand Up @@ -178,7 +187,6 @@ extension NSManagedObjectContext {
if let optionId {
let currentVoteCount = poll.voteCountsByOption?[optionId] ?? 0
poll.voteCountsByOption?[optionId] = currentVoteCount + 1
poll.latestAnswers.remove(dto)
}

if let query = query {
Expand All @@ -187,7 +195,7 @@ extension NSManagedObjectContext {
}

option?.latestVotes.insert(dto)

return dto
}

Expand Down Expand Up @@ -227,16 +235,8 @@ extension NSManagedObjectContext {
extension PollVoteDTO {
static func pollVoteListFetchRequest(query: PollVoteListQuery) -> NSFetchRequest<PollVoteDTO> {
let request = NSFetchRequest<PollVoteDTO>(entityName: PollVoteDTO.entityName)

// Fetch results controller requires at least one sorting descriptor.
// At the moment, we do not allow changing the query sorting.
request.sortDescriptors = [.init(key: #keyPath(PollVoteDTO.createdAt), ascending: false)]

// If a filter exists, use is for the predicate. Otherwise, `nil` filter matches all reactions.
if let filterHash = query.filter?.filterHash {
request.predicate = NSPredicate(format: "ANY queries.filterHash == %@", filterHash)
}

request.predicate = NSPredicate(format: "ANY queries.filterHash == %@", query.queryHash)
return request
}
}
Expand Down
11 changes: 3 additions & 8 deletions Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,16 @@ extension NSManagedObjectContext {
}

func saveQuery(query: PollVoteListQuery) throws -> PollVoteListQueryDTO? {
guard let filterHash = query.filter?.filterHash else {
// A query without a filter doesn't have to be saved to the DB because it matches all users by default.
return nil
}

if let existingDTO = PollVoteListQueryDTO.load(filterHash: filterHash, context: self) {
if let existingDTO = PollVoteListQueryDTO.load(filterHash: query.queryHash, context: self) {
return existingDTO
}

let request = PollVoteListQueryDTO.fetchRequest(
keyPath: #keyPath(PollVoteListQueryDTO.filterHash),
equalTo: filterHash
equalTo: query.queryHash
)
let newDTO = NSEntityDescription.insertNewObject(into: self, for: request)
newDTO.filterHash = filterHash
newDTO.filterHash = query.queryHash

do {
newDTO.filterJSONData = try JSONEncoder.default.encode(query.filter)
Expand Down
3 changes: 2 additions & 1 deletion Sources/StreamChat/Database/DatabaseSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,8 @@ extension DatabaseSession {
}
}

if let poll = payload.poll {
if var poll = payload.poll {
poll.fromEvent = true
try savePoll(payload: poll, cache: nil)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24A335" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AttachmentDTO" representedClassName="AttachmentDTO" syncable="YES">
<attribute name="data" attributeType="Binary"/>
<attribute name="id" attributeType="String"/>
Expand Down Expand Up @@ -330,7 +330,7 @@
<attribute name="voteCountsByOption" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData"/>
<attribute name="votingVisibility" optional="YES" attributeType="String"/>
<relationship name="createdBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserDTO" inverseName="pollCreatedBy" inverseEntity="UserDTO"/>
<relationship name="latestAnswers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteDTO" inverseName="poll" inverseEntity="PollVoteDTO"/>
<relationship name="latestVotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteDTO" inverseName="poll" inverseEntity="PollVoteDTO"/>
<relationship name="latestVotesByOption" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="pollLatestVotes" inverseEntity="PollOptionDTO"/>
<relationship name="message" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MessageDTO" inverseName="poll" inverseEntity="MessageDTO"/>
<relationship name="options" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PollOptionDTO" inverseName="poll" inverseEntity="PollOptionDTO"/>
Expand All @@ -352,7 +352,7 @@
<attribute name="pollId" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="option" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollOptionDTO" inverseName="latestVotes" inverseEntity="PollOptionDTO"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="latestAnswers" inverseEntity="PollDTO"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PollDTO" inverseName="latestVotes" inverseEntity="PollDTO"/>
<relationship name="queries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollVoteListQueryDTO" inverseName="votes" inverseEntity="PollVoteListQueryDTO"/>
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserDTO" inverseName="votes" inverseEntity="UserDTO"/>
</entity>
Expand Down
1 change: 1 addition & 0 deletions Sources/StreamChat/Models/ChatMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ extension ChatMessage: Hashable {
guard lhs.localState == rhs.localState else { return false }
guard lhs.updatedAt == rhs.updatedAt else { return false }
guard lhs.allAttachments == rhs.allAttachments else { return false }
guard lhs.poll == rhs.poll else { return false }
guard lhs.author == rhs.author else { return false }
guard lhs.currentUserReactionsCount == rhs.currentUserReactionsCount else { return false }
guard lhs.text == rhs.text else { return false }
Expand Down
Loading
Loading