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

Add inline error message banners for polls #504

Merged
merged 3 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Dismiss keyboard when tapping on the empty message list [#513](https://github.com/GetStream/stream-chat-swiftui/pull/513)
- Reset composer text when there is provisional text (e.g. Japanese - kana keyboard) but the text is reset to empty string [#512](https://github.com/GetStream/stream-chat-swiftui/pull/512)

### 🔄 Changed
- Show inline alert banner when encountering a failure while interacting with polls [#504](https://github.com/GetStream/stream-chat-swiftui/pull/504)

# [4.57.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.57.0)
_June 07, 2024_

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ public struct ChatChannelView<Factory: ViewFactory>: View, KeyboardReadable {
)
.padding(.bottom, keyboardShown || !tabBarAvailable || generatingSnapshot ? 0 : bottomPadding)
.ignoresSafeArea(.container, edges: tabBarAvailable ? .bottom : [])
.alertBanner(isPresented: $viewModel.showAlertBanner)
.accessibilityElement(children: .contain)
.accessibilityIdentifier("ChatChannelView")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
@Published public var listId = UUID().uuidString

@Published public var showScrollToLatestButton = false
@Published var showAlertBanner = false

@Published public var currentDateString: String?
@Published public var messages = LazyCachedMapCollection<ChatMessage>() {
Expand Down Expand Up @@ -191,6 +192,13 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(onShowChannelAlertBanner),
name: .showChannelAlertBannerNotification,
object: nil
)

if messageController == nil {
NotificationCenter.default.addObserver(
self,
Expand All @@ -213,6 +221,11 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
}
}

@objc
private func onShowChannelAlertBanner() {
showAlertBanner = true
}

@objc
private func didReceiveMemoryWarning() {
ImageCache.shared.removeAll()
Expand Down Expand Up @@ -811,8 +824,12 @@ let firstMessageKey = "firstMessage"
let lastMessageKey = "lastMessage"

extension Notification.Name {
/// A notification for notifying when an error occured and an alert banner should be shown at the top of the message list.
static let showChannelAlertBannerNotification = Notification.Name("showChannelAlertBannerNotification")

/// A notification for notifying when message dismissed a sheet.
static let messageSheetHiddenNotification = Notification.Name("messageSheetHiddenNotification")

/// A notification for notifying when message view displays a sheet.
///
/// When a sheet is presented, the message cell is not reloaded.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ struct PollAllOptionsView: View {
}
.padding()
}
.alert(isPresented: $viewModel.errorShown) {
Alert.defaultErrorAlert
}
.toolbar {
ToolbarItem(placement: .principal) {
Text(L10n.Message.Polls.Toolbar.optionsTitle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,6 @@ public struct PollAttachmentView<Factory: ViewFactory>: View {
}
}
}
.alert(isPresented: $viewModel.errorShown) {
Alert.defaultErrorAlert
}
.disabled(!viewModel.canInteract)
.padding()
.modifier(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class PollAttachmentViewModel: ObservableObject, PollControllerDelegate {

static let numberOfVisibleOptionsShown = 10
private var isCastingVote = false
private var isClosingPoll = false
@Published private var isClosingPoll = false

@Injected(\.chatClient) var chatClient

Expand Down Expand Up @@ -62,6 +62,8 @@ public class PollAttachmentViewModel: ObservableObject, PollControllerDelegate {

/// If true, an action sheet is shown for closing the poll, otherwise hidden.
@Published public var endVoteConfirmationShown = false

@available(*, deprecated, message: "Replaced with inline alert banners displayed by the showChannelAlertBannerNotification")
@Published public var errorShown = false
laevandus marked this conversation as resolved.
Show resolved Hide resolved

/// If true, poll controls are in enabled state, otherwise disabled.
Expand Down Expand Up @@ -154,10 +156,10 @@ public class PollAttachmentViewModel: ObservableObject, PollControllerDelegate {
pollController.castPollVote(
answerText: comment,
optionId: nil
) { [weak self] error in
) { error in
if let error {
log.error("Error casting a vote \(error.localizedDescription)")
self?.errorShown = true
NotificationCenter.default.post(name: .showChannelAlertBannerNotification, object: nil)
}
}
commentText = ""
Expand Down Expand Up @@ -189,7 +191,7 @@ public class PollAttachmentViewModel: ObservableObject, PollControllerDelegate {
self?.isClosingPoll = false
if let error {
log.error("Error closing the poll \(error.localizedDescription)")
self?.errorShown = true
NotificationCenter.default.post(name: .showChannelAlertBannerNotification, object: nil)
}
}
}
Expand All @@ -205,10 +207,10 @@ public class PollAttachmentViewModel: ObservableObject, PollControllerDelegate {
suggestOptionText = ""
let isDuplicate = poll.options.contains(where: { $0.text.trimmed.caseInsensitiveCompare(option.trimmed) == .orderedSame })
guard !isDuplicate else { return }
pollController.suggestPollOption(text: option) { [weak self] error in
pollController.suggestPollOption(text: option) { error in
if let error {
log.error("Error closing the poll \(error.localizedDescription)")
self?.errorShown = true
NotificationCenter.default.post(name: .showChannelAlertBannerNotification, object: nil)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ struct PollCommentsView: View {
}
.padding()
}
.alert(isPresented: $viewModel.errorShown) {
Alert.defaultErrorAlert
}
.alertBanner(
isPresented: $viewModel.errorShown,
action: viewModel.refresh
)
.toolbar {
ToolbarItem(placement: .principal) {
Text(L10n.Message.Polls.Toolbar.commentsTitle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,19 @@ class PollCommentsViewModel: ObservableObject, PollVoteListControllerDelegate {
) {
self.commentsController = commentsController
self.pollController = pollController

commentsController.delegate = self
refresh()

// No animation for initial load
$comments
.dropFirst()
.map { _ in true }
.assignWeakly(to: \.animateChanges, on: self)
.store(in: &cancellables)
}

func refresh() {
loadingComments = true
commentsController.synchronize { [weak self] error in
guard let self else { return }
self.loadingComments = false
Expand All @@ -52,12 +63,6 @@ class PollCommentsViewModel: ObservableObject, PollVoteListControllerDelegate {
self.errorShown = true
}
}
// No animation for initial load
$comments
.dropFirst()
.map { _ in true }
.assignWeakly(to: \.animateChanges, on: self)
.store(in: &cancellables)
}

var showsAddCommentButton: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ struct PollOptionAllVotesView: View {
)
}
}
.alert(isPresented: $viewModel.errorShown) {
Alert.defaultErrorAlert
}
.alertBanner(
isPresented: $viewModel.errorShown,
action: viewModel.refresh
)
.toolbar {
ToolbarItem(placement: .principal) {
Text(viewModel.option.text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ class PollOptionAllVotesViewModel: ObservableObject, PollVoteListControllerDeleg
)
controller = InjectedValues[\.chatClient].pollVoteListController(query: query)
controller.delegate = self
refresh()

// No animation for initial load
$pollVotes
.dropFirst()
.map { _ in true }
.assignWeakly(to: \.animateChanges, on: self)
.store(in: &cancellables)
}

func refresh() {
controller.synchronize { [weak self] error in
guard let self else { return }
self.pollVotes = Array(self.controller.votes)
Expand All @@ -38,12 +49,6 @@ class PollOptionAllVotesViewModel: ObservableObject, PollVoteListControllerDeleg
self.errorShown = true
}
}
// No animation for initial load
$pollVotes
.dropFirst()
.map { _ in true }
.assignWeakly(to: \.animateChanges, on: self)
.store(in: &cancellables)
}

func onAppear(vote: PollVote) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import SwiftUI

extension View {
/// Presents an alert banner with a title at the top of the view.
///
/// - Parameters:
/// - title: A text string used as the title of the alert banner.
/// - isPresented: A binding to a Boolean value that determines whether to present the alert banner.
/// - action: An action which is added to the view through refreshable view modifier.
/// - duration: The amount if time after which the banner is dismissed automatically.
func alertBanner(
_ title: String = L10n.Alert.Error.title,
isPresented: Binding<Bool>,
action: (() -> Void)? = nil,
duration: TimeInterval = 3
) -> some View {
modifier(
AlertBannerViewModifier(
title: title,
isPresented: isPresented,
action: action,
duration: duration
)
)
}
}

private struct AlertBannerViewModifier: ViewModifier {
@Injected(\.colors) private var colors
let title: String
@Binding var isPresented: Bool
let action: (() -> Void)?
let duration: TimeInterval
@State private var timer: Timer?

func body(content: Content) -> some View {
VStack(spacing: 0) {
VStack {
if isPresented {
Text(title)
.font(.body)
.foregroundColor(Color(colors.staticColorText))
.padding(.init(top: 4, leading: 16, bottom: 4, trailing: 16))
.frame(maxWidth: .infinity)
.background(Color(colors.textLowEmphasis))
.transition(.move(edge: .top))
}
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.clipped()
content
}
.alertBannerRefreshable(action: action)
.animation(.easeIn, value: isPresented)
.onChange(of: isPresented) { newValue in
guard newValue else { return }
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { _ in
isPresented = false
}
}
}
}

// MARK: - Refreshable Compatibility

private struct AlertBannerActionViewModifier: ViewModifier {
let action: (() -> Void)?

func body(content: Content) -> some View {
if #available(iOS 15.0, *), let action {
content
.refreshable { action() }
laevandus marked this conversation as resolved.
Show resolved Hide resolved
} else {
content
}
}
}

private extension View {
func alertBannerRefreshable(action: (() -> Void)?) -> some View {
modifier(AlertBannerActionViewModifier(action: action))
}
}
24 changes: 20 additions & 4 deletions StreamChatSwiftUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
402C54492B6AAC0100672BFB /* StreamChatSwiftUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8465FBB52746873A00AF091E /* StreamChatSwiftUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4F198FDD2C0480EC00148F49 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */; };
4F6D83352C0F05040098C298 /* PollCommentsViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */; };
4F6D83512C1079A00098C298 /* AlertBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */; };
4F6D83542C1094220098C298 /* AlertBannerViewModifier_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */; };
4F7DD9A02BFC7C6100599AA6 /* ChatClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */; };
4F7DD9A22BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */; };
4FD3592A2C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD359292C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift */; };
Expand Down Expand Up @@ -570,6 +572,8 @@
4A65451E274BA170003C5FA8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollCommentsViewModel_Tests.swift; sourceTree = "<group>"; };
4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier.swift; sourceTree = "<group>"; };
4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier_Tests.swift; sourceTree = "<group>"; };
4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatClient+Extensions.swift"; sourceTree = "<group>"; };
4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatClientExtensions_Tests.swift; sourceTree = "<group>"; };
4FD359292C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollViewModel_Tests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1115,6 +1119,14 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
4F6D83522C108D470098C298 /* CommonViews */ = {
isa = PBXGroup;
children = (
4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */,
);
path = CommonViews;
sourceTree = "<group>";
};
82A1814528FD69E8005F9D43 /* Message Delivery Status */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1635,13 +1647,14 @@
isa = PBXGroup;
children = (
8465FCFA2746A95600AF091E /* ActionItemView.swift */,
84F2908D276B92A40045472D /* GalleryHeaderView.swift */,
8434E582277088D9001E1B83 /* TitleWithCloseButton.swift */,
4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */,
84AB7B1C2771F4AA00631A10 /* DiscardButtonView.swift */,
84F2908D276B92A40045472D /* GalleryHeaderView.swift */,
8465FD4B2746A95600AF091E /* LoadingView.swift */,
846608E2278C303800D3D7B3 /* TypingIndicatorView.swift */,
8421BCED27A43E14000F977D /* SearchBar.swift */,
844EF8EC2809AACD00CC82F9 /* NoContentView.swift */,
8421BCED27A43E14000F977D /* SearchBar.swift */,
8434E582277088D9001E1B83 /* TitleWithCloseButton.swift */,
846608E2278C303800D3D7B3 /* TypingIndicatorView.swift */,
);
path = CommonViews;
sourceTree = "<group>";
Expand Down Expand Up @@ -2001,6 +2014,7 @@
84E6EC24279AEE9F0017207B /* StreamChatTestCase.swift */,
84C94C7D27567CC2007FE2B9 /* ChatChannelList */,
84C94D472758BDB2007FE2B9 /* ChatChannel */,
4F6D83522C108D470098C298 /* CommonViews */,
84C94D52275A135F007FE2B9 /* Utils */,
);
path = Tests;
Expand Down Expand Up @@ -2592,6 +2606,7 @@
84F2908C276B91700045472D /* ZoomableScrollView.swift in Sources */,
842383E427678A4D00888CFC /* QuotedMessageView.swift in Sources */,
84289BE328071C7200282ABE /* ChatChannelInfoViewModel.swift in Sources */,
4F6D83512C1079A00098C298 /* AlertBannerViewModifier.swift in Sources */,
8465FD932746A95700AF091E /* PhotoAttachmentPickerView.swift in Sources */,
841B64C82774BA770016FF3B /* CommandsHandler.swift in Sources */,
8465FDC42746A95700AF091E /* ChatChannelListScreen.swift in Sources */,
Expand Down Expand Up @@ -2876,6 +2891,7 @@
84E04798284A444E00BAFA17 /* InternetConnectionMock.swift in Sources */,
84204BFE2C052BD600AE522B /* CreatePollView_Tests.swift in Sources */,
8413D90227A9654600A89432 /* SearchResultsView_Tests.swift in Sources */,
4F6D83542C1094220098C298 /* AlertBannerViewModifier_Tests.swift in Sources */,
84C94D1527578BF3007FE2B9 /* TestDispatchQueue.swift in Sources */,
84D419BA28EAD20C00F574F9 /* ChatMessageBubbles_Tests.swift in Sources */,
84C2042327917B6A0024D616 /* MessageListView_Tests.swift in Sources */,
Expand Down
Loading
Loading