diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index 889f57a75..f9c45d4f0 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -91,6 +91,7 @@ jobs: FASTLANE_USER: ${{ secrets.FASTLANE_USER }} FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} DIAWI_API_TOKEN: ${{ secrets.DIAWI_API_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Add or update PR comment with Ad-hoc release informations uses: NejcZdovc/comment-pr@v1 diff --git a/Brewfile b/Brewfile index f3c727fda..e7b7707b9 100644 --- a/Brewfile +++ b/Brewfile @@ -1,2 +1,3 @@ brew "xcodegen" brew "mint" +brew "getsentry/tools/sentry-cli" diff --git a/CHANGES.md b/CHANGES.md index dcf4afff8..048c6c97b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,45 @@ +## Changes in 1.9.0 (2022-08-24) + +🙌 Improvements + +- KeyBackup: Adapt changes from sdk, add an entry into encryption info view of a message. ([#6555](https://github.com/vector-im/element-ios/pull/6555)) +- Upgrade MatrixSDK version ([v0.23.16](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.16)). +- Display the option "Share invite link" only when the room is accessible by link. ([#6496](https://github.com/vector-im/element-ios/issues/6496)) +- New App Layout: Added missing empty states in room list and space bottom sheet ([#6514](https://github.com/vector-im/element-ios/issues/6514)) +- Groups: Support for groups has been removed now that Spaces are fully available. ([#6523](https://github.com/vector-im/element-ios/issues/6523)) +- Change text when swiping on room from Delete to Leave. ([#6568](https://github.com/vector-im/element-ios/issues/6568)) +- New App Layout: added suppport for room invites in the all chats screen ([#6600](https://github.com/vector-im/element-ios/issues/6600)) +- App Layout: UI tweaks for Tabs ([#6605](https://github.com/vector-im/element-ios/issues/6605)) +- New App Layout: Added onboarding screen ([#6607](https://github.com/vector-im/element-ios/issues/6607)) +- App Layout: last UI tweaks before RC ([#6608](https://github.com/vector-im/element-ios/issues/6608)) +- App Layout: Activated feature in BuildSettings ([#6616](https://github.com/vector-im/element-ios/issues/6616)) +- App Layout: Added usage measures ([#6618](https://github.com/vector-im/element-ios/issues/6618)) + +🐛 Bugfixes + +- RoomViewController: Wait for table view updates before checing read marker visibility. ([#5932](https://github.com/vector-im/element-ios/issues/5932)) +- Add a login and signup fallback SSO option for homeservers that don't offer a list of identity providers. ([#6569](https://github.com/vector-im/element-ios/issues/6569)) +- App Layout: fixed Cancel and Back on Spaces Bottom Sheet ([#6572](https://github.com/vector-im/element-ios/issues/6572)) +- App Layout: updated context menus according to last design update ([#6574](https://github.com/vector-im/element-ios/issues/6574)) +- App Layout: reintroduced existing Notification left markers on room cells ([#6578](https://github.com/vector-im/element-ios/issues/6578)) +- App Layout: Leaving a Space now sends user to All Chats ([#6581](https://github.com/vector-im/element-ios/issues/6581)) +- App Layout: added space invites in space bottom sheet ([#6599](https://github.com/vector-im/element-ios/issues/6599)) + +⚠️ API Changes + +- Reverts #6275, bringing the local DesignKit package back. ([#6586](https://github.com/vector-im/element-ios/pull/6586)) +- Communities: GroupsViewController etc have all been removed now that Spaces are available in the app. ([#6523](https://github.com/vector-im/element-ios/issues/6523)) + +🚧 In development 🚧 + +- Device manager: Add new session management screen. ([#6585](https://github.com/vector-im/element-ios/issues/6585)) + +Others + +- Sentry: Upload Dsyms to Sentry when building Alpha ([#6413](https://github.com/vector-im/element-ios/pull/6413)) +- Analytics: Log all errors to analytics ([#6611](https://github.com/vector-im/element-ios/pull/6611)) + + ## Changes in 1.8.27 (2022-08-12) Others diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 55cc0ae89..05b88688a 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.8.27 -CURRENT_PROJECT_VERSION = 1.8.27 +MARKETING_VERSION = 1.9.0 +CURRENT_PROJECT_VERSION = 1.9.0 diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 0e6209163..7315755ac 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -265,7 +265,6 @@ final class BuildSettings: NSObject { static let homeScreenShowFavouritesTab: Bool = true static let homeScreenShowPeopleTab: Bool = true static let homeScreenShowRoomsTab: Bool = true - static let homeScreenShowCommunitiesTab: Bool = true // MARK: - General Settings Screen @@ -348,7 +347,6 @@ final class BuildSettings: NSObject { static let roomSettingsScreenAllowChangingAccessSettings: Bool = true static let roomSettingsScreenAllowChangingHistorySettings: Bool = true static let roomSettingsScreenShowAddressSettings: Bool = true - static let roomSettingsScreenShowFlairSettings: Bool = true static let roomSettingsScreenShowAdvancedSettings: Bool = true static let roomSettingsScreenAdvancedShowEncryptToVerifiedOption: Bool = true @@ -421,5 +419,9 @@ final class BuildSettings: NSObject { static let syncLocalContacts: Bool = false // MARK: - New App Layout - static let newAppLayoutEnabled = false + static let newAppLayoutEnabled = true + + // MARK: - Device manager + + static let deviceManagerEnabled = false } diff --git a/DesignKit/Common.xcconfig b/DesignKit/Common.xcconfig new file mode 100644 index 000000000..eb4b88c16 --- /dev/null +++ b/DesignKit/Common.xcconfig @@ -0,0 +1,28 @@ +// +// Copyright 2021 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Config/AppIdentifiers.xcconfig" +#include "Config/AppVersion.xcconfig" + +PRODUCT_NAME = DesignKit +PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).designkit + +INFOPLIST_FILE = DesignKit/Info.plist + +SKIP_INSTALL = YES diff --git a/DesignKit/Debug.xcconfig b/DesignKit/Debug.xcconfig new file mode 100644 index 000000000..11a7288a4 --- /dev/null +++ b/DesignKit/Debug.xcconfig @@ -0,0 +1,20 @@ +// +// Copyright 2021 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Common.xcconfig" diff --git a/DesignKit/DesignKit.h b/DesignKit/DesignKit.h new file mode 100644 index 000000000..4ff68e722 --- /dev/null +++ b/DesignKit/DesignKit.h @@ -0,0 +1,27 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +//! Project version number for DesignKit. +FOUNDATION_EXPORT double DesignKitVersionNumber; + +//! Project version string for DesignKit. +FOUNDATION_EXPORT const unsigned char DesignKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/DesignKit/Extensions/UIFont.swift b/DesignKit/Extensions/UIFont.swift new file mode 100644 index 000000000..7804c8066 --- /dev/null +++ b/DesignKit/Extensions/UIFont.swift @@ -0,0 +1,55 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +public extension UIFont { + + // MARK: - Convenient methods + + /// Update current font with a SymbolicTraits + func vc_withTraits(_ traits: UIFontDescriptor.SymbolicTraits) -> UIFont { + guard let descriptor = fontDescriptor.withSymbolicTraits(traits) else { + return self + } + return UIFont(descriptor: descriptor, size: 0) // Size 0 means keep the size as it is + } + + /// Update current font with a given Weight + func vc_withWeight(weight: Weight) -> UIFont { + // Add the font weight to the descriptor + let weightedFontDescriptor = fontDescriptor.addingAttributes([ + UIFontDescriptor.AttributeName.traits: [ + UIFontDescriptor.TraitKey.weight: weight + ] + ]) + return UIFont(descriptor: weightedFontDescriptor, size: 0) + } + + // MARK: - Shortcuts + + var vc_bold: UIFont { + return self.vc_withTraits(.traitBold) + } + + var vc_semiBold: UIFont { + return self.vc_withWeight(weight: .semibold) + } + + var vc_italic: UIFont { + return self.vc_withTraits(.traitItalic) + } +} diff --git a/DesignKit/Info.plist b/DesignKit/Info.plist new file mode 100644 index 000000000..c0701c6d7 --- /dev/null +++ b/DesignKit/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/DesignKit/Release.xcconfig b/DesignKit/Release.xcconfig new file mode 100644 index 000000000..11a7288a4 --- /dev/null +++ b/DesignKit/Release.xcconfig @@ -0,0 +1,20 @@ +// +// Copyright 2021 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Common.xcconfig" diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarSize.swift b/DesignKit/Source/AvatarSize.swift similarity index 95% rename from RiotSwiftUI/Modules/Common/Avatar/Model/AvatarSize.swift rename to DesignKit/Source/AvatarSize.swift index 09daff1d2..bac46e6f3 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarSize.swift +++ b/DesignKit/Source/AvatarSize.swift @@ -17,8 +17,6 @@ import Foundation import UIKit -// TODO: Move into element-design-tokens repo. - // Figma Avatar Sizes: https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1258%3A19678 public enum AvatarSize: Int { case xxSmall = 16 diff --git a/DesignKit/Source/ColorValues.swift b/DesignKit/Source/ColorValues.swift new file mode 100644 index 000000000..338d1cfe8 --- /dev/null +++ b/DesignKit/Source/ColorValues.swift @@ -0,0 +1,52 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/** + Struct for holding colour values for a particular theme. + */ +public struct ColorValues: Colors { + + public let accent: UIColor + + public let alert: UIColor + + public let primaryContent: UIColor + + public let secondaryContent: UIColor + + public let tertiaryContent: UIColor + + public let quarterlyContent: UIColor + + public let quinaryContent: UIColor + + public let separator: UIColor + + public let system: UIColor + + public let tile: UIColor + + public let navigation: UIColor + + public let background: UIColor + + public let ems: UIColor + + public let namesAndAvatars: [UIColor] +} diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift new file mode 100644 index 000000000..bf3e9abd3 --- /dev/null +++ b/DesignKit/Source/Colors.swift @@ -0,0 +1,73 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Colors at https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1255%3A1104 +public protocol Colors { + + associatedtype ColorType + + /// - Focused/Active states + /// - CTAs + var accent: ColorType { get } + + /// - Error messages + /// - Content requiring user attention + /// - Notification, alerts + var alert: ColorType { get } + + /// - Text + /// - Icons + var primaryContent: ColorType { get } + + /// - Text + /// - Icons + var secondaryContent: ColorType { get } + + /// - Text + /// - Icons + var tertiaryContent: ColorType { get } + + /// - Text + /// - Icons + var quarterlyContent: ColorType { get } + + /// - separating lines and other UI components + var quinaryContent: ColorType { get } + + /// - System-based areas and backgrounds + var system: ColorType { get } + + /// Separating line + var separator: ColorType { get } + + /// Cards, tiles + var tile: ColorType { get } + + /// Top navigation background on iOS + var navigation: ColorType { get } + + /// Background UI color + var background: ColorType { get } + + /// Global color: The EMS brand's purple colour. + var ems: ColorType { get } + + /// - Names in chat timeline + /// - Avatars default states that include first name letter + var namesAndAvatars: [ColorType] { get } +} diff --git a/DesignKit/Source/ColorsSwiftUI.swift b/DesignKit/Source/ColorsSwiftUI.swift new file mode 100644 index 000000000..ea3ca6779 --- /dev/null +++ b/DesignKit/Source/ColorsSwiftUI.swift @@ -0,0 +1,69 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +/** + Struct for holding colors for use in SwiftUI. + */ +public struct ColorSwiftUI: Colors { + + public let accent: Color + + public let alert: Color + + public let primaryContent: Color + + public let secondaryContent: Color + + public let tertiaryContent: Color + + public let quarterlyContent: Color + + public let quinaryContent: Color + + public let separator: Color + + public var system: Color + + public let tile: Color + + public let navigation: Color + + public let background: Color + + public var ems: Color + + public let namesAndAvatars: [Color] + + init(values: ColorValues) { + accent = Color(values.accent) + alert = Color(values.alert) + primaryContent = Color(values.primaryContent) + secondaryContent = Color(values.secondaryContent) + tertiaryContent = Color(values.tertiaryContent) + quarterlyContent = Color(values.quarterlyContent) + quinaryContent = Color(values.quinaryContent) + separator = Color(values.separator) + system = Color(values.system) + tile = Color(values.tile) + navigation = Color(values.navigation) + background = Color(values.background) + ems = Color(values.ems) + namesAndAvatars = values.namesAndAvatars.map({ Color($0) }) + } +} diff --git a/DesignKit/Source/ColorsUIkit.swift b/DesignKit/Source/ColorsUIkit.swift new file mode 100644 index 000000000..3add385c3 --- /dev/null +++ b/DesignKit/Source/ColorsUIkit.swift @@ -0,0 +1,67 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/** + ObjC class for holding colors for use in UIKit. + */ +@objcMembers public class ColorsUIKit: NSObject { + + public let accent: UIColor + + public let alert: UIColor + + public let primaryContent: UIColor + + public let secondaryContent: UIColor + + public let tertiaryContent: UIColor + + public let quarterlyContent: UIColor + + public let quinaryContent: UIColor + + public let separator: UIColor + + public let system: UIColor + + public let tile: UIColor + + public let navigation: UIColor + + public let background: UIColor + + public let namesAndAvatars: [UIColor] + + init(values: ColorValues) { + accent = values.accent + alert = values.alert + primaryContent = values.primaryContent + secondaryContent = values.secondaryContent + tertiaryContent = values.tertiaryContent + quarterlyContent = values.quarterlyContent + quinaryContent = values.quinaryContent + separator = values.separator + system = values.system + tile = values.tile + navigation = values.navigation + background = values.background + namesAndAvatars = values.namesAndAvatars + } +} + diff --git a/DesignKit/Source/Fonts.swift b/DesignKit/Source/Fonts.swift new file mode 100644 index 000000000..1203a2888 --- /dev/null +++ b/DesignKit/Source/Fonts.swift @@ -0,0 +1,85 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// Describe fonts used in the application. +/// Font names are based on Element typograhy https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1362%3A0 which is based on Apple font text styles (UIFont.TextStyle): https://developer.apple.com/documentation/uikit/uifonttextstyle +/// Create a custom TextStyle enum (like DesignKit.Fonts.TextStyle) is also a possiblity +public protocol Fonts { + + associatedtype FontType + + /// The font for large titles. + var largeTitle: FontType { get } + + /// `largeTitle` with a Bold weight. + var largeTitleB: FontType { get } + + /// The font for first-level hierarchical headings. + var title1: FontType { get } + + /// `title1` with a Bold weight. + var title1B: FontType { get } + + /// The font for second-level hierarchical headings. + var title2: FontType { get } + + /// `title2` with a Bold weight. + var title2B: FontType { get } + + /// The font for third-level hierarchical headings. + var title3: FontType { get } + + /// `title3` with a Semi Bold weight. + var title3SB: FontType { get } + + /// The font for headings. + var headline: FontType { get } + + /// The font for subheadings. + var subheadline: FontType { get } + + /// The font for body text. + var body: FontType { get } + + /// `body` with a Semi Bold weight. + var bodySB: FontType { get } + + /// The font for callouts. + var callout: FontType { get } + + /// `callout` with a Semi Bold weight. + var calloutSB: FontType { get } + + /// The font for footnotes. + var footnote: FontType { get } + + /// `footnote` with a Semi Bold weight. + var footnoteSB: FontType { get } + + /// The font for standard captions. + var caption1: FontType { get } + + /// `caption1` with a Semi Bold weight. + var caption1SB: FontType { get } + + /// The font for alternate captions. + var caption2: FontType { get } + + /// `caption2` with a Semi Bold weight. + var caption2SB: FontType { get } +} diff --git a/DesignKit/Source/FontsSwiftUI.swift b/DesignKit/Source/FontsSwiftUI.swift new file mode 100644 index 000000000..83b4e820b --- /dev/null +++ b/DesignKit/Source/FontsSwiftUI.swift @@ -0,0 +1,91 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +/** + Struct for holding fonts for use in SwiftUI. + */ +public struct FontSwiftUI: Fonts { + + public let uiFonts: FontsUIKit + + public var largeTitle: Font + + public var largeTitleB: Font + + public var title1: Font + + public var title1B: Font + + public var title2: Font + + public var title2B: Font + + public var title3: Font + + public var title3SB: Font + + public var headline: Font + + public var subheadline: Font + + public var body: Font + + public var bodySB: Font + + public var callout: Font + + public var calloutSB: Font + + public var footnote: Font + + public var footnoteSB: Font + + public var caption1: Font + + public var caption1SB: Font + + public var caption2: Font + + public var caption2SB: Font + + public init(values: ElementFonts) { + self.uiFonts = FontsUIKit(values: values) + + self.largeTitle = values.largeTitle.font + self.largeTitleB = values.largeTitleB.font + self.title1 = values.title1.font + self.title1B = values.title1B.font + self.title2 = values.title2.font + self.title2B = values.title2B.font + self.title3 = values.title3.font + self.title3SB = values.title3SB.font + self.headline = values.headline.font + self.subheadline = values.subheadline.font + self.body = values.body.font + self.bodySB = values.bodySB.font + self.callout = values.callout.font + self.calloutSB = values.calloutSB.font + self.footnote = values.footnote.font + self.footnoteSB = values.footnoteSB.font + self.caption1 = values.caption1.font + self.caption1SB = values.caption1SB.font + self.caption2 = values.caption2.font + self.caption2SB = values.caption2SB.font + } +} diff --git a/DesignKit/Source/FontsUIkit.swift b/DesignKit/Source/FontsUIkit.swift new file mode 100644 index 000000000..ec65cdaa6 --- /dev/null +++ b/DesignKit/Source/FontsUIkit.swift @@ -0,0 +1,87 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/** + ObjC class for holding fonts for use in UIKit. + */ +@objcMembers public class FontsUIKit: NSObject, Fonts { + + public var largeTitle: UIFont + + public var largeTitleB: UIFont + + public var title1: UIFont + + public var title1B: UIFont + + public var title2: UIFont + + public var title2B: UIFont + + public var title3: UIFont + + public var title3SB: UIFont + + public var headline: UIFont + + public var subheadline: UIFont + + public var body: UIFont + + public var bodySB: UIFont + + public var callout: UIFont + + public var calloutSB: UIFont + + public var footnote: UIFont + + public var footnoteSB: UIFont + + public var caption1: UIFont + + public var caption1SB: UIFont + + public var caption2: UIFont + + public var caption2SB: UIFont + + public init(values: ElementFonts) { + self.largeTitle = values.largeTitle.uiFont + self.largeTitleB = values.largeTitleB.uiFont + self.title1 = values.title1.uiFont + self.title1B = values.title1B.uiFont + self.title2 = values.title2.uiFont + self.title2B = values.title2B.uiFont + self.title3 = values.title3.uiFont + self.title3SB = values.title3SB.uiFont + self.headline = values.headline.uiFont + self.subheadline = values.subheadline.uiFont + self.body = values.body.uiFont + self.bodySB = values.bodySB.uiFont + self.callout = values.callout.uiFont + self.calloutSB = values.calloutSB.uiFont + self.footnote = values.footnote.uiFont + self.footnoteSB = values.footnoteSB.uiFont + self.caption1 = values.caption1.uiFont + self.caption1SB = values.caption1SB.uiFont + self.caption2 = values.caption2.uiFont + self.caption2SB = values.caption2SB.uiFont + } +} diff --git a/Riot/Managers/Theme/ThemeV2.swift b/DesignKit/Source/ThemeV2.swift similarity index 70% rename from Riot/Managers/Theme/ThemeV2.swift rename to DesignKit/Source/ThemeV2.swift index 56a97a93a..dedc4d6df 100644 --- a/Riot/Managers/Theme/ThemeV2.swift +++ b/DesignKit/Source/ThemeV2.swift @@ -14,18 +14,29 @@ // limitations under the License. // +import Foundation import UIKit -import DesignKit -import DesignTokens /// Theme v2. May be named again as `Theme` when the migration completed. @objc public protocol ThemeV2 { /// Colors object - var colors: ElementUIColorsResolved { get } + var colors: ColorsUIKit { get } /// Fonts object - var fonts: ElementUIFonts { get } + var fonts: FontsUIKit { get } + + /// may contain more design components in future, like icons, audio files etc. +} + +/// Theme v2 for SwiftUI. +public protocol ThemeSwiftUIType { + + /// Colors object + var colors: ColorSwiftUI { get } + + /// Fonts object + var fonts: FontSwiftUI { get } /// may contain more design components in future, like icons, audio files etc. } diff --git a/DesignKit/Variants/Colors/Dark/DarkColors.swift b/DesignKit/Variants/Colors/Dark/DarkColors.swift new file mode 100644 index 000000000..88bd12ff3 --- /dev/null +++ b/DesignKit/Variants/Colors/Dark/DarkColors.swift @@ -0,0 +1,51 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit +import SwiftUI + +/// Dark theme colors. +public class DarkColors { + private static let values = ColorValues( + accent: UIColor(rgb:0x0DBD8B), + alert: UIColor(rgb:0xFF4B55), + primaryContent: UIColor(rgb:0xFFFFFF), + secondaryContent: UIColor(rgb:0xA9B2BC), + tertiaryContent: UIColor(rgb:0x8E99A4), + quarterlyContent: UIColor(rgb:0x6F7882), + quinaryContent: UIColor(rgb:0x394049), + separator: UIColor(rgb:0x21262C), + system: UIColor(rgb:0x21262C), + tile: UIColor(rgb:0x394049), + navigation: UIColor(rgb:0x21262C), + background: UIColor(rgb:0x15191E), + ems: UIColor(rgb: 0x7E69FF), + namesAndAvatars: [ + UIColor(rgb:0x368BD6), + UIColor(rgb:0xAC3BA8), + UIColor(rgb:0x03B381), + UIColor(rgb:0xE64F7A), + UIColor(rgb:0xFF812D), + UIColor(rgb:0x2DC2C5), + UIColor(rgb:0x5C56F5), + UIColor(rgb:0x74D12C) + ] + ) + + public static var uiKit = ColorsUIKit(values: values) + public static var swiftUI = ColorSwiftUI(values: values) +} diff --git a/DesignKit/Variants/Colors/Light/LightColors.swift b/DesignKit/Variants/Colors/Light/LightColors.swift new file mode 100644 index 000000000..93cb3eadb --- /dev/null +++ b/DesignKit/Variants/Colors/Light/LightColors.swift @@ -0,0 +1,57 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit +import SwiftUI + + +/// Light theme colors. +public class LightColors { + private static let values = ColorValues( + accent: UIColor(rgb:0x0DBD8B), + alert: UIColor(rgb:0xFF4B55), + primaryContent: UIColor(rgb:0x17191C), + secondaryContent: UIColor(rgb:0x737D8C), + tertiaryContent: UIColor(rgb:0x8D97A5), + quarterlyContent: UIColor(rgb:0xC1C6CD), + quinaryContent: UIColor(rgb:0xE3E8F0), + separator: UIColor(rgb:0xE3E8F0), + system: UIColor(rgb:0xF4F6FA), + tile: UIColor(rgb:0xF3F8FD), + navigation: UIColor(rgb:0xF4F6FA), + background: UIColor(rgb:0xFFFFFF), + ems: UIColor(rgb: 0x7E69FF), + namesAndAvatars: [ + UIColor(rgb:0x368BD6), + UIColor(rgb:0xAC3BA8), + UIColor(rgb:0x03B381), + UIColor(rgb:0xE64F7A), + UIColor(rgb:0xFF812D), + UIColor(rgb:0x2DC2C5), + UIColor(rgb:0x5C56F5), + UIColor(rgb:0x74D12C) + ] + ) + + public static var uiKit = ColorsUIKit(values: values) + public static var swiftUI = ColorSwiftUI(values: values) +} + + + + + diff --git a/DesignKit/Variants/Fonts/ElementFonts.swift b/DesignKit/Variants/Fonts/ElementFonts.swift new file mode 100644 index 000000000..e0a612f85 --- /dev/null +++ b/DesignKit/Variants/Fonts/ElementFonts.swift @@ -0,0 +1,150 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// Fonts at https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1362%3A0 +@objcMembers +public class ElementFonts { + + // MARK: - Types + + /// A wrapper to provide both a `UIFont` and a SwiftUI `Font` in the same type. + /// The need for this comes from `Font` not adapting for dynamic type until the app + /// is restarted (or working at all in Xcode Previews) when initialised from a `UIFont` + /// (even if that font was created with the appropriate metrics). + public struct SharedFont { + public let uiFont: UIFont + public let font: Font + } + + // MARK: - Setup + + public init() { + } + + // MARK: - Private + + /// Returns an instance of the font associated with the text style and scaled appropriately for the content size category defined in the trait collection. + /// Keep this method private method at the moment and create a DesignKit.Fonts.TextStyle if needed. + fileprivate func font(forTextStyle textStyle: UIFont.TextStyle, compatibleWith traitCollection: UITraitCollection? = nil) -> UIFont { + return UIFont.preferredFont(forTextStyle: textStyle, compatibleWith: traitCollection) + } +} + +// MARK: - Fonts protocol +extension ElementFonts: Fonts { + + public var largeTitle: SharedFont { + let uiFont = self.font(forTextStyle: .largeTitle) + return SharedFont(uiFont: uiFont, font: .largeTitle) + } + + public var largeTitleB: SharedFont { + let uiFont = self.largeTitle.uiFont.vc_bold + return SharedFont(uiFont: uiFont, font: .largeTitle.bold()) + } + + public var title1: SharedFont { + let uiFont = self.font(forTextStyle: .title1) + return SharedFont(uiFont: uiFont, font: .title) + } + + public var title1B: SharedFont { + let uiFont = self.title1.uiFont.vc_bold + return SharedFont(uiFont: uiFont, font: .title.bold()) + } + + public var title2: SharedFont { + let uiFont = self.font(forTextStyle: .title2) + return SharedFont(uiFont: uiFont, font: .title2) + } + + public var title2B: SharedFont { + let uiFont = self.title2.uiFont.vc_bold + return SharedFont(uiFont: uiFont, font: .title2.bold()) + } + + public var title3: SharedFont { + let uiFont = self.font(forTextStyle: .title3) + return SharedFont(uiFont: uiFont, font: .title3) + } + + public var title3SB: SharedFont { + let uiFont = self.title3.uiFont.vc_semiBold + return SharedFont(uiFont: uiFont, font: .title3.weight(.semibold)) + } + + public var headline: SharedFont { + let uiFont = self.font(forTextStyle: .headline) + return SharedFont(uiFont: uiFont, font: .headline) + } + + public var subheadline: SharedFont { + let uiFont = self.font(forTextStyle: .subheadline) + return SharedFont(uiFont: uiFont, font: .subheadline) + } + + public var body: SharedFont { + let uiFont = self.font(forTextStyle: .body) + return SharedFont(uiFont: uiFont, font: .body) + } + + public var bodySB: SharedFont { + let uiFont = self.body.uiFont.vc_semiBold + return SharedFont(uiFont: uiFont, font: .body.weight(.semibold)) + } + + public var callout: SharedFont { + let uiFont = self.font(forTextStyle: .callout) + return SharedFont(uiFont: uiFont, font: .callout) + } + + public var calloutSB: SharedFont { + let uiFont = self.callout.uiFont.vc_semiBold + return SharedFont(uiFont: uiFont, font: .callout.weight(.semibold)) + } + + public var footnote: SharedFont { + let uiFont = self.font(forTextStyle: .footnote) + return SharedFont(uiFont: uiFont, font: .footnote) + } + + public var footnoteSB: SharedFont { + let uiFont = self.footnote.uiFont.vc_semiBold + return SharedFont(uiFont: uiFont, font: .footnote.weight(.semibold)) + } + + public var caption1: SharedFont { + let uiFont = self.font(forTextStyle: .caption1) + return SharedFont(uiFont: uiFont, font: .caption) + } + + public var caption1SB: SharedFont { + let uiFont = self.caption1.uiFont.vc_semiBold + return SharedFont(uiFont: uiFont, font: .caption.weight(.semibold)) + } + + public var caption2: SharedFont { + let uiFont = self.font(forTextStyle: .caption2) + return SharedFont(uiFont: uiFont, font: .caption2) + } + + public var caption2SB: SharedFont { + let uiFont = self.caption2.uiFont.vc_semiBold + return SharedFont(uiFont: uiFont, font: .caption2.weight(.semibold)) + } +} diff --git a/DesignKit/target.yml b/DesignKit/target.yml new file mode 100644 index 000000000..e10f76f12 --- /dev/null +++ b/DesignKit/target.yml @@ -0,0 +1,33 @@ +name: DesignKit + +schemes: + DesignKit: + analyze: + config: Debug + archive: + config: Release + build: + targets: + DesignKit: + - running + - profiling + - analyzing + - archiving + profile: + config: Release + run: + config: Debug + disableMainThreadChecker: true + +targets: + DesignKit: + type: framework + platform: iOS + + configFiles: + Debug: Debug.xcconfig + Release: Release.xcconfig + + sources: + - path: . + - path: ../Riot/Categories/UIColor.swift diff --git a/Gemfile.lock b/Gemfile.lock index 722a75731..6590decc4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,37 +3,37 @@ GEM specs: CFPropertyList (3.0.5) rexml - activesupport (6.1.6) + activesupport (6.1.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.598.0) - aws-sdk-core (3.131.1) + aws-partitions (1.621.0) + aws-sdk-core (3.134.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.57.0) + aws-sdk-kms (1.58.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) aws-sdk-s3 (1.114.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.0) + aws-sigv4 (1.5.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - claide (1.0.3) + claide (1.1.0) clamp (1.3.2) cocoapods (1.11.3) addressable (~> 2.8) @@ -82,13 +82,13 @@ GEM rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.6) + dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) - excon (0.92.3) - faraday (1.10.0) + excon (0.92.4) + faraday (1.10.2) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -117,7 +117,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.206.2) + fastlane (2.209.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -159,16 +159,17 @@ GEM fastlane-plugin-brew (0.1.1) fastlane-plugin-diawi (2.1.0) rest-client (>= 2.0.0) - fastlane-plugin-versioning (0.5.0) + fastlane-plugin-sentry (1.12.2) + fastlane-plugin-versioning (0.5.1) fastlane-plugin-xcodegen (1.1.0) fastlane-plugin-brew (~> 0.1.1) ffi (1.15.5) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.22.0) - google-apis-core (>= 0.5, < 2.a) - google-apis-core (0.5.0) + google-apis-androidpublisher_v3 (0.25.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-core (0.7.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -177,27 +178,27 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.11.0) - google-apis-core (>= 0.5, < 2.a) - google-apis-playcustomapp_v1 (0.8.0) - google-apis-core (>= 0.5, < 2.a) - google-apis-storage_v1 (0.15.0) - google-apis-core (>= 0.5, < 2.a) + google-apis-iamcredentials_v1 (0.13.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-playcustomapp_v1 (0.10.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-storage_v1 (0.17.0) + google-apis-core (>= 0.7, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.36.2) + google-cloud-storage (1.38.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.17.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.3) + googleauth (1.2.0) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -209,7 +210,7 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.10.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) jmespath (1.6.1) json (2.6.2) @@ -221,7 +222,7 @@ GEM mini_magick (4.11.0) mini_mime (1.1.2) mini_portile2 (2.8.0) - minitest (5.15.0) + minitest (5.16.3) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.0.0) @@ -229,7 +230,7 @@ GEM nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - nokogiri (1.13.6) + nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) optparse (0.1.1) @@ -254,9 +255,9 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.1) + signet (0.17.0) addressable (~> 2.8) - faraday (>= 0.17.5, < 3.0) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) @@ -278,7 +279,7 @@ GEM tty-cursor (~> 0.7) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) uber (0.1.0) unf (0.1.4) @@ -287,10 +288,10 @@ GEM unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) - xcode-install (2.8.0) - claide (>= 0.9.1, < 1.1.0) + xcode-install (2.8.1) + claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.21.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -310,10 +311,11 @@ DEPENDENCIES cocoapods (~> 1.11.2) fastlane fastlane-plugin-diawi + fastlane-plugin-sentry fastlane-plugin-versioning fastlane-plugin-xcodegen slather xcode-install BUNDLED WITH - 2.3.9 + 2.3.20 diff --git a/Podfile b/Podfile index bfc7ad7f7..5f856ef64 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.23.15' +$matrixSDKVersion = '= 0.23.16' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/Podfile.lock b/Podfile.lock index 77370969e..e92bfbcac 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -72,11 +72,11 @@ PODS: - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - MatrixSDKCrypto (0.1.0) - - OLMKit (3.2.5): - - OLMKit/olmc (= 3.2.5) - - OLMKit/olmcpp (= 3.2.5) - - OLMKit/olmc (3.2.5) - - OLMKit/olmcpp (3.2.5) + - OLMKit (3.2.12): + - OLMKit/olmc (= 3.2.12) + - OLMKit/olmcpp (= 3.2.12) + - OLMKit/olmc (3.2.12) + - OLMKit/olmcpp (3.2.12) - PostHog (1.4.4) - ReadMoreTextView (3.0.1) - Realm (10.27.0): @@ -92,7 +92,7 @@ PODS: - Sentry/Core (7.15.0) - SideMenu (6.5.0) - SwiftBase32 (0.9.0) - - SwiftGen (6.5.1) + - SwiftGen (6.6.2) - SwiftJWT (3.6.200): - BlueCryptor (~> 1.0) - BlueECC (~> 1.1) @@ -193,12 +193,12 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: AnalyticsEvents: - :commit: b275ccb194a219a61b3100159d51cadbf7c9020c + :commit: 53ad46ba1ea1ee8f21139dda3c351890846a202f :git: https://github.com/matrix-org/matrix-analytics-events.git SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce - AnalyticsEvents: 333bf47d67dc628fadd29ce887b7ac93d8bd6e05 + AnalyticsEvents: 0cc8cf52da2fd464a2f39b788a295988151116ce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 @@ -223,7 +223,7 @@ SPEC CHECKSUMS: Logging: beeb016c9c80cf77042d62e83495816847ef108b MatrixSDK: 9ed379b45f6809fc573db53a30a4d09f960e5886 MatrixSDKCrypto: 4b9146d5ef484550341be056a164c6930038028e - OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 + OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: 9ca328bd7e700cc19703799785e37f77d1a130f2 @@ -231,7 +231,7 @@ SPEC CHECKSUMS: Sentry: 63ca44f5e0c8cea0ee5a07686b02e56104f41ef7 SideMenu: f583187d21c5b1dd04c72002be544b555a2627a2 SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17 - SwiftGen: a6d22010845f08fe18fbdf3a07a8e380fd22e0ea + SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c SwiftJWT: 88c412708f58c169d431d344c87bc79a87c830ae SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584 SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 @@ -243,4 +243,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 044b4d9f0d9485e1407019fb3c7b267458120a89 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 899701b2f..a4ed2f4c0 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,23 +1,5 @@ { "pins" : [ - { - "identity" : "element-design-tokens", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vector-im/element-design-tokens.git", - "state" : { - "revision" : "02ba42d9ec02f90370a6cfc35a68d7312696636c", - "version" : "0.0.2" - } - }, - { - "identity" : "element-x-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vector-im/element-x-ios", - "state" : { - "revision" : "0a199ee61126feb8c8a462200cb4749d6eb3ba77", - "version" : "1.0.1-202207011447" - } - }, { "identity" : "maplibre-gl-native-distribution", "kind" : "remoteSourceControl", @@ -62,15 +44,6 @@ "branch" : "main", "revision" : "0ffad3f7b45a6a4760db090d503b00f094bbecc0" } - }, - { - "identity" : "swiftui-introspect", - "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect.git", - "state" : { - "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", - "version" : "0.1.4" - } } ], "version" : 2 diff --git a/Riot/Assets/Base.lproj/Main.storyboard b/Riot/Assets/Base.lproj/Main.storyboard index c38d8d929..18e5cbd7f 100644 --- a/Riot/Assets/Base.lproj/Main.storyboard +++ b/Riot/Assets/Base.lproj/Main.storyboard @@ -171,7 +171,7 @@ - + @@ -257,7 +257,6 @@ - @@ -371,7 +370,7 @@ - + @@ -403,25 +402,6 @@ - - - - - - - - - - - - - - - - - - - @@ -506,7 +486,7 @@ - + @@ -523,16 +503,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json new file mode 100644 index 000000000..d6a6b5903 --- /dev/null +++ b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "all_chats_onboarding1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "all_chats_onboarding1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "all_chats_onboarding1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png new file mode 100644 index 000000000..95fb854c7 Binary files /dev/null and b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png new file mode 100644 index 000000000..40c13c07a Binary files /dev/null and b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png new file mode 100644 index 000000000..8cb11ba5d Binary files /dev/null and b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json new file mode 100644 index 000000000..68a004064 --- /dev/null +++ b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "all_chats_onboarding2.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.svg b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.svg new file mode 100644 index 000000000..72518cdd8 --- /dev/null +++ b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json new file mode 100644 index 000000000..fd0b40307 --- /dev/null +++ b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "all_chats_onboarding3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "all_chats_onboarding3@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "all_chats_onboarding3@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png new file mode 100644 index 000000000..274db9f56 Binary files /dev/null and b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png new file mode 100644 index 000000000..6c2ae7bbf Binary files /dev/null and b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png new file mode 100644 index 000000000..8bb136ca1 Binary files /dev/null and b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json b/Riot/Assets/Images.xcassets/Home/all_chats_edit_icon.imageset/Contents.json similarity index 88% rename from Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json rename to Riot/Assets/Images.xcassets/Home/all_chats_edit_icon.imageset/Contents.json index d50c95b02..f00e7d1d2 100644 --- a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Home/all_chats_edit_icon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "home_my_spaces_action.svg", + "filename" : "all_chats_edit_icon.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/Riot/Assets/Images.xcassets/Home/all_chats_edit_icon.imageset/all_chats_edit_icon.svg b/Riot/Assets/Images.xcassets/Home/all_chats_edit_icon.imageset/all_chats_edit_icon.svg new file mode 100644 index 000000000..6a56df918 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Home/all_chats_edit_icon.imageset/all_chats_edit_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/Home/all_chats_empty_list_placeholder_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Home/all_chats_empty_list_placeholder_icon.imageset/Contents.json new file mode 100644 index 000000000..90791b942 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Home/all_chats_empty_list_placeholder_icon.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "all_chats_empty_list_placeholder_icon.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Home/all_chats_empty_list_placeholder_icon.imageset/all_chats_empty_list_placeholder_icon.svg b/Riot/Assets/Images.xcassets/Home/all_chats_empty_list_placeholder_icon.imageset/all_chats_empty_list_placeholder_icon.svg new file mode 100644 index 000000000..b3b3d4802 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Home/all_chats_empty_list_placeholder_icon.imageset/all_chats_empty_list_placeholder_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/Home/all_chats_spaces_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Home/all_chats_spaces_icon.imageset/Contents.json new file mode 100644 index 000000000..20c13cc1f --- /dev/null +++ b/Riot/Assets/Images.xcassets/Home/all_chats_spaces_icon.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "all_chats_spaces_icon.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Home/all_chats_spaces_icon.imageset/all_chats_spaces_icon.svg b/Riot/Assets/Images.xcassets/Home/all_chats_spaces_icon.imageset/all_chats_spaces_icon.svg new file mode 100644 index 000000000..681bcbefc --- /dev/null +++ b/Riot/Assets/Images.xcassets/Home/all_chats_spaces_icon.imageset/all_chats_spaces_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg b/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg deleted file mode 100644 index 3b956bb6b..000000000 --- a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Riot/Assets/ar.lproj/Vector.strings b/Riot/Assets/ar.lproj/Vector.strings index 835a0195e..d1752ce51 100644 --- a/Riot/Assets/ar.lproj/Vector.strings +++ b/Riot/Assets/ar.lproj/Vector.strings @@ -76,7 +76,7 @@ "joined" = "مُنضَمّ"; "skip" = "تَخَطِّي"; "close" = "إغلاق"; -"sending" = "يَجري الإرسَال"; +"sending" = "جاري الإرسَال"; "send_to" = "إرسَال إلى %@"; "collapse" = "تضييق"; "rename" = "إعَادة التسمية"; @@ -189,8 +189,8 @@ "contacts_address_book_section" = "جِهاتُ الاِتِّصال المَحَلِّيَّة"; "directory_search_fail" = "فَشَلَ جَلبُ البَيَانَات"; "directory_searching_title" = "بَحثُ الدَّلِيلِ جَارٍ…"; -"directory_search_results_more_than" = ">عُثِرَ عَلَى %tu نَتائج لِ%@"; -"directory_search_results" = "عُثِرَ عَلَى %tu نَتائج لِ%@"; +"directory_search_results_more_than" = ">عُثِرَ عَلَى %1$tu نَتائج لِ%2$@"; +"directory_search_results" = "عُثِرَ عَلَى %1$tu نَتائج لِ%2$@"; "directory_search_results_title" = "تَصَفُحُ نَتائِجِ الدَّلِيل"; "directory_cell_description" = "%tu غُرَف"; @@ -350,11 +350,11 @@ "onboarding_use_case_existing_server_message" = "هَل تَتَطلَّع إلى الانْضِمام إلى الخادِم المَوجود؟"; "onboarding_use_case_skip_button" = "تَخَطي هَذا السُؤال"; /* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ -"onboarding_use_case_not_sure_yet" = "لَستَ مٌتَأكداً بَعدَ؟ يُمَكَّنَك %@"; +"onboarding_use_case_not_sure_yet" = "لَستَ مٌتَأكداً بَعدَ؟ %@"; "onboarding_use_case_community_messaging" = "المُجتَمَعَات"; "onboarding_use_case_work_messaging" = "الفَرِقَ"; "onboarding_use_case_personal_messaging" = "الأصَدقاء والعائِلة"; -"onboarding_use_case_message" = "سنساعَدَك على الاتصال."; +"onboarding_use_case_message" = "سنساعَدَك على الاتصال"; "onboarding_use_case_title" = "مَنْ الذي ستَتَحَدَّثَ إليه أكْثَر؟"; "onboarding_splash_page_4_message" = "Element رائِع أيضاً لمَكان العَمِلَ. تَثِقُ به أكثر المٌؤسَسات أمانًا في العالم."; "onboarding_splash_page_4_title_no_pun" = "مُراسَلة لفَريقك."; @@ -973,3 +973,58 @@ "attachment_size_prompt_title" = "تأكيد الحَجم للإرسَال"; "message_reply_to_sender_sent_their_location" = "شَارك موقعهُم."; "room_displayname_all_other_members_left" = "%@ (غادر)"; +"authentication_verify_email_waiting_title" = "قم بتأكيد بريدك الألكتروني."; +"authentication_verify_email_text_field_placeholder" = "البَريد الإلِكتُرونيّ"; +/* The placeholder will show the homeserver's domain */ +"authentication_verify_email_input_message" = "%@ يحتاج إلى التَّحَقُّق من حِسابك"; +"authentication_verify_email_input_title" = "أدخل البَريد الإلِكتُرونيّ الخاص بك"; +"authentication_cancel_flow_confirmation_message" = "لم يتم إنشاء حسابك بعد. إيقاف عملية التَّسجِيل؟"; +"authentication_server_selection_generic_error" = "لا يمكن العثور على خادم باستخدام هذا الرابط، يرجى التأكد من صحته."; +"authentication_server_selection_server_url" = "رابط الخادِم الرَّئيسي"; +"authentication_server_selection_register_message" = "ما هو عنوان الخادِم الخاص بك؟ هذا مثل منزل لجميع بياناتك"; +"authentication_server_selection_register_title" = "اختر الخادِم الرَّئيسي الخاص بك"; +"authentication_server_selection_login_message" = "ما هو عنوان الخادِم الخاص بك؟"; +"authentication_server_selection_login_title" = "الاتصال بالخادِم الرَّئيسي"; +"authentication_server_info_title_login" = "أين تعيش مُحادَثَاتك"; +"authentication_server_info_title" = "أين ستعيش مُحادَثَاتك"; +"authentication_login_forgot_password" = "نِسيان كَلِمَةُ المُرُور"; +"authentication_login_username" = "اِسم مُستَخدِم / البَريد الإلِكتُرونيّ / رَقم الهَاتِف"; +"authentication_login_title" = "مرحباً بعَوْدتك!"; +"authentication_registration_password_footer" = "يجب أن يتكون من 8 أحرف أو أكثر"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "يمكن للآخرين اكتشافك %@"; +"authentication_registration_username_footer" = "لا يمكنك تغيير هذا لاحقًا"; +"authentication_registration_username" = "اِسم مُستَخدِم"; + +// MARK: Authentication +"authentication_registration_title" = "أنشئ حِسابك"; +"onboarding_celebration_button" = "هيا بنا"; +"onboarding_celebration_message" = "توجه إلى الإعدَادَات في أي وقت لتحديث ملف التعريف الخاص بك"; +"onboarding_celebration_title" = "تَبَدو جَيدة!"; +"onboarding_avatar_accessibility_label" = "صورة المَلفّ شَّخصي"; +"onboarding_avatar_message" = "حان الوقت لوضع وجه للاسم"; +"onboarding_avatar_title" = "أضف صورة للمَلفّ الشَّخصي"; +"onboarding_display_name_max_length" = "اِسم العَرَض الخاص بك يجب أن يكون أقل من 256 حرفاً"; +"onboarding_display_name_hint" = "بالإمكان تغيير هذا لاحقاً"; +"onboarding_display_name_placeholder" = "اِسم العَرَض"; +"onboarding_display_name_message" = "سيَظَهر هذا عند إرسَال الرَسائِل."; +"onboarding_display_name_title" = "اختر اِسم العَرَض"; +"onboarding_personalization_skip" = "تَخَطِّي هذه الخَطوة"; +"onboarding_personalization_save" = "حِفظ والاِستِمرار"; +"onboarding_congratulations_home_button" = "خذني إلى الرَّئيسَة"; +"onboarding_congratulations_personalize_button" = "تَخصيص المَلفّ شَّخصي"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "تَمَّ إنشاء حِسابُك %@"; +"onboarding_congratulations_title" = "مَبْروك!"; +"saving" = "جاري الحِفظ"; + +// Activities +"loading" = "تَحْميل"; +"confirm" = "تَأْكيد"; +"edit" = "تعديل"; +"suggest" = "اقْتَرح"; +"add" = "إضافة"; +"existing" = "الحالي"; +"new_word" = "جديد"; +"stop" = "إيقاف"; +"joining" = "الانْضِمام إلى"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 5ae490f35..90fbf058d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -77,6 +77,7 @@ "suggest" = "Suggest"; "edit" = "Edit"; "confirm" = "Confirm"; +"invite_to" = "Invite to %@"; // Activities "loading" = "Loading"; @@ -893,6 +894,9 @@ Tap the + to start adding people."; "manage_session_not_trusted" = "Not trusted"; "manage_session_sign_out" = "Sign out of this session"; +// User sessions management +"user_sessions_settings" = "Manage sessions"; + // AuthenticatedSessionViewControllerFactory "authenticated_session_flow_not_supported" = "This app does not support the authentication mechanism on your homeserver."; @@ -2006,6 +2010,7 @@ Tap the + to start adding people."; "leave_space_only_action" = "Don't leave any rooms"; "leave_space_and_all_rooms_action" = "Leave all rooms and spaces"; "spaces_explore_rooms" = "Explore rooms"; +"spaces_explore_rooms_format" = "Explore %@"; "spaces_suggested_room" = "Suggested"; "spaces_explore_rooms_room_number" = "%@ rooms"; "spaces_explore_rooms_one_room" = "1 room"; @@ -2176,6 +2181,13 @@ Tap the + to start adding people."; "all_chats_edit_layout_activity_order" = "Sort by activity"; "all_chats_edit_layout_alphabetical_order" = "Sort A-Z"; "all_chats_all_filter" = "All"; +"all_chats_empty_view_title" = "%@\nis looking a little empty."; +"all_chats_empty_space_information" = "Spaces are a new way to group rooms and people. Add an existing room, or create a new one, using the bottom-right button."; +"all_chats_empty_view_information" = "The all-in-one secure chat app for teams, friends and organisations. Create a chat, or join an existing room, to get started."; +"all_chats_empty_list_placeholder_title" = "You’re all caught up."; +"all_chats_empty_unreads_placeholder_message" = "This is where you're unread messages will show up, when you have some."; +"all_chats_nothing_found_placeholder_title" = "Nothing found."; +"all_chats_nothing_found_placeholder_message" = "Try adjusting your search."; "room_recents_recently_viewed_section" = "Recently viewed"; @@ -2184,9 +2196,29 @@ Tap the + to start adding people."; "all_chats_edit_menu_leave_space" = "Leave %@"; "all_chats_edit_menu_space_settings" = "Space settings"; +"all_chats_onboarding_page_title1" = "Welcome to a new view!"; +"all_chats_onboarding_page_message1" = "To simplify your Element, tabs are now optional. Manage them using the top-right menu."; +"all_chats_onboarding_page_title2" = "Access Spaces"; +"all_chats_onboarding_page_message2" = "Access your Spaces (bottom-left) faster and easier than ever before."; +"all_chats_onboarding_page_title3" = "Give Feedback"; +"all_chats_onboarding_page_message3" = "Tap your profile to let us know what you think."; +"all_chats_onboarding_title" = "What's new"; +"all_chats_onboarding_try_it" = "Try it out"; + +// Mark: - Room invites + +"room_invites_empty_view_title" = "Nothing new."; +"room_invites_empty_view_information" = "This is where your invites appear."; + // Mark: - Space Selector "space_selector_title" = "My spaces"; +"space_selector_empty_view_title" = "No spaces yet."; +"space_selector_empty_view_information" = "Spaces are a way to group rooms and people. Create a space to get started."; +"space_selector_create_space" = "Create Space"; + +"space_detail_nav_title" = "Space detail"; +"space_invite_nav_title" = "Space invite"; // Mark: - Polls @@ -2320,6 +2352,10 @@ To enable access, tap Settings> Location and select Always"; "location_sharing_live_lab_promotion_text" = "Please note: this is a labs feature using a temporary implementation that allows the history of your shared location to be permanently visible to other people in the room."; "location_sharing_live_lab_promotion_activation" = "Enable live location sharing"; +// MARK: User sessions management + +"user_sessions_overview_title" = "Sessions"; + // MARK: - MatrixKit diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings index 34dc438b1..d24f42ab5 100644 --- a/Riot/Assets/es.lproj/Vector.strings +++ b/Riot/Assets/es.lproj/Vector.strings @@ -2384,7 +2384,7 @@ "onboarding_congratulations_home_button" = "Ir al inicio"; "onboarding_congratulations_personalize_button" = "Personalizar perfil"; /* The placeholder string contains the user's matrix ID */ -"onboarding_congratulations_message" = "Has creado tu cuenta, %@."; +"onboarding_congratulations_message" = "Tu cuenta %@ ha sido creada"; "onboarding_congratulations_title" = "¡Enhorabuena!"; "saving" = "Guardando"; @@ -2494,3 +2494,48 @@ "authentication_choose_password_not_verified_message" = "Comprueba tu bandeja de entrada"; "authentication_choose_password_text_field_placeholder" = "Nueva contraseña"; "authentication_forgot_password_waiting_button" = "Volver a enviar correo"; + +// Mark: - Space Selector + +"space_selector_title" = "Mis espacios"; +"all_chats_edit_menu_space_settings" = "Ajustes del espacio"; +"all_chats_edit_menu_leave_space" = "Salir de %@"; +"all_chats_user_menu_settings" = "Ajustes de usuario"; +"room_recents_recently_viewed_section" = "Visto recientemente"; +"all_chats_all_filter" = "Todos"; +"all_chats_edit_layout_alphabetical_order" = "Ordenar alfabéticamente"; +"all_chats_edit_layout_activity_order" = "Ordenar por última actividad"; +"all_chats_edit_layout_show_filters" = "Mostrar filtros"; +"all_chats_edit_layout_show_recents" = "Mostrar recientes"; +"all_chats_edit_layout_sorting_options_title" = "Ordenar mensajes por"; +"all_chats_edit_layout_add_filters_title" = "Filtrar tus mensajes"; +"all_chats_edit_layout_add_section_title" = "Añadir sección a inicio"; +"all_chats_edit_layout_unreads" = "Sin leer"; +"all_chats_edit_layout_recents" = "Recientes"; +"all_chats_edit_layout" = "Ajustes de disposición"; +"spaces_subspace_creation_visibility_message" = "El espacio se añadirá a %@."; +"spaces_subspace_creation_visibility_title" = "¿Qué tipo de subespacio quieres crear?"; +"spaces_create_subspace_title" = "Crear un subespacio"; +"spaces_add_subspace_title" = "Crear espacio dentro de %@"; +"password_validation_error_contain_symbol" = "Tiene un símbolo."; +"password_validation_error_contain_number" = "Tiene un número."; +"password_validation_error_contain_lowercase_letter" = "Tiene una letra minúscula."; +"password_validation_error_contain_uppercase_letter" = "Tiene una letra mayúscula."; +/* The placeholder will show a number */ +"password_validation_error_max_length" = "%d caracteres como máximo."; +/* The placeholder will show a number */ +"password_validation_error_min_length" = "Al menos %d caracteres."; +"authentication_verify_msisdn_invalid_phone_number" = "Número de teléfono no válido"; +/* The placeholder will show the phone number that was entered. */ +"authentication_verify_msisdn_waiting_message" = "Código enviado a %@"; +"authentication_verify_msisdn_waiting_title" = "Verifica tu número de teléfono"; +/* The placeholder will show the homeserver's domain */ +"authentication_verify_msisdn_input_message" = "%@ necesita verificar tu cuenta"; +"authentication_verify_msisdn_input_title" = "Escribe tu número de teléfono"; +"authentication_choose_password_not_verified_title" = "Correo sin verificar"; +"authentication_choose_password_submit_button" = "Restablecer contraseña"; +"authentication_choose_password_signout_all_devices" = "Cerrar sesión de todos los dispositivos"; +"authentication_choose_password_input_message" = "Asegúrate de que tiene al menos 8 caracteres"; +"authentication_choose_password_input_title" = "Elige una nueva contraseña"; +/* The placeholder will show the email address that was entered. */ +"authentication_forgot_password_waiting_message" = "Sigue las instrucciones enviadas a %@"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 1d7221745..1143d96d3 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2371,3 +2371,35 @@ "authentication_verify_email_text_field_placeholder" = "E-posti aadress"; /* The placeholder will show the homeserver's domain */ "authentication_verify_email_input_message" = "%@ soovib sinu kasutajakonto verifitseerimist"; +"location_sharing_map_loading_error" = "Kaardi laadimine ei õnnestu\nSee koduserver ei pruugi olla seadistatud kuvama kaarte"; + +// Mark: - Space Selector + +"space_selector_title" = "Minu kogukonnad"; +"all_chats_edit_menu_space_settings" = "Kogukonna seadistused"; +"all_chats_edit_menu_leave_space" = "Lahku %@ kogukonnast"; +"all_chats_user_menu_settings" = "Kasutaja seadistused"; +"room_recents_recently_viewed_section" = "Hiljuti vaadatud"; +"all_chats_all_filter" = "Kõik"; +"all_chats_edit_layout_alphabetical_order" = "Sorteeri A-Z tähestiku järjekorras"; +"all_chats_edit_layout_activity_order" = "Sorteeri aktiivsuse alusel"; +"all_chats_edit_layout_show_filters" = "Näita otsinguvalikuid"; +"all_chats_edit_layout_show_recents" = "Näita hiljutisi sõnumeid"; +"all_chats_edit_layout_sorting_options_title" = "Sõnumite sortimise alus"; +"all_chats_edit_layout_pin_spaces_title" = "Lisa oma kogukonnad avalehele"; +"all_chats_edit_layout_add_filters_message" = "Liigita sõnumeid automaatselt sinu eelistatud kategooriate alusel"; +"all_chats_edit_layout_add_filters_title" = "Jaga oma sõnumeid"; +"all_chats_edit_layout_add_section_message" = "Lihtsamaks ligipääsuks lisa valik avalehele"; +"all_chats_edit_layout_add_section_title" = "Lisa valik avalehele"; +"all_chats_edit_layout_unreads" = "Lugemata"; +"all_chats_edit_layout_recents" = "Hiljutised"; +"all_chats_edit_layout" = "Paigutuse seadistused"; +"all_chats_section_title" = "Vestlused"; + +// Mark: - All Chats + +"all_chats_title" = "Kõik vestlused"; +"spaces_subspace_creation_visibility_message" = "Loodud kogukond saab olema osa %@ kogukonnast."; +"spaces_subspace_creation_visibility_title" = "Missugust alamkogukonda sooviksid sa luua?"; +"spaces_create_subspace_title" = "Loo alamkogukond"; +"spaces_add_subspace_title" = "Loo uus alamkogukond olemasolevas kogukonnas %@"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index de1e77de8..94a024fd1 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2415,3 +2415,39 @@ // MARK: Authentication "authentication_registration_title" = "Fiók létrehozása"; "authentication_server_info_title_login" = "Ahol a beszélgetéseid lesznek"; +"location_sharing_map_loading_error" = "A térkép betöltése sikertelen\nEz a matrix szerver nincs beállítva, hogy térképet mutasson"; +"location_sharing_invalid_power_level_message" = "Az élő helymegosztáshoz ebben a szobában megfelelő jogosultságokra van szükséged."; +"location_sharing_invalid_power_level_title" = "Nincs jogosultságod a élő helymegosztáshoz"; + +// Mark: - Space Selector + +"space_selector_title" = "Tereim"; +"all_chats_edit_menu_space_settings" = "Tér beállítások"; +"all_chats_edit_menu_leave_space" = "Elhagy: %@"; +"all_chats_user_menu_settings" = "Felhasználói beállítások"; +"room_recents_recently_viewed_section" = "Nemrég megtekintett"; +"all_chats_all_filter" = "Mind"; +"all_chats_edit_layout_alphabetical_order" = "Rendezés A-Z"; +"all_chats_edit_layout_activity_order" = "Rendezés aktivitás szerint"; +"all_chats_edit_layout_show_filters" = "Szűrők megjelenítése"; +"all_chats_edit_layout_show_recents" = "Legfrissebbek megjelenítése"; +"all_chats_edit_layout_sorting_options_title" = "Üzenetek rendezése"; +"all_chats_edit_layout_pin_spaces_title" = "Terek kitűzése"; +"all_chats_edit_layout_add_filters_message" = "Üzeneteid automatikus szűrése az általad választott kategóriákba"; +"all_chats_edit_layout_add_filters_title" = "Üzenetek szűrése"; +"all_chats_edit_layout_add_section_message" = "Rész kitűzése a kezdő képernyőhöz a könnyebb hozzáféréshez"; +"all_chats_edit_layout_add_section_title" = "Rész hozzáadása a kezdőképernyőhöz"; +"all_chats_edit_layout_unreads" = "Olvasatlan"; +"all_chats_edit_layout_recents" = "Legutóbbiak"; +"all_chats_edit_layout" = "Kinézet beállításai"; +"all_chats_section_title" = "Csevegések"; + +// Mark: - All Chats + +"all_chats_title" = "Minden beszélgetés"; +"spaces_subspace_creation_visibility_message" = "Az elkészített tér hozzá lesz adva ehhez: %@."; +"spaces_subspace_creation_visibility_title" = "Milyen típusú al-teret szeretnél készíteni?"; +"spaces_create_subspace_title" = "Téren belüli tér készítése"; +"spaces_add_subspace_title" = "Tér készítés itt: %@"; +"authentication_choose_password_not_verified_message" = "Nézd meg a bejövő e-mailjeidet"; +"authentication_choose_password_not_verified_title" = "E-mail nincs ellenőrizve"; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 87df61cb1..bf4a16e7d 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2626,3 +2626,35 @@ "location_sharing_invalid_power_level_title" = "Anda tidak memiliki izin untuk membagikan lokasi"; "authentication_choose_password_not_verified_message" = "Periksa kotak masuk Anda"; "authentication_choose_password_not_verified_title" = "Email belum diverifikasi"; +"location_sharing_map_loading_error" = "Tidak dapat memuat peta\nHomeserver ini tidak diatur untuk menampilkan peta"; + +// Mark: - Space Selector + +"space_selector_title" = "Space saya"; +"all_chats_edit_menu_space_settings" = "Pengaturan space"; +"all_chats_edit_menu_leave_space" = "Tinggalkan %@"; +"all_chats_user_menu_settings" = "Pengaturan pengguna"; +"room_recents_recently_viewed_section" = "Baru saja dilihat"; +"all_chats_all_filter" = "Semua"; +"all_chats_edit_layout_alphabetical_order" = "Urutkan A-Z"; +"all_chats_edit_layout_activity_order" = "Urutkan berdasarkan aktivitas"; +"all_chats_edit_layout_show_filters" = "Tampikan saringan"; +"all_chats_edit_layout_show_recents" = "Tampilkan yang terkini"; +"all_chats_edit_layout_sorting_options_title" = "Urutkan pesan berdasarkan"; +"all_chats_edit_layout_pin_spaces_title" = "Sematkan space Anda"; +"all_chats_edit_layout_add_filters_message" = "Saring pesan Anda ke kategori pilihan Anda secara otomatis"; +"all_chats_edit_layout_add_filters_title" = "Saring pesan Anda"; +"all_chats_edit_layout_add_section_message" = "Sematkan bagian ke beranda untuk akses yang mudah"; +"all_chats_edit_layout_add_section_title" = "Tambahkan bagian ke beranda"; +"all_chats_edit_layout_unreads" = "Belum dibaca"; +"all_chats_edit_layout_recents" = "Terkini"; +"all_chats_edit_layout" = "Preferensi tata letak"; +"all_chats_section_title" = "Obrolan"; + +// Mark: - All Chats + +"all_chats_title" = "Semua obrolan"; +"spaces_subspace_creation_visibility_message" = "Space yang dibuat akan ditambahkan ke %@."; +"spaces_subspace_creation_visibility_title" = "Tipe subspace apa yang Anda ingin buat?"; +"spaces_create_subspace_title" = "Buat sebuah subspace"; +"spaces_add_subspace_title" = "Buat space dalam %@"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 9f042fdd0..49f4d1f1f 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2399,3 +2399,35 @@ "location_sharing_invalid_power_level_title" = "Non hai l'autorizzazione di condividere la posizione in tempo reale"; "authentication_choose_password_not_verified_message" = "Controlla la posta in arrivo"; "authentication_choose_password_not_verified_title" = "Email non verificata"; +"location_sharing_map_loading_error" = "Impossibile caricare la mappa\nQuesto homeserver non è configurato per mostrare mappe"; + +// Mark: - Space Selector + +"space_selector_title" = "I miei spazi"; +"all_chats_edit_menu_space_settings" = "Impostazioni spazio"; +"all_chats_edit_menu_leave_space" = "Esci da %@"; +"all_chats_user_menu_settings" = "Impostazioni utente"; +"room_recents_recently_viewed_section" = "Viste di recente"; +"all_chats_all_filter" = "Tutte"; +"all_chats_edit_layout_alphabetical_order" = "Ordina A-Z"; +"all_chats_edit_layout_activity_order" = "Ordina per attività"; +"all_chats_edit_layout_show_filters" = "Mostra filtri"; +"all_chats_edit_layout_show_recents" = "Mostra recenti"; +"all_chats_edit_layout_sorting_options_title" = "Ordina messaggi per"; +"all_chats_edit_layout_pin_spaces_title" = "Fissa i tuoi spazi"; +"all_chats_edit_layout_add_filters_message" = "Filtra automaticamente i tuoi messaggi in categorie di tua scelta"; +"all_chats_edit_layout_add_filters_title" = "Filtra i tuoi messaggi"; +"all_chats_edit_layout_add_section_message" = "Fissa le sezioni alla pagina principale per un facile accesso"; +"all_chats_edit_layout_add_section_title" = "Aggiungi sezione a pagina principale"; +"all_chats_edit_layout_unreads" = "Non lette"; +"all_chats_edit_layout_recents" = "Recenti"; +"all_chats_edit_layout" = "Preferenze layout"; +"all_chats_section_title" = "Chat"; + +// Mark: - All Chats + +"all_chats_title" = "Tutte le chat"; +"spaces_subspace_creation_visibility_message" = "Lo spazio creato verrà aggiunto a %@."; +"spaces_subspace_creation_visibility_title" = "Che tipo di sottospazio vuoi creare?"; +"spaces_create_subspace_title" = "Crea un sottospazio"; +"spaces_add_subspace_title" = "Crea spazio all'interno di %@"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index d45044134..faec28e3c 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -2400,3 +2400,35 @@ "location_sharing_invalid_power_level_title" = "Você não tem permissão para compartilhar localização ao vivo"; "authentication_choose_password_not_verified_message" = "Cheque sua inbox"; "authentication_choose_password_not_verified_title" = "Email não verificado"; +"location_sharing_map_loading_error" = "Incapaz de carregar mapa\nEste servidocasa não está configurado para exibir mapas"; + +// Mark: - Space Selector + +"space_selector_title" = "Meus espaços"; +"all_chats_edit_menu_space_settings" = "Ajustes de espaço"; +"all_chats_edit_menu_leave_space" = "Sair de %@"; +"all_chats_user_menu_settings" = "Ajustes de usuária(o)"; +"room_recents_recently_viewed_section" = "Vistos recentemente"; +"all_chats_all_filter" = "Todos"; +"all_chats_edit_layout_alphabetical_order" = "Ordenar A-Z"; +"all_chats_edit_layout_activity_order" = "Classificar por atividade"; +"all_chats_edit_layout_show_filters" = "Mostrar filtros"; +"all_chats_edit_layout_show_recents" = "Mostrar recentes"; +"all_chats_edit_layout_sorting_options_title" = "Classificar mensagens por"; +"all_chats_edit_layout_pin_spaces_title" = "Pinnar seus espaços"; +"all_chats_edit_layout_add_filters_message" = "Filtrar automaticamente suas menagens para dentro das categorias de sua escolha"; +"all_chats_edit_layout_add_filters_title" = "Filtrar suas mensagens"; +"all_chats_edit_layout_add_section_message" = "Pinnar seções a home para acesso fácil"; +"all_chats_edit_layout_add_section_title" = "Adicionar seção a home"; +"all_chats_edit_layout_unreads" = "Não-lidos"; +"all_chats_edit_layout_recents" = "Recentes"; +"all_chats_edit_layout" = "Preferências de layout"; +"all_chats_section_title" = "Chats"; + +// Mark: - All Chats + +"all_chats_title" = "Todos os chats"; +"spaces_subspace_creation_visibility_message" = "O espaço criado vai ser adicionado a %@."; +"spaces_subspace_creation_visibility_title" = "Que tipo de subespaço você quer criar?"; +"spaces_create_subspace_title" = "Criar um subespaço"; +"spaces_add_subspace_title" = "Criar espaço dentro de %@"; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 7b0a3f06e..2a92a9830 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2622,3 +2622,35 @@ "location_sharing_invalid_power_level_title" = "Nemáte oprávnenie na zdieľanie polohy v reálnom čase"; "authentication_choose_password_not_verified_message" = "Skontrolujte si doručenú poštu"; "authentication_choose_password_not_verified_title" = "E-mail nie je overený"; +"location_sharing_map_loading_error" = "Nie je možné načítať mapu\nTento domovský server nemusí byť nakonfigurovaný na zobrazovanie máp"; + +// Mark: - Space Selector + +"space_selector_title" = "Moje priestory"; +"all_chats_edit_menu_space_settings" = "Nastavenia priestoru"; +"all_chats_edit_menu_leave_space" = "Opustiť %@"; +"all_chats_user_menu_settings" = "Používateľské nastavenia"; +"room_recents_recently_viewed_section" = "Nedávno zobrazené"; +"all_chats_all_filter" = "Všetky"; +"all_chats_edit_layout_alphabetical_order" = "Zoradiť A-Z"; +"all_chats_edit_layout_activity_order" = "Zoradiť podľa aktivity"; +"all_chats_edit_layout_show_filters" = "Zobraziť filtre"; +"all_chats_edit_layout_show_recents" = "Zobraziť posledné"; +"all_chats_edit_layout_sorting_options_title" = "Zoradiť správy podľa"; +"all_chats_edit_layout_pin_spaces_title" = "Pripnite svoje priestory"; +"all_chats_edit_layout_add_filters_message" = "Automaticky filtrujte svoje správy do kategórií podľa vlastného výberu"; +"all_chats_edit_layout_add_filters_title" = "Filtrujte svoje správy"; +"all_chats_edit_layout_add_section_message" = "Pripnite sekcie na domovskú obrazovku, aby ste k nim mali ľahký prístup"; +"all_chats_edit_layout_add_section_title" = "Pridať sekciu na domovskú stránku"; +"all_chats_edit_layout_unreads" = "Neprečítané"; +"all_chats_edit_layout_recents" = "Nedávne"; +"all_chats_edit_layout" = "Predvoľby rozmiestnenia"; +"all_chats_section_title" = "Konverzácie"; + +// Mark: - All Chats + +"all_chats_title" = "Všetky konverzácie"; +"spaces_subspace_creation_visibility_message" = "Vytvorený priestor bude zaradený do %@."; +"spaces_subspace_creation_visibility_title" = "Aký typ podpriestoru chcete vytvoriť?"; +"spaces_create_subspace_title" = "Vytvoriť podpriestor"; +"spaces_add_subspace_title" = "Vytvoriť priestor v rámci %@"; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 54ec797fd..078427461 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -159,7 +159,7 @@ // People tab "people_invites_section" = "ЗАПРОШЕННЯ"; "people_conversation_section" = "БЕСІДИ"; -"people_no_conversation" = "Нема балачок"; +"people_no_conversation" = "Немає бесід"; "room_participants_leave_prompt_msg_for_dm" = "Ви впевнені, що хочете вийти?"; "room_participants_leave_prompt_title_for_dm" = "Вийти"; "client_android_name" = "Element Android"; @@ -1039,7 +1039,7 @@ "security_settings_blacklist_unverified_devices_description" = "Звірте всі сеанси користувача, щоб позначити його довіреним і надіслати йому повідомлення."; "security_settings_crosssigning_bootstrap" = "Налаштувати"; "security_settings_crosssigning_info_ok" = "Перехресне підписування готове до використання."; -"security_settings_crosssigning_info_trusted" = "Перехресне підписування увімкнено. Ви можете робити інших користувачів і свої інші сеанси довіреними на підставі перехресного підпису, але ви не можете перехресно підписувати цим сеансом, бо в нього ще нема закритих ключів перехресного підписування. Доповніть захист цього сеансу."; +"security_settings_crosssigning_info_trusted" = "Перехресне підписування увімкнено. Ви можете робити інших користувачів і свої інші сеанси довіреними на підставі перехресного підписування, але ви не можете перехресно підписувати з цього сеансу, оскільки в нього ще немає приватних ключів перехресного підписування. Завершіть захист цього сеансу."; "security_settings_crosssigning_info_exists" = "Ваш обліковий запис має ідентичність перехресного підписування, але вона ще не довірена цим сеансом. Доповніть захист цього сеансу."; "security_settings_secure_backup_description" = "Зробіть резервну копію своїх ключів шифрування й даних облікового запису на випадок втрати доступу до своїх сеансів. Ваші ключі будуть захищені унікальним ключем безпеки."; "security_settings_crypto_sessions_description_2" = "Якщо не впізнаєте сеанс, скиньте пароль облікового запису Matrix і налаштування безпечного резервного копіювання."; @@ -1059,7 +1059,7 @@ "settings_confirm_media_size_description" = "Коли це ввімкнено, при надсиланні зображень чи відео вам пропонуватиметься підтвердити їхній розмір."; "settings_three_pids_management_information_part2" = "Знаходження"; "settings_config_user_id" = "Ви ввійшли як %@"; -"unknown_devices_alert" = "Кімната містить сеанси, які досі не пройшли звірку.\nТобто нема гарантії, що ці сеанси належать користувачам, від імені яких вони створені.\nРадимо звірити кожен сеанс, перш ніж продовжити; але за потреби можете повторити надсилання повідомлення без звірки."; +"unknown_devices_alert" = "Кімната містить невідомі не звірені сеанси.\nТобто немає гарантії, що ці сеанси належать користувачам, від імені яких вони створені.\nРадимо звірити кожен сеанс, перш ніж продовжувати; але за потреби можете повторити надсилання повідомлення без звірки."; "room_action_camera" = "Зробити світлину або відео"; "room_member_power_level_short_custom" = "Інше"; "room_member_power_level_custom_in" = "Інше (%@) у %@"; @@ -1479,8 +1479,8 @@ "widget_integration_room_not_visible" = "Кімната %@ недоступна."; "widget_integration_missing_user_id" = "В запиті бракує user_id."; "widget_integration_missing_room_id" = "В запиті бракує room_id."; -"widget_integration_no_permission_in_room" = "У вас нема такого дозволу в цій кімнаті."; -"widget_integration_must_be_in_room" = "Вас нема в цій кімнаті."; +"widget_integration_no_permission_in_room" = "У вас немає такого дозволу в цій кімнаті."; +"widget_integration_must_be_in_room" = "Вас немає в цій кімнаті."; "widget_integration_positive_power_level" = "Рівень повноважень має бути цілим додатним числом."; "widget_integration_room_not_recognised" = "Кімнату не знайдено."; "widget_integration_failed_to_send_request" = "Не вдалося надіслати запит."; @@ -2624,3 +2624,35 @@ "location_sharing_invalid_power_level_title" = "Ви не маєте дозволу ділитися поточним місцем перебування"; "authentication_choose_password_not_verified_message" = "Перевірте свою поштову скриньку"; "authentication_choose_password_not_verified_title" = "Електронна адреса не підтверджена"; +"location_sharing_map_loading_error" = "Неможливо завантажити карту\nЦей домашній сервер не налаштовано для показу карт"; + +// Mark: - Space Selector + +"space_selector_title" = "Мої простори"; +"all_chats_edit_menu_space_settings" = "Налаштування простору"; +"all_chats_user_menu_settings" = "Налаштування користувача"; +"all_chats_edit_menu_leave_space" = "Вийти з %@"; +"room_recents_recently_viewed_section" = "Останні переглянуті"; +"all_chats_all_filter" = "Усі"; +"all_chats_edit_layout_alphabetical_order" = "Упорядкувати А-Я"; +"all_chats_edit_layout_activity_order" = "Упорядкувати за активністю"; +"all_chats_edit_layout_show_filters" = "Показати фільтри"; +"all_chats_edit_layout_show_recents" = "Показати найновіші"; +"all_chats_edit_layout_sorting_options_title" = "Упорядкувати повідомлення за"; +"all_chats_edit_layout_pin_spaces_title" = "Прикріплюйте свої простори"; +"all_chats_edit_layout_add_filters_message" = "Автоматично упорядковуйте свої повідомлення за категоріями на ваш вибір"; +"all_chats_edit_layout_add_filters_title" = "Упорядковуйте свої повідомлення"; +"all_chats_edit_layout_add_section_message" = "Закріпіть розділи в домівці для легкого доступу"; +"all_chats_edit_layout_add_section_title" = "Додати розділ у домівку"; +"all_chats_edit_layout_unreads" = "Непрочитані"; +"all_chats_edit_layout_recents" = "Найновіші"; +"all_chats_edit_layout" = "Параметри макета"; +"all_chats_section_title" = "Бесіди"; + +// Mark: - All Chats + +"all_chats_title" = "Усі бесіди"; +"spaces_subspace_creation_visibility_message" = "Створений простір буде додано до %@."; +"spaces_subspace_creation_visibility_title" = "Який тип підпростору ви хочете створити?"; +"spaces_create_subspace_title" = "Створити підпростір"; +"spaces_add_subspace_title" = "Створити простір у %@"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index f8b24462b..643aeb5fe 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -228,7 +228,7 @@ // Room Preview "room_preview_invitation_format" = "您已经通过 %@ 的邀请而加入此房间"; "room_preview_subtitle" = "这是此房间的一个预览。房间交互已禁用。"; -"room_preview_unlinked_email_warning" = "此邀请已发送至未与此帐户关联的 %@。你可能希望用一个不同的账户登录,或者把这个电子邮箱加入到您的账户。"; +"room_preview_unlinked_email_warning" = "此邀请已发送至未与此账户关联的 %@。你可能希望用一个不同的账户登录,或者把这个电子邮箱加入到您的账户。"; "room_preview_try_join_an_unknown_room" = "你正在尝试访问 %@。你要加入以参与讨论吗?"; "room_preview_try_join_an_unknown_room_default" = "一间房间"; // Settings @@ -498,7 +498,7 @@ "room_resource_limit_exceeded_message_contact_1" = " 请 "; "room_resource_limit_exceeded_message_contact_2_link" = "联系您的服务管理员"; "room_resource_limit_exceeded_message_contact_3" = " 以继续使用本服务。"; -"settings_deactivate_account" = "停用帐户"; +"settings_deactivate_account" = "停用账户"; "settings_labs_room_members_lazy_loading" = "延迟加载聊天室成员"; "settings_labs_room_members_lazy_loading_error_message" = "您的主服务器尚不支持延迟加载聊天室成员。请稍后再试。"; "settings_deactivate_my_account" = "永久停用账户"; @@ -522,7 +522,7 @@ "room_resource_usage_limit_reached_message_contact_3" = " 以提高限制。"; // String for App Store "store_short_description" = "安全、去中心化的聊天及 VoIP 应用"; -"store_full_description" = "Element 是一种新型的通讯与协作应用:\n\n1. 使您可以掌控您的隐私\n2. 使您与 Matrix 网络中的任何人交流,甚至可以通过集成功能与如 Slack 之类的其他应用通讯\n3. 保护您免受广告,大数据挖掘和封闭服务的侵害\n4. 通过端到端加密保证安全,通过交叉签名验证其他人\n\nElement 与其他通讯与协作应用完全不同,因为它是去中心化且开源的。\n\nElement 允许您自托管——或者选择托管商——因此,您能拥有数据和会话的隐私权,所有权和控制权。它允许您访问开放网络;因此,您可以与 Element 用户以外的人交流。并且它非常安全。\n\nElement 之所以可以做到这些,是因为它在 Matrix 上运行——开放,去中心化通讯的标准。\n\n通过让您选择由谁来托管您的会话,Element 让您掌控一切。在 Element 应用中,您可以选择不同的托管方式:\n\n1. 在由 Matrix 开发者托管的 matrix.org 公共服务器上获取免费帐户,或从志愿者托管的上千个公共服务器中选择\n2. 在您自己的硬件上运行服务器,自托管您的会话\n3. 通过订阅 Element Matrix Services 托管平台,简单地在自定义服务器上注册账户\n\n为什么选择 Element?\n\n掌控您的数据:您来决定存放您的数据和消息的位置。拥有并控制它的是您,而不是挖掘您的数据或与第三方分享的巨型企业。\n\n开放通讯与协作:您可以与 Matrix 网络中的任何人聊天,不论他们使用 Element 还是其他 Matrix 应用,甚至/即使他们在使用不同的通讯系统,例如 Slack,IRC 或 XMPP。\n\n超级安全:支持真正的端到端加密(仅有会话中的人可以解密消息),还有能够验证会话参与方的设备的交叉签名。\n\n完善的通讯方式:消息,语音和视频通话,文件共享,屏幕共享和大量集成功能,机器人和挂件。建立房间与社区,保持联系并完成工作。\n\n随时随地:消息历史可在您的全部设备和 https://app.element.io 网页端之间完全同步,无论您在哪里,都可以保持联系。"; +"store_full_description" = "Element 是一种新型的通讯与协作应用:\n\n1. 使您可以掌控您的隐私\n2. 使您与 Matrix 网络中的任何人交流,甚至可以通过集成功能与如 Slack 之类的其他应用通讯\n3. 保护您免受广告,大数据挖掘和封闭服务的侵害\n4. 通过端到端加密保证安全,通过交叉签名验证其他人\n\nElement 与其他通讯与协作应用完全不同,因为它是去中心化且开源的。\n\nElement 允许您自托管——或者选择托管商——因此,您能拥有数据和会话的隐私权,所有权和控制权。它允许您访问开放网络;因此,您可以与 Element 用户以外的人交流。并且它非常安全。\n\nElement 之所以可以做到这些,是因为它在 Matrix 上运行——开放,去中心化通讯的标准。\n\n通过让您选择由谁来托管您的会话,Element 让您掌控一切。在 Element 应用中,您可以选择不同的托管方式:\n\n1. 在由 Matrix 开发者托管的 matrix.org 公共服务器上获取免费账户,或从志愿者托管的上千个公共服务器中选择\n2. 在您自己的硬件上运行服务器,自托管您的会话\n3. 通过订阅 Element Matrix Services 托管平台,简单地在自定义服务器上注册账户\n\n为什么选择 Element?\n\n掌控您的数据:您来决定存放您的数据和消息的位置。拥有并控制它的是您,而不是挖掘您的数据或与第三方分享的巨型企业。\n\n开放通讯与协作:您可以与 Matrix 网络中的任何人聊天,不论他们使用 Element 还是其他 Matrix 应用,甚至/即使他们在使用不同的通讯系统,例如 Slack,IRC 或 XMPP。\n\n超级安全:支持真正的端到端加密(仅有会话中的人可以解密消息),还有能够验证会话参与方的设备的交叉签名。\n\n完善的通讯方式:消息,语音和视频通话,文件共享,屏幕共享和大量集成功能,机器人和挂件。建立房间与社区,保持联系并完成工作。\n\n随时随地:消息历史可在您的全部设备和 https://app.element.io 网页端之间完全同步,无论您在哪里,都可以保持联系。"; "auth_accept_policies" = "请查看并接受此主页服务器的服务条款:"; "room_replacement_information" = "这个房间已被替换,不再有效。"; "settings_flair" = "在允许的地方显示个性徽章"; @@ -530,7 +530,7 @@ "settings_key_backup_info" = "消息已被端对端安全加密。只有您和持有密钥的接收方可以阅读这些消息。"; "settings_key_backup_info_checking" = "正在检查…"; "settings_key_backup_info_none" = "您的密钥未从此会话备份。"; -"settings_key_backup_info_signout_warning" = "在退出账号之前备份你的密钥以避免丢失它们。"; +"settings_key_backup_info_signout_warning" = "在退出账户之前备份你的密钥以避免丢失它们。"; "settings_key_backup_info_version" = "密钥备份版本:%@"; "settings_key_backup_info_algorithm" = "算法:%@"; "settings_key_backup_info_valid" = "此会话正在备份密钥。"; @@ -574,13 +574,13 @@ // GDPR "gdpr_consent_not_given_alert_message" = "要继续使用该 %@ 主服务器,您必须查看并同意其服务条款和条件。"; "gdpr_consent_not_given_alert_review_now_action" = "立即查看"; -"deactivate_account_title" = "停用帐户"; -"deactivate_account_informations_part1" = "这将使您的账号永久无法使用。 您将无法登录,也无法重新注册相同的用户 ID 。 这将导致您的账号离开其参与的所有房间,并且会从您的身份服务器中删除您的账号详细信息。 "; +"deactivate_account_title" = "停用账户"; +"deactivate_account_informations_part1" = "这将使您的账户永久无法使用。 您将无法登录,也无法重新注册相同的用户 ID 。 这将导致您的账户离开其参与的所有房间,并且会从您的身份服务器中删除您的账户详细信息。 "; "deactivate_account_informations_part2_emphasize" = "此项操作无法逆转。"; -"deactivate_account_informations_part3" = "\n\n正在停用您的账号 "; +"deactivate_account_informations_part3" = "\n\n正在停用您的账户 "; "deactivate_account_informations_part4_emphasize" = "默认情况下不会导致我们忘记您发送的消息。 "; "deactivate_account_informations_part5" = "如果您希望我们忘记您的消息,请勾选下面的框\n\nMatrix中的消息可见性与电子邮件类似。 我们忘记您的消息意味着您已发送的消息将不会再与任何新用户或未注册用户共享,但已有权访问这些消息的注册用户仍可访问其副本。"; -"deactivate_account_forget_messages_information_part1" = "当我的帐户被停用时,请忘记我发送的所有消息("; +"deactivate_account_forget_messages_information_part1" = "当我的账户被停用时,请忘记我发送的所有消息("; "deactivate_account_forget_messages_information_part3" = ": 这会导致将来加入的用户看到的是一段不完整的对话)"; "deactivate_account_password_alert_message" = "要继续,请输入您的密码"; "rerequest_keys_alert_message" = "请在另一台可以解密消息的设备上启动%@,这样它就可以将密钥发送到此会话。"; @@ -594,22 +594,22 @@ "key_backup_setup_intro_setup_action_with_existing_backup" = "使用密钥备份"; "key_backup_setup_intro_manual_export_info" = "(高级)"; "key_backup_setup_intro_manual_export_action" = "手动导出密钥"; -"key_backup_setup_passphrase_title" = "使用一段安全口令保护您的备份"; -"key_backup_setup_passphrase_info" = "我们将会在主服务器上保存一份您的密钥的加密拷贝。设置一个密码口令来保护您的备份的安全。\n\n为了最大的安全性,这个密码应当与您的账号密码不同。"; +"key_backup_setup_passphrase_title" = "使用一段安全词组保护您的备份"; +"key_backup_setup_passphrase_info" = "我们将会在主服务器上保存一份您的密钥的加密拷贝。设置一个词组来保护您的备份的安全。\n\n为了最大的安全性,它应当与您的Matrix账户密码不同。"; "key_backup_setup_passphrase_passphrase_title" = "输入"; -"key_backup_setup_passphrase_passphrase_placeholder" = "输入密码口令"; +"key_backup_setup_passphrase_passphrase_placeholder" = "输入词组"; "key_backup_setup_passphrase_passphrase_valid" = "太棒了!"; "key_backup_setup_passphrase_passphrase_invalid" = "尝试添加一个字符"; "key_backup_setup_passphrase_confirm_passphrase_title" = "确认"; -"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "确认密码口令"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "确认词组"; "key_backup_setup_passphrase_confirm_passphrase_valid" = "太棒了!"; -"key_backup_setup_passphrase_confirm_passphrase_invalid" = "密码口令不匹配"; -"key_backup_setup_passphrase_set_passphrase_action" = "设置密码口令"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "词组不匹配"; +"key_backup_setup_passphrase_set_passphrase_action" = "设置词组"; "key_backup_setup_passphrase_setup_recovery_key_info" = "或者使用安全密钥来保护您的备份,将密钥保存在一个安全的地方。"; -"key_backup_setup_passphrase_setup_recovery_key_action" = "(高级) 使用安全密钥进行设置"; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(高级)使用安全密钥进行设置"; "key_backup_setup_success_title" = "成功!"; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "您的密钥已备份。\n\n您的安全密钥是一张安全网 - 如果您忘记了密码您可以利用它重获您的已加密信息的访问权。\n\n请将您的安全密钥保存在一个非常安全的地方,比如密码管理器中 (或保险箱里)。"; +"key_backup_setup_success_from_passphrase_info" = "您的密钥正在备份。\n\n您的安全密钥是一张安全网——如果您忘记了口令词组您可以利用它重获您的已加密信息的访问权。\n\n请将您的安全密钥保存在一个非常安全的地方,比如密码管理器中(或保险箱里)。"; "key_backup_setup_success_from_passphrase_save_recovery_key_action" = "保存安全密钥"; "key_backup_setup_success_from_passphrase_done_action" = "完成"; // Success from recovery key @@ -618,15 +618,15 @@ "key_backup_setup_success_from_recovery_key_make_copy_action" = "制作副本"; "key_backup_setup_success_from_recovery_key_made_copy_action" = "我已制作一份副本"; "key_backup_recover_title" = "安全消息"; -"key_backup_recover_invalid_passphrase_title" = "密码口令不正确"; -"key_backup_recover_invalid_passphrase" = "备份无法用此密码口令解密:请检查您输入的安全口令是否正确。"; +"key_backup_recover_invalid_passphrase_title" = "安全词组不正确"; +"key_backup_recover_invalid_passphrase" = "备份无法用此词组解密:请检查您输入的安全词组是否正确。"; "key_backup_recover_invalid_recovery_key_title" = "安全密钥不匹配"; "key_backup_recover_invalid_recovery_key" = "备份无法用此密钥解密:请检查您输入的安全密钥是否正确。"; -"key_backup_recover_from_passphrase_info" = "使用安全口令解锁您的安全消息历史"; +"key_backup_recover_from_passphrase_info" = "使用安全词组解锁您的安全消息历史"; "key_backup_recover_from_passphrase_passphrase_title" = "输入"; -"key_backup_recover_from_passphrase_passphrase_placeholder" = "输入密码口令"; +"key_backup_recover_from_passphrase_passphrase_placeholder" = "输入词组"; "key_backup_recover_from_passphrase_recover_action" = "解锁历史"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "忘记了您的安全口令?您可以 "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "忘记了您的安全词组?您可以 "; "key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "使用你的安全密钥"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "。"; "key_backup_recover_from_recovery_key_info" = "使用安全密钥解锁您的安全消息历史"; @@ -640,16 +640,16 @@ "key_backup_setup_banner_subtitle" = "开始使用密钥备份"; "key_backup_recover_banner_title" = "永不丢失加密消息"; "key_backup_recover_banner_subtitle" = "使用密钥备份"; -"sign_out_existing_key_backup_alert_title" = "您确定要登出账号吗?"; +"sign_out_existing_key_backup_alert_title" = "您确定要登出账户吗?"; "sign_out_existing_key_backup_alert_sign_out_action" = "登出"; -"sign_out_non_existing_key_backup_alert_title" = "如果您此时登出账号,您将会失去已加密消息的访问权"; +"sign_out_non_existing_key_backup_alert_title" = "如果您此时登出账户,您将会失去已加密消息的访问权"; "sign_out_non_existing_key_backup_alert_setup_key_backup_action" = "开始使用密钥备份"; "sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "我不想要我的已加密消息了"; "sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "您将会丢失您的已加密消息"; -"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "如果您在登出账号之前不备份您的密钥,您将会失去已加密消息的访问权。"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "如果您在登出账户之前不备份您的密钥,您将会失去已加密消息的访问权。"; "sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "登出"; "sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "备份"; -"sign_out_key_backup_in_progress_alert_title" = "密钥备份进行中。如果您此时登出账号将会失去已加密消息的访问权。"; +"sign_out_key_backup_in_progress_alert_title" = "密钥备份进行中。如果您此时登出账户将会失去已加密消息的访问权。"; "sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "我不想要我的已加密消息了"; "sign_out_key_backup_in_progress_alert_cancel_action" = "等待"; "auth_login_single_sign_on" = "使用单点登录方式登入"; @@ -660,21 +660,21 @@ "accessibility_checkbox_label" = "多选框"; "auth_add_email_message_2" = "设置邮箱以便恢复账户,并且之后可选让认识的人找到你。"; "auth_add_phone_message_2" = "设置电话号码,并且之后可选让认识的人找到你。"; -"auth_add_email_phone_message_2" = "设置邮箱以便恢复账号。之后可选用邮箱或者电话号码让认识的人找到你。"; +"auth_add_email_phone_message_2" = "设置邮箱以便恢复账户。之后可选用邮箱或者电话号码让认识的人找到你。"; "auth_email_is_required" = "未设置身份认证服务器,所以你不能添加邮箱地址来重设你的密码。"; "auth_phone_is_required" = "未设置身份认证服务器,所以你不能添加电话号码来重设你的密码。"; "auth_forgot_password_error_no_configured_identity_server" = "未设置身份认证服务器:添加服务器以重设你的密码。"; "auth_reset_password_error_is_required" = "未设置身份认证服务器:在服务器选项中添加以便重设你的密码。"; "auth_softlogout_signed_out" = "你已登出"; "auth_softlogout_sign_in" = "登录"; -"auth_softlogout_reason" = "你的主服务器(%1$@)管理员已将你的账号%2$@(%3$@)登出。"; +"auth_softlogout_reason" = "你的主服务器(%1$@)管理员已将你的账户%2$@(%3$@)登出。"; "auth_softlogout_recover_encryption_keys" = "登录以恢复单独保存在此设备上的加密密钥。你需要它们才能阅读任何设备上的安全消息。"; "auth_softlogout_clear_data" = "清空个人信息"; "auth_softlogout_clear_data_message_1" = "警告:你的个人信息(包括加密密钥)仍将保存在这台设备上。"; -"auth_softlogout_clear_data_message_2" = "当你不再使用此设备,或者想登录另一个账号时,请清空它。"; +"auth_softlogout_clear_data_message_2" = "当你不再使用此设备,或者想登录另一个账户时,请清空它。"; "auth_softlogout_clear_data_button" = "清空所有数据"; "auth_softlogout_clear_data_sign_out_title" = "你确定吗?"; -"auth_softlogout_clear_data_sign_out_msg" = "你确定希望清空所有当前保存在此设备上数据吗?再次登录可以获取你的账号数据和消息。"; +"auth_softlogout_clear_data_sign_out_msg" = "你确定希望清空所有当前保存在此设备上数据吗?再次登录可以获取你的账户数据和消息。"; "auth_softlogout_clear_data_sign_out" = "登出"; // Errors "error_user_already_logged_in" = "您似乎正在尝试连接另一个主服务器。您想要登出吗?"; @@ -709,18 +709,18 @@ "media_type_accessibility_video" = "视频"; "media_type_accessibility_location" = "位置"; "media_type_accessibility_file" = "文件"; -"media_type_accessibility_sticker" = "贴图"; +"media_type_accessibility_sticker" = "贴纸"; "settings_discovery_settings" = "发现"; "settings_identity_server_settings" = "身份认证服务器"; "settings_integrations" = "集成"; -"settings_three_pids_management_information_part1" = "在这里管理你可以用来登录或者恢复账号的电子邮箱地址或者电话号码。控制谁可以通过以下途径找到你: "; +"settings_three_pids_management_information_part1" = "在这里管理你可以用来登录或者恢复账户的电子邮箱地址或者电话号码。控制谁可以通过以下途径找到你: "; "settings_three_pids_management_information_part2" = "发现"; "settings_three_pids_management_information_part3" = "。"; "settings_security" = "安全"; "settings_calls_stun_server_fallback_button" = "允许使用通话辅助服务器作为备用手段"; "settings_calls_stun_server_fallback_description" = "允许在你的主服务器不能提供时使用通话辅助服务器 %@ 作为备用手段(你的IP地址在通话时会被分享)。"; "settings_integrations_allow_button" = "管理集成"; -"settings_integrations_allow_description" = "使用集成管理器(%@)来管理机器人,桥接,小插件和贴图包。\n\n集成管理器会收到设置数据,而且能修改小插件,发送房间邀请以及代表你设置权力级别。"; +"settings_integrations_allow_description" = "使用集成管理器(%@)来管理机器人,桥接,小插件和贴纸包。\n\n集成管理器会收到设置数据,而且能修改小插件,发送房间邀请以及代表你设置权力级别。"; "settings_labs_message_reaction" = "用emoji表情回应消息"; "settings_labs_enable_cross_signing" = "开启交叉签名按用户验证而不是按设备验证(开发中)"; "settings_add_3pid_password_title_email" = "添加邮箱地址"; @@ -736,9 +736,9 @@ "settings_discovery_three_pids_management_information_part3" = "。"; "settings_discovery_error_message" = "发生错误。请重试。"; "settings_discovery_three_pid_details_title_email" = "管理邮箱"; -"settings_discovery_three_pid_details_information_email" = "设置此邮箱地址的偏好,其他用户可使用它发现你和邀请你加入房间。在“账号”中添加或者删除邮箱地址。"; +"settings_discovery_three_pid_details_information_email" = "设置此邮箱地址的偏好,其他用户可使用它发现你和邀请你加入房间。在“账户”中添加或者删除邮箱地址。"; "settings_discovery_three_pid_details_title_phone_number" = "管理电话号码"; -"settings_discovery_three_pid_details_information_phone_number" = "设置此电话号码的偏好,其他用户可使用它发现你和邀请你加入房间。在“账号”中添加或者删除电话号码。"; +"settings_discovery_three_pid_details_information_phone_number" = "设置此电话号码的偏好,其他用户可使用它发现你和邀请你加入房间。在“账户”中添加或者删除电话号码。"; "settings_discovery_three_pid_details_share_action" = "分享"; "settings_discovery_three_pid_details_revoke_action" = "撤回"; "settings_discovery_three_pid_details_cancel_email_validation_action" = "取消邮箱验证"; @@ -973,7 +973,7 @@ "user_verification_session_details_information_untrusted_current_user" = "验证这个会话以标记它为已信任并且给与它加密消息的获取权限:"; "user_verification_session_details_information_untrusted_other_user" = " 用新会话登录:"; "user_verification_session_details_additional_information_untrusted_other_user" = "在此用户信任这个会话之前,发往和收到它的消息会有警告标签。或者,你可以手动验证它。"; -"user_verification_session_details_additional_information_untrusted_current_user" = "如果你没有登录到这个会话,你的账号可能在被盗用。"; +"user_verification_session_details_additional_information_untrusted_current_user" = "如果你没有登录到这个会话,你的账户可能在被盗用。"; "user_verification_session_details_verify_action_current_user" = "交互式验证"; "user_verification_session_details_verify_action_other_user" = "手动验证"; "room_participants_action_security_status_complete_security" = "完整安全性"; @@ -984,7 +984,7 @@ "room_member_power_level_short_moderator" = "协管员"; "security_settings_crosssigning" = "交叉签名"; "security_settings_crosssigning_info_not_bootstrapped" = "交叉签名还没有被设置。"; -"security_settings_crosssigning_info_exists" = "您的帐户有一个交叉签名身份,但是还没有被这个会话信任。完全安全的会话。"; +"security_settings_crosssigning_info_exists" = "您的账户有一个交叉签名身份,但是还没有被这个会话信任。完全安全的会话。"; "skip" = "跳过"; "room_member_power_level_custom_in" = "%@里的自定义(%@)"; "room_member_power_level_short_custom" = "自定义"; @@ -1006,7 +1006,7 @@ "device_verification_security_advice_number" = "比较数字,确保它们以相同的顺序出现。"; // New login "device_verification_self_verify_alert_title" = "新登录。这是你吗?"; -"device_verification_self_verify_alert_message" = "验证访问您的帐户的新登录名:%@"; +"device_verification_self_verify_alert_message" = "验证访问您的账户的新登录名:%@"; "device_verification_self_verify_alert_validate_action" = "验证"; "device_verification_self_verify_start_verify_action" = "开始验证"; "device_verification_self_verify_start_information" = "使用此会话验证您的新会话,并授予其访问加密信息的权限。"; @@ -1015,14 +1015,14 @@ "key_verification_self_verify_current_session_alert_message" = "其他用户可能不信任它。"; "key_verification_self_verify_current_session_alert_validate_action" = "验证"; "key_verification_self_verify_unverified_sessions_alert_title" = "查看您的登录位置"; -"key_verification_self_verify_unverified_sessions_alert_message" = "验证您的所有会话以确保您的帐户和消息的安全。"; +"key_verification_self_verify_unverified_sessions_alert_message" = "验证您的所有会话以确保您的账户和消息的安全。"; "key_verification_self_verify_unverified_sessions_alert_validate_action" = "检查"; "device_verification_self_verify_wait_title" = "绝对安全"; "device_verification_self_verify_wait_new_sign_in_title" = "验证此登录名"; "device_verification_self_verify_wait_information" = "从您的其他会话之一验证此会话,授予它访问加密消息的权限。\n\n在您的其他设备上使用最新版的%@:"; "device_verification_self_verify_wait_additional_information" = "这适用于%@和其他支持交叉签名的Matrix客户端。"; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "使用安全密钥"; -"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "使用安全口令或密钥"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "使用安全词组或密钥"; "device_verification_self_verify_wait_recover_secrets_additional_information" = "如果您无法访问一个现有会话"; "key_verification_verify_sas_title_emoji" = "比较emoji"; "key_verification_verify_sas_title_number" = "比较数字"; @@ -1062,17 +1062,17 @@ "key_verification_scan_confirmation_scanned_user_information" = "是否%@显示相同的盾牌?"; "key_verification_scan_confirmation_scanned_device_information" = "另一台设备是否显示相同的盾牌?"; "user_verification_session_details_verify_action_current_user_manually" = "通过文本手动验证"; -"secrets_recovery_with_passphrase_title" = "安全口令"; -"secrets_recovery_with_passphrase_information_default" = "通过输入安全口令,访问您的安全信息历史记录和交叉登录身份,以验证其他会话。"; -"secrets_recovery_with_passphrase_information_verify_device" = "使用您的安全口令验证此设备。"; +"secrets_recovery_with_passphrase_title" = "安全词组"; +"secrets_recovery_with_passphrase_information_default" = "通过输入安全词组,访问您的安全信息历史记录和交叉登录身份,以验证其他会话。"; +"secrets_recovery_with_passphrase_information_verify_device" = "使用您的安全词组验证此设备。"; "secrets_recovery_with_passphrase_passphrase_title" = "输入"; -"secrets_recovery_with_passphrase_passphrase_placeholder" = "输入安全口令"; -"secrets_recovery_with_passphrase_recover_action" = "使用密码口令"; -"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "忘记了您的安全口令?您可以 "; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "输入安全词组"; +"secrets_recovery_with_passphrase_recover_action" = "使用词组"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "忘记了您的安全词组?您可以 "; "secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "使用您的安全密钥"; "secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; "secrets_recovery_with_passphrase_invalid_passphrase_title" = "无法访问机密存储"; -"secrets_recovery_with_passphrase_invalid_passphrase_message" = "请验证您输入的安全口令是否正确。"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "请验证您输入的安全词组是否正确。"; "secrets_recovery_with_key_title" = "安全密钥"; "secrets_recovery_with_key_information_default" = "通过输入安全密钥访问安全信息历史记录和交叉登录身份,以验证其他会话。"; "secrets_recovery_with_key_information_verify_device" = "使用您的安全密钥验证此设备。"; @@ -1082,7 +1082,7 @@ "secrets_recovery_with_key_invalid_recovery_key_title" = "无法访问机密存储"; "secrets_recovery_with_key_invalid_recovery_key_message" = "请验证您输入的安全密钥是否正确。"; "rooms_empty_view_information" = "房间非常适合任何群聊,无论是私人的还是公共的。点击+以查找现有房间,或新建房间。"; -"security_settings_user_password_description" = "通过输入您的帐户密码确认您的身份"; +"security_settings_user_password_description" = "通过输入您的账户密码确认您的身份"; "rooms_empty_view_title" = "房间"; "people_empty_view_information" = "与任何人安全聊天。点击+开始添加人员。"; "people_empty_view_title" = "用户"; @@ -1103,7 +1103,7 @@ "security_settings_secure_backup_delete" = "删除备份"; "security_settings_secure_backup_synchronise" = "同步"; "security_settings_secure_backup_setup" = "设置"; -"security_settings_secure_backup_description" = "备份你的帐户数据备份和加密密钥,以防你无法访问会话。 你的密钥将受到唯一的安全密钥保护。"; +"security_settings_secure_backup_description" = "备份你的账户数据备份和加密密钥,以防你无法访问会话。 你的密钥将受到唯一的安全密钥保护。"; "security_settings_crypto_sessions_description_2" = "如果您未曾发起登录,请更改密码并重置安全备份。"; "settings_show_NSFW_public_rooms" = "显示 NSFW 公共房间"; "external_link_confirmation_message" = "此链接 %@ 会将您带至另一个网站:%@\n\n是否前往?"; @@ -1209,15 +1209,15 @@ "major_update_title" = "Riot 现已成为 %@"; "secrets_reset_reset_action" = "重置"; -"secrets_setup_recovery_passphrase_summary_title" = "保存您的安全密语"; -"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "确认口令"; +"secrets_setup_recovery_passphrase_summary_title" = "保存您的安全词组"; +"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "确认词组"; "secrets_setup_recovery_passphrase_confirm_passphrase_title" = "确认"; -"secrets_setup_recovery_passphrase_additional_information" = "不要使用你的账号密码。"; +"secrets_setup_recovery_passphrase_additional_information" = "不要使用你的Matrix账户密码。"; "secrets_setup_recovery_passphrase_validate_action" = "完成"; // Recovery passphrase -"secrets_setup_recovery_passphrase_title" = "设置安全密语"; +"secrets_setup_recovery_passphrase_title" = "设置安全词组"; "secrets_setup_recovery_key_done_action" = "完成"; "secrets_setup_recovery_key_export_action" = "保存"; "secrets_setup_recovery_key_loading" = "正在加载…"; @@ -1280,7 +1280,7 @@ "pin_protection_confirm_pin" = "验证你的 PIN"; "pin_protection_choose_pin" = "创建 PIN 以确保安全"; "major_update_done_action" = "知道了"; -"major_update_information" = "我们很激动地宣布,我们改名了!你的应用程序是最新的,你已经登录了你的帐户。"; +"major_update_information" = "我们很激动地宣布,我们改名了!你的应用程序是最新的,你已经登录了你的账户。"; "cross_signing_setup_banner_subtitle" = "更容易验证你的其他设备"; // MARK: - Cross-signing @@ -1288,7 +1288,7 @@ // Banner "cross_signing_setup_banner_title" = "设置加密"; -"secrets_reset_authentication_message" = "请输入你的帐户密码进行确认"; +"secrets_reset_authentication_message" = "请输入你的账户密码进行确认"; "secrets_reset_warning_message" = "您将重新启动,没有历史记录,消息,受信任的设备或受信任的用户。"; "secrets_reset_warning_title" = "如果你选择全部重置"; "secrets_reset_information" = "仅当没有其他设备可用来验证此设备时,才执行此操作。"; @@ -1296,9 +1296,9 @@ // MARK: - Secrets reset "secrets_reset_title" = "全部重置"; -"secrets_setup_recovery_passphrase_summary_information" = "记住你的安全短语。它可以用来解锁你的加密信息和数据。"; -"secrets_setup_recovery_passphrase_confirm_information" = "再次输入您的安全口令以确认。"; -"secrets_setup_recovery_passphrase_information" = "输入只有您知道的安全口令,用于保护您的服务器上的秘密。"; +"secrets_setup_recovery_passphrase_summary_information" = "记住你的安全词组。它可以用来解锁你的加密信息和数据。"; +"secrets_setup_recovery_passphrase_confirm_information" = "再次输入您的安全词组以确认。"; +"secrets_setup_recovery_passphrase_information" = "输入只有您知道的安全词组,用于保护您的服务器上的秘密。"; "secrets_setup_recovery_key_storage_alert_message" = "✓ 把它打印出来,储存在一个安全的地方\n✓ 把它保存在 USB 钥匙或备份驱动器上\n✓ 把它复制到你的个人云存储"; "secrets_setup_recovery_key_storage_alert_title" = "保持安全"; "secrets_setup_recovery_key_information" = "将安全密钥保存在安全的地方。它可以用来解锁你的加密信息和数据。"; @@ -1324,8 +1324,8 @@ "secure_key_backup_setup_existing_backup_error_unlock_it" = "解锁"; "secure_key_backup_setup_existing_backup_error_info" = "解锁它以在安全备份中复用它,或删除它以在安全备份中创建一个新的消息备份。"; "secure_key_backup_setup_existing_backup_error_title" = "消息的备份已经存在"; -"secure_key_backup_setup_intro_use_security_passphrase_info" = "输入仅有您知道的安全口令,生成备份用的密钥。"; -"secure_key_backup_setup_intro_use_security_passphrase_title" = "使用安全口令"; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "输入仅有您知道的秘密词组,生成备份用的密钥。"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "使用安全词组"; "secure_key_backup_setup_intro_use_security_key_info" = "生成安全密钥存储在安全的地方如密码管理器或保险箱。"; "secure_key_backup_setup_intro_use_security_key_title" = "使用安全密钥"; "secure_key_backup_setup_intro_info" = "通过在你的服务器上备份加密密钥防止丢失对加密消息和数据的访问。"; @@ -1515,7 +1515,7 @@ "done" = "完成"; "open" = "打开"; "service_terms_modal_information_description_integration_manager" = "集成管理器允许您添加来自第三方的功能。"; -"service_terms_modal_information_description_identity_server" = "身份服务器通过查找电话号码或电子邮件地址帮助您找到联系人,看看他们是否已经有一个帐户。"; +"service_terms_modal_information_description_identity_server" = "身份服务器通过查找电话号码或电子邮件地址帮助您找到联系人,看看他们是否已经有一个账户。"; "service_terms_modal_information_title_integration_manager" = "集成管理器"; // Alert explaining what an identity server / integration manager is. @@ -1774,11 +1774,11 @@ // E2E import "e2e_import_room_keys" = "导入房间密钥"; "e2e_import" = "导入"; -"e2e_passphrase_enter" = "输入密码"; +"e2e_passphrase_enter" = "输入口令词组"; // E2E export "e2e_export_room_keys" = "导出房间密钥"; "e2e_export" = "导出"; -"e2e_passphrase_empty" = "密码不能为空"; +"e2e_passphrase_empty" = "口令词组不能为空"; // Others "user_id_title" = "用户 ID:"; "offline" = "离线"; @@ -1823,7 +1823,7 @@ "delete" = "删除"; "create_room" = "创建房间"; "login" = "登录"; -"create_account" = "创建账号"; +"create_account" = "创建账户"; "membership_invite" = "邀请"; "membership_leave" = "退出"; "membership_ban" = "已被封禁"; @@ -1856,7 +1856,7 @@ "contact_mx_users" = "Matrix 用户"; "contact_local_contacts" = "本地联系人"; // Groups -"e2e_passphrase_confirm" = "确认密码"; +"e2e_passphrase_confirm" = "确认口令词组"; "notification_settings_enable_notifications" = "启用通知"; // Notification settings screen "notification_settings_disable_all" = "禁用通知"; @@ -1883,7 +1883,7 @@ "account_msisdn_validation_title" = "等待验证中"; "account_msisdn_validation_error" = "无法验证此手机号。"; "account_error_picture_change_failed" = "头像修改失败"; -"e2e_passphrase_not_match" = "密码必须匹配"; +"e2e_passphrase_not_match" = "口令词组必须匹配"; "not_supported_yet" = "尚未支持"; "local_contacts_access_discovery_warning_title" = "发现用户"; "notice_topic_changed" = "%@ 将话题修改为 \"%@\"。"; @@ -1937,16 +1937,16 @@ "message_reply_to_sender_sent_a_video" = "发送了一段视频。"; "call_invite_expired" = "通话邀请已过期"; "ssl_fingerprint_hash" = "指纹(%@):"; -"e2e_import_prompt" = "此操作允许您导入此前从其他 Matrix 客户端上导出的加密密钥。您将能够解密任何该客户端能解密的消息。\n该导出文件受密码保护。您应在此处输入密码以解密该文件。"; +"e2e_import_prompt" = "此操作允许您导入此前从其他 Matrix 客户端上导出的加密密钥。您将能够解密任何该客户端能解密的消息。\n该导出文件受口令词组保护。您应在此处输入口令词组以解密该文件。"; "e2e_export_prompt" = "此操作允许您将加密房间中接收到的消息导出为一个本地文件。您将来可以将此文件导入到其他 Matrix 客户端中去解密这些消息。\n导出的文件将允许任何能够读取它的人解密您可以看到的任何加密消息,因此您应该小心保证其安全。"; -"e2e_passphrase_create" = "创建密码"; +"e2e_passphrase_create" = "创建口令词组"; "error_common_message" = "出现错误。请稍后再试。"; // Permissions "camera_access_not_granted_for_call" = "视频通话需要摄像头使用权限,但 %@ 无此权限"; "microphone_access_not_granted_for_call" = "通话需要麦克风使用权限,但 %@ 无此权限"; "local_contacts_access_not_granted" = "本地通讯录用户查找功能需要通讯录权限,但 %@ 无此权限"; "local_contacts_access_discovery_warning" = "为了发现已经使用 Matrix 的联系人,%@ 可以把你地址簿里的邮箱地址和电话号码发送给你选定的 Matrix 身份认证服务器。如果支持的话,个人数据会在发送前被哈希——请检查你的身份认证服务器的隐私条款获知更多细节。"; -"notification_settings_global_info" = "通知设置已保存在您的账号中并在所有支持的客户端中共享(包括桌面通知)。\n\n规则会按顺序应用;第一条匹配的规则定义了消息的输出结果。\n因此:按字符规则的通知比按房间规则的通知级别更高,而这两者都比按发送者规则的通知级别更高。\n对于同一类型的多条规则,匹配列表中的第一条优先级最高。"; +"notification_settings_global_info" = "通知设置已保存在您的账户中并在所有支持的客户端中共享(包括桌面通知)。\n\n规则会按顺序应用;第一条匹配的规则定义了消息的输出结果。\n因此:按字符规则的通知比按房间规则的通知级别更高,而这两者都比按发送者规则的通知级别更高。\n对于同一类型的多条规则,匹配列表中的第一条优先级最高。"; "notification_settings_per_word_notifications" = "按字符通知"; "notification_settings_per_word_info" = "单词不区分大小写,并且可能包含 * 通配符。 所以:\nfoo 匹配由单词分隔符包围的字符串 foo(例如标点符号和空格,或一行的开头/结尾)。\nfoo* 匹配任何以 foo 开头的单词。\n*foo* 匹配任何包含3个字母 foo 的单词。"; "notification_settings_word_to_match" = "匹配的单词"; @@ -2055,7 +2055,7 @@ "call_consulting_with_user" = "与 %@ 商量"; "call_video_with_user" = "与 %@ 进行视频通话"; "call_voice_with_user" = "与 %@ 进行语音通话"; -"e2e_passphrase_too_short" = "密码口令太短 (长度至少为 %d 个字符)"; +"e2e_passphrase_too_short" = "口令词组太短(字符长度至少为%d)"; "microphone_access_not_granted_for_voice_message" = "语音消息需要访问麦克风,但 %@ 无权使用它"; "message_reply_to_sender_sent_a_voice_message" = "发送了一条语音消息。"; "attachment_large_with_resolution" = "大 %@ (~%@)"; @@ -2121,8 +2121,48 @@ "spaces_add_space_title" = "创建空间"; "space_invite_not_enough_permission" = "你没权限邀请人来此空间"; "room_invite_not_enough_permission" = "你没权限邀请人来此房间"; -"room_invite_to_space_option_detail" = "他们可以探索%@,但不会成为其成员%@。"; // MARK: - Share invite link "share_invite_link_action" = "分享邀请链接"; +"authentication_verify_email_waiting_hint" = "没收到电子邮件吗?"; +/* The placeholder will show the email address that was entered. */ +"authentication_verify_email_waiting_message" = "按照发送到%@的说明操作"; +"authentication_verify_email_waiting_title" = "验证你的电子邮件。"; +"authentication_verify_email_text_field_placeholder" = "电子邮件"; +/* The placeholder will show the homeserver's domain */ +"authentication_verify_email_input_message" = "%@需要验证你的账户"; +"authentication_verify_email_input_title" = "输入你的电子邮件"; +"authentication_cancel_flow_confirmation_message" = "账户还未创建。停止注册流程吗?"; +"authentication_server_selection_generic_error" = "无法在这个URL上找到服务器,请检查其是否正确。"; +"authentication_server_selection_server_url" = "主服务器URL"; +"authentication_server_selection_register_message" = "你服务器的地址是什么?它就像你全部数据的家"; +"authentication_server_selection_register_title" = "选择你的主服务器"; +"authentication_server_selection_login_message" = "你服务器的地址是什么?"; +"authentication_server_selection_login_title" = "连接到主服务器"; +"authentication_server_info_title_login" = "你的对话发生的地方"; +"authentication_login_forgot_password" = "忘记密码"; +"authentication_login_username" = "用户名/电子邮件/电话"; +"authentication_login_title" = "欢迎回来!"; +"authentication_server_info_title" = "你的对话发生的地方"; +"authentication_registration_password_footer" = "必须是8个或以上的字符"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "别人可通过%@发现你"; +"authentication_registration_username_footer" = "你可在以后更改"; +"authentication_registration_username" = "用户名"; + +// MARK: Authentication +"authentication_registration_title" = "创建账户"; +"onboarding_celebration_button" = "开始吧"; +"onboarding_celebration_message" = "随时前往设置更新你的用户资料"; +"onboarding_celebration_title" = "看起来不错!"; +"onboarding_avatar_accessibility_label" = "用户资料图片"; +"onboarding_avatar_message" = "是时候给名称放一张脸了"; +"onboarding_avatar_title" = "添加用户资料图片"; +"onboarding_display_name_max_length" = "你的显示名称必须少于256个字符"; +"onboarding_display_name_hint" = "你可以后更改"; +"onboarding_display_name_placeholder" = "显示名称"; +"onboarding_display_name_message" = "这会在你发送消息时显示。"; +"onboarding_display_name_title" = "选择显示名称"; +"onboarding_personalization_skip" = "跳过此步"; +"onboarding_personalization_save" = "保存并继续"; diff --git a/Riot/Categories/MXGroup+Riot.h b/Riot/Categories/MXGroup+Riot.h deleted file mode 100644 index 76b1e76e5..000000000 --- a/Riot/Categories/MXGroup+Riot.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import -#import "MatrixKit.h" - -/** - Define a `MXGroup` category at Riot level. - */ -@interface MXGroup (Riot) - -/** - Set the group avatar in the dedicated MXKImageView. - The riot style implies to use in order : - 1 - the default avatar if there is one - 2 - the first letter of the group name. - - @param mxkImageView the destinated MXKImageView. - @param mxSession the matrix session - */ -- (void)setGroupAvatarImageIn:(MXKImageView*)mxkImageView matrixSession:(MXSession*)mxSession; - -@end diff --git a/Riot/Categories/MXGroup+Riot.m b/Riot/Categories/MXGroup+Riot.m deleted file mode 100644 index a68710c06..000000000 --- a/Riot/Categories/MXGroup+Riot.m +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - Copyright 2018 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXGroup+Riot.h" - -#import "AvatarGenerator.h" - -@implementation MXGroup (Riot) - -- (void)setGroupAvatarImageIn:(MXKImageView*)mxkImageView matrixSession:(MXSession*)mxSession -{ - // Use the group display name to prepare the default avatar image. - NSString *avatarDisplayName = self.profile.name; - UIImage* avatarImage = [AvatarGenerator generateAvatarForMatrixItem:self.groupId withDisplayName:avatarDisplayName]; - - if (self.profile.avatarUrl && mxSession) - { - mxkImageView.enableInMemoryCache = YES; - - [mxkImageView setImageURI:self.profile.avatarUrl - withType:nil - andImageOrientation:UIImageOrientationUp - toFitViewSize:mxkImageView.frame.size - withMethod:MXThumbnailingMethodCrop - previewImage:avatarImage - mediaManager:mxSession.mediaManager]; - } - else - { - mxkImageView.image = avatarImage; - } - - mxkImageView.contentMode = UIViewContentModeScaleAspectFill; -} - -@end diff --git a/Riot/Categories/UIViewController.swift b/Riot/Categories/UIViewController.swift index 5fb780d8c..2f29631b4 100644 --- a/Riot/Categories/UIViewController.swift +++ b/Riot/Categories/UIViewController.swift @@ -142,7 +142,7 @@ extension UIViewController { // Even when .never, needs to be true otherwise animation will be broken on iOS11, 12, 13 navigationController?.navigationBar.prefersLargeTitles = true @unknown default: - MXLog.failure("[UIViewController] setLargeTitleDisplayMode: Missing handler for \(largeTitleDisplayMode)") + MXLog.failure("[UIViewController] setLargeTitleDisplayMode: Missing handler", context: largeTitleDisplayMode) } } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index d0373b94f..4aaf0337d 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -22,6 +22,9 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image internal class Asset: NSObject { @objcMembers @objc(AssetImages) internal class Images: NSObject { + internal static let allChatsOnboarding1 = ImageAsset(name: "all_chats_onboarding1") + internal static let allChatsOnboarding2 = ImageAsset(name: "all_chats_onboarding2") + internal static let allChatsOnboarding3 = ImageAsset(name: "all_chats_onboarding3") internal static let analyticsCheckmark = ImageAsset(name: "AnalyticsCheckmark") internal static let analyticsLogo = ImageAsset(name: "AnalyticsLogo") internal static let socialLoginButtonApple = ImageAsset(name: "social_login_button_apple") @@ -113,9 +116,11 @@ internal class Asset: NSObject { internal static let roomActionNotificationMuted = ImageAsset(name: "room_action_notification_muted") internal static let roomActionPriorityHigh = ImageAsset(name: "room_action_priority_high") internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low") + internal static let allChatsEditIcon = ImageAsset(name: "all_chats_edit_icon") + internal static let allChatsEmptyListPlaceholderIcon = ImageAsset(name: "all_chats_empty_list_placeholder_icon") + internal static let allChatsSpacesIcon = ImageAsset(name: "all_chats_spaces_icon") internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork") internal static let homeEmptyScreenArtworkDark = ImageAsset(name: "home_empty_screen_artwork_dark") - internal static let homeMySpacesAction = ImageAsset(name: "home_my_spaces_action") internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action") internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon") internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 08c2a34af..086139eec 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -179,6 +179,66 @@ public class VectorL10n: NSObject { public static var allChatsEditMenuSpaceSettings: String { return VectorL10n.tr("Vector", "all_chats_edit_menu_space_settings") } + /// You’re all caught up. + public static var allChatsEmptyListPlaceholderTitle: String { + return VectorL10n.tr("Vector", "all_chats_empty_list_placeholder_title") + } + /// Spaces are a new way to group rooms and people. Add an existing room, or create a new one, using the bottom-right button. + public static var allChatsEmptySpaceInformation: String { + return VectorL10n.tr("Vector", "all_chats_empty_space_information") + } + /// This is where you're unread messages will show up, when you have some. + public static var allChatsEmptyUnreadsPlaceholderMessage: String { + return VectorL10n.tr("Vector", "all_chats_empty_unreads_placeholder_message") + } + /// The all-in-one secure chat app for teams, friends and organisations. Create a chat, or join an existing room, to get started. + public static var allChatsEmptyViewInformation: String { + return VectorL10n.tr("Vector", "all_chats_empty_view_information") + } + /// %@\nis looking a little empty. + public static func allChatsEmptyViewTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "all_chats_empty_view_title", p1) + } + /// Try adjusting your search. + public static var allChatsNothingFoundPlaceholderMessage: String { + return VectorL10n.tr("Vector", "all_chats_nothing_found_placeholder_message") + } + /// Nothing found. + public static var allChatsNothingFoundPlaceholderTitle: String { + return VectorL10n.tr("Vector", "all_chats_nothing_found_placeholder_title") + } + /// To simplify your Element, tabs are now optional. Manage them using the top-right menu. + public static var allChatsOnboardingPageMessage1: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_page_message1") + } + /// Access your Spaces (bottom-left) faster and easier than ever before. + public static var allChatsOnboardingPageMessage2: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_page_message2") + } + /// Tap your profile to let us know what you think. + public static var allChatsOnboardingPageMessage3: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_page_message3") + } + /// Welcome to a new view! + public static var allChatsOnboardingPageTitle1: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_page_title1") + } + /// Access Spaces + public static var allChatsOnboardingPageTitle2: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_page_title2") + } + /// Give Feedback + public static var allChatsOnboardingPageTitle3: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_page_title3") + } + /// What's new + public static var allChatsOnboardingTitle: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_title") + } + /// Try it out + public static var allChatsOnboardingTryIt: String { + return VectorL10n.tr("Vector", "all_chats_onboarding_try_it") + } /// Chats public static var allChatsSectionTitle: String { return VectorL10n.tr("Vector", "all_chats_section_title") @@ -2491,6 +2551,10 @@ public class VectorL10n: NSObject { public static func inviteFriendsShareText(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "invite_friends_share_text", p1, p2) } + /// Invite to %@ + public static func inviteTo(_ p1: String) -> String { + return VectorL10n.tr("Vector", "invite_to", p1) + } /// Invite matrix User public static var inviteUser: String { return VectorL10n.tr("Vector", "invite_user") @@ -5651,6 +5715,14 @@ public class VectorL10n: NSObject { public static func roomInviteToSpaceOptionTitle(_ p1: String) -> String { return VectorL10n.tr("Vector", "room_invite_to_space_option_title", p1) } + /// This is where your invites appear. + public static var roomInvitesEmptyViewInformation: String { + return VectorL10n.tr("Vector", "room_invites_empty_view_information") + } + /// Nothing new. + public static var roomInvitesEmptyViewTitle: String { + return VectorL10n.tr("Vector", "room_invites_empty_view_title") + } /// Join public static var roomJoinGroupCall: String { return VectorL10n.tr("Vector", "room_join_group_call") @@ -7747,6 +7819,10 @@ public class VectorL10n: NSObject { public static var spaceBetaAnnounceTitle: String { return VectorL10n.tr("Vector", "space_beta_announce_title") } + /// Space detail + public static var spaceDetailNavTitle: String { + return VectorL10n.tr("Vector", "space_detail_nav_title") + } /// Spaces are a new way to group rooms and people.\n\nThey’ll be here soon. For now, if you join one on another platform, you will be able to access any rooms you join here. public static var spaceFeatureUnavailableInformation: String { return VectorL10n.tr("Vector", "space_feature_unavailable_information") @@ -7763,6 +7839,10 @@ public class VectorL10n: NSObject { public static var spaceHomeShowAllRooms: String { return VectorL10n.tr("Vector", "space_home_show_all_rooms") } + /// Space invite + public static var spaceInviteNavTitle: String { + return VectorL10n.tr("Vector", "space_invite_nav_title") + } /// You do not have permission to invite people to this space public static var spaceInviteNotEnoughPermission: String { return VectorL10n.tr("Vector", "space_invite_not_enough_permission") @@ -7791,6 +7871,18 @@ public class VectorL10n: NSObject { public static var spacePublicJoinRuleDetail: String { return VectorL10n.tr("Vector", "space_public_join_rule_detail") } + /// Create Space + public static var spaceSelectorCreateSpace: String { + return VectorL10n.tr("Vector", "space_selector_create_space") + } + /// Spaces are a way to group rooms and people. Create a space to get started. + public static var spaceSelectorEmptyViewInformation: String { + return VectorL10n.tr("Vector", "space_selector_empty_view_information") + } + /// No spaces yet. + public static var spaceSelectorEmptyViewTitle: String { + return VectorL10n.tr("Vector", "space_selector_empty_view_title") + } /// My spaces public static var spaceSelectorTitle: String { return VectorL10n.tr("Vector", "space_selector_title") @@ -8047,6 +8139,10 @@ public class VectorL10n: NSObject { public static var spacesExploreRooms: String { return VectorL10n.tr("Vector", "spaces_explore_rooms") } + /// Explore %@ + public static func spacesExploreRoomsFormat(_ p1: String) -> String { + return VectorL10n.tr("Vector", "spaces_explore_rooms_format", p1) + } /// 1 room public static var spacesExploreRoomsOneRoom: String { return VectorL10n.tr("Vector", "spaces_explore_rooms_one_room") @@ -8347,6 +8443,14 @@ public class VectorL10n: NSObject { public static var userIdTitle: String { return VectorL10n.tr("Vector", "user_id_title") } + /// Sessions + public static var userSessionsOverviewTitle: String { + return VectorL10n.tr("Vector", "user_sessions_overview_title") + } + /// Manage sessions + public static var userSessionsSettings: String { + return VectorL10n.tr("Vector", "user_sessions_settings") + } /// If you didn’t sign in to this session, your account may be compromised. public static var userVerificationSessionDetailsAdditionalInformationUntrustedCurrentUser: String { return VectorL10n.tr("Vector", "user_verification_session_details_additional_information_untrusted_current_user") diff --git a/Riot/Managers/Logging/MatrixSDKLogger.swift b/Riot/Managers/Logging/MatrixSDKLogger.swift index 3012f7bb3..1d9f80fd1 100644 --- a/Riot/Managers/Logging/MatrixSDKLogger.swift +++ b/Riot/Managers/Logging/MatrixSDKLogger.swift @@ -33,7 +33,7 @@ class MatrixSDKLogger: LoggerProtocol { static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { MXLog.warning(message(), file, function, line: line, context: context) } - static func error(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + static func error(_ message: @autoclosure () -> StaticString, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { MXLog.error(message(), file, function, line: line, context: context) } } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 548fad9c7..c3732c144 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -287,9 +287,6 @@ final class RiotSettings: NSObject { @UserDefault(key: "homeScreenShowRoomsTab", defaultValue: BuildSettings.homeScreenShowRoomsTab, storage: defaults) var homeScreenShowRoomsTab - @UserDefault(key: "homeScreenShowCommunitiesTab", defaultValue: BuildSettings.homeScreenShowCommunitiesTab, storage: defaults) - var homeScreenShowCommunitiesTab - // MARK: General Settings @UserDefault(key: "settingsScreenShowChangePassword", defaultValue: BuildSettings.settingsScreenShowChangePassword, storage: defaults) @@ -342,9 +339,6 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomSettingsScreenShowAddressSettings", defaultValue: BuildSettings.roomSettingsScreenShowAddressSettings, storage: defaults) var roomSettingsScreenShowAddressSettings - @UserDefault(key: "roomSettingsScreenShowFlairSettings", defaultValue: BuildSettings.roomSettingsScreenShowFlairSettings, storage: defaults) - var roomSettingsScreenShowFlairSettings - @UserDefault(key: "roomSettingsScreenShowAdvancedSettings", defaultValue: BuildSettings.roomSettingsScreenShowAdvancedSettings, storage: defaults) var roomSettingsScreenShowAdvancedSettings @@ -382,6 +376,11 @@ final class RiotSettings: NSObject { /// Number of spaces previously tracked by the `AnalyticsSpaceTracker` instance. @UserDefault(key: "lastNumberOfTrackedSpaces", defaultValue: nil, storage: defaults) var lastNumberOfTrackedSpaces: Int? + + // MARK: - All Chats Onboarding + + @UserDefault(key: "allChatsOnboardingHasBeenDisplayed", defaultValue: false, storage: defaults) + var allChatsOnboardingHasBeenDisplayed } // MARK: - RiotSettings notification constants diff --git a/Riot/Managers/Theme/ElementUIColorsResolved.swift b/Riot/Managers/Theme/ElementUIColorsResolved.swift deleted file mode 100644 index 20118bfd3..000000000 --- a/Riot/Managers/Theme/ElementUIColorsResolved.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import UIKit -import DesignTokens - -extension UIColor { - /// The colors from DesignKit, resolved for light mode only. - static let elementLight = ElementUIColorsResolved(dynamicColors: element, userInterfaceStyle: .light) - /// The colors from DesignKit, resolved for dark mode only. - static let elementDark = ElementUIColorsResolved(dynamicColors: element, userInterfaceStyle: .dark) -} - -/// The dynamic colors from DesignKit, resolved to light or dark mode for use in the UIKit themes. -/// -/// As Element doesn't (currently) update the app's `UIUserInterfaceStyle` when selecting -/// a custom theme, the dynamic colors provided by DesignKit need resolving for each theme to -/// prevent them from respecting the interface style and rendering in the wrong style. -@objcMembers public class ElementUIColorsResolved: NSObject { - // MARK: Compound - public let accent: UIColor - public let alert: UIColor - public let primaryContent: UIColor - public let secondaryContent: UIColor - public let tertiaryContent: UIColor - public let quaternaryContent: UIColor - public let quinaryContent: UIColor - public let system: UIColor - public let background: UIColor - - public let namesAndAvatars: [UIColor] - - // MARK: Legacy - public let quarterlyContent: UIColor - public let navigation: UIColor - public let tile: UIColor - public let separator: UIColor - - // MARK: Setup - public init(dynamicColors: ElementUIColors, userInterfaceStyle: UIUserInterfaceStyle) { - let traitCollection = UITraitCollection(userInterfaceStyle: userInterfaceStyle) - - self.accent = dynamicColors.accent.resolvedColor(with: traitCollection) - self.alert = dynamicColors.alert.resolvedColor(with: traitCollection) - self.primaryContent = dynamicColors.primaryContent.resolvedColor(with: traitCollection) - self.secondaryContent = dynamicColors.secondaryContent.resolvedColor(with: traitCollection) - self.tertiaryContent = dynamicColors.tertiaryContent.resolvedColor(with: traitCollection) - self.quaternaryContent = dynamicColors.quaternaryContent.resolvedColor(with: traitCollection) - self.quinaryContent = dynamicColors.quinaryContent.resolvedColor(with: traitCollection) - self.system = dynamicColors.system.resolvedColor(with: traitCollection) - self.background = dynamicColors.background.resolvedColor(with: traitCollection) - - self.namesAndAvatars = dynamicColors.contentAndAvatars - - // Legacy colours - self.quarterlyContent = dynamicColors.quaternaryContent.resolvedColor(with: traitCollection) - self.navigation = dynamicColors.system.resolvedColor(with: traitCollection) - - if userInterfaceStyle == .light { - self.tile = UIColor(rgb: 0xF3F8FD) - self.separator = dynamicColors.quinaryContent.resolvedColor(with: traitCollection) - } else { - self.tile = dynamicColors.quinaryContent.resolvedColor(with: traitCollection) - self.separator = dynamicColors.system.resolvedColor(with: traitCollection) - } - - super.init() - } -} diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index eaf4c4184..35651e6b1 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -185,9 +185,9 @@ class DarkTheme: NSObject, Theme { button.setTitleColor(self.tintColor, for: .normal) } - // MARK: - Theme v2 - var colors = UIColor.elementDark + /// MARK: - Theme v2 + var colors: ColorsUIKit = DarkColors.uiKit - var fonts = UIFont.element + var fonts: FontsUIKit = FontsUIKit(values: ElementFonts()) } diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index f2d9231ec..412390a47 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -14,6 +14,7 @@ limitations under the License. */ +import Foundation import UIKit import DesignKit @@ -193,8 +194,8 @@ class DefaultTheme: NSObject, Theme { button.setTitleColor(self.tintColor, for: .normal) } - // MARK: - Theme v2 - var colors = UIColor.elementLight + /// MARK: - Theme v2 + var colors: ColorsUIKit = LightColors.uiKit - var fonts = UIFont.element + var fonts: FontsUIKit = FontsUIKit(values: ElementFonts()) } diff --git a/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift index ecd152fa2..0ead80e80 100644 --- a/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift +++ b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift @@ -59,14 +59,14 @@ class URLPreviewStore { // Load the persistent stores into the container container.loadPersistentStores { storeDescription, error in if let error = error { - MXLog.error("[URLPreviewStore] Core Data container error: \(error.localizedDescription)") + MXLog.error("[URLPreviewStore] Core Data container", context: error) } if let url = storeDescription.url { do { try FileManager.default.excludeItemFromBackup(at: url) } catch { - MXLog.error("[URLPreviewStore] Cannot exclude Core Data from backup: \(error.localizedDescription)") + MXLog.error("[URLPreviewStore] Cannot exclude Core Data from backup", context: error) } } } @@ -130,7 +130,7 @@ class URLPreviewStore { do { try context.execute(NSBatchDeleteRequest(fetchRequest: request)) } catch { - MXLog.error("[URLPreviewStore] Error executing batch delete request: \(error.localizedDescription)") + MXLog.error("[URLPreviewStore] Error executing batch delete request", context: error) } } @@ -140,7 +140,7 @@ class URLPreviewStore { _ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewDataMO.fetchRequest())) _ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewUserDataMO.fetchRequest())) } catch { - MXLog.error("[URLPreviewStore] Error executing batch delete request: \(error.localizedDescription)") + MXLog.error("[URLPreviewStore] Error executing batch delete request", context: error) } } @@ -171,7 +171,7 @@ class URLPreviewStore { do { try context.save() } catch { - MXLog.error("[URLPreviewStore] Error saving changes: \(error.localizedDescription)") + MXLog.error("[URLPreviewStore] Error saving changes", context: error) } } } diff --git a/Riot/Managers/UserSessions/UserSessionProperties.swift b/Riot/Managers/UserSessions/UserSessionProperties.swift index 4bac87579..3cbc03403 100644 --- a/Riot/Managers/UserSessions/UserSessionProperties.swift +++ b/Riot/Managers/UserSessions/UserSessionProperties.swift @@ -22,6 +22,7 @@ class UserSessionProperties: NSObject { // MARK: - Constants private enum Constants { static let useCaseKey = "useCase" + static let activeFilterKey = "activeFilter" } // MARK: - Properties @@ -64,6 +65,25 @@ class UserSessionProperties: NSObject { case skipped } + /// The active filter in the All Chats screen. + var allChatsActiveFilter: AllChatsActiveFilter? { + get { + guard let rawValue = dictionary[Constants.activeFilterKey] as? String else { return nil } + return AllChatsActiveFilter(rawValue: rawValue) + } set { + dictionary[Constants.activeFilterKey] = newValue?.rawValue + } + } + + /// Represents the active filter in the All Chats screen. + /// Note: The raw string value is used for storage. + public enum AllChatsActiveFilter: String { + case all + case favourites + case people + case unreads + } + // MARK: - Setup /// Create new properties for the specified user ID. diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 030b4463c..631ea4277 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -229,10 +229,11 @@ extension Analytics { /// Updates any user properties to help with creating cohorts. /// /// Only non-nil properties will be updated when calling this method. - func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil, numFavouriteRooms: Int? = nil, numSpaces: Int? = nil) { + func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil, numFavouriteRooms: Int? = nil, numSpaces: Int? = nil, allChatsActiveFilter: UserSessionProperties.AllChatsActiveFilter? = nil) { let userProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: ftueUseCase?.analyticsName, numFavouriteRooms: numFavouriteRooms, - numSpaces: numSpaces) + numSpaces: numSpaces, + allChatsActiveFilter: allChatsActiveFilter?.analyticsName) client.updateUserProperties(userProperties) } @@ -384,7 +385,7 @@ extension Analytics: MXAnalyticsDelegate { capture(event: event) } - func trackNonFatalIssue(_ issue: String, details: [String : Any]?) { + func trackNonFatalIssue(_ issue: String, details: [String: Any]?) { monitoringClient.trackNonFatalIssue(issue, details: details) } } diff --git a/Riot/Modules/Analytics/AnalyticsScreen.swift b/Riot/Modules/Analytics/AnalyticsScreen.swift index 85e304b9d..4c207a3ae 100644 --- a/Riot/Modules/Analytics/AnalyticsScreen.swift +++ b/Riot/Modules/Analytics/AnalyticsScreen.swift @@ -58,7 +58,10 @@ import AnalyticsEvents case spaceMembers case spaceExploreRooms case dialpad - + case spaceBottomSheet + case invites + case createSpace + /// The screen name reported to the AnalyticsEvent. var screenName: AnalyticsEvent.MobileScreen.ScreenName { switch self { @@ -142,6 +145,12 @@ import AnalyticsEvents return .SpaceExploreRooms case .dialpad: return .Dialpad + case .spaceBottomSheet: + return .SpaceBottomSheet + case .invites: + return .Invites + case .createSpace: + return .CreateSpace } } } diff --git a/Riot/Modules/Analytics/AnalyticsUIElement.swift b/Riot/Modules/Analytics/AnalyticsUIElement.swift index 75a976126..3374a5ce2 100644 --- a/Riot/Modules/Analytics/AnalyticsUIElement.swift +++ b/Riot/Modules/Analytics/AnalyticsUIElement.swift @@ -24,7 +24,17 @@ import AnalyticsEvents case threadListFilterItem case spacePanelSelectedSpace case spacePanelSwitchSpace - + case spacePanelSwitchSubSpace + case allChatsRecentsEnabled + case allChatsRecentsDisabled + case allChatsFiltersEnabled + case allChatsFiltersDisabled + case allChatsFilterAll + case allChatsFilterFavourites + case allChatsFilterUnreads + case allChatsFilterPeople + case spaceCreationValidated + /// The element name reported to the AnalyticsEvent. var name: AnalyticsEvent.Interaction.Name { switch self { @@ -40,6 +50,26 @@ import AnalyticsEvents return .SpacePanelSelectedSpace case .spacePanelSwitchSpace: return .SpacePanelSwitchSpace + case .spacePanelSwitchSubSpace: + return .SpacePanelSwitchSubSpace + case .allChatsRecentsEnabled: + return .MobileAllChatsRecentsEnabled + case .allChatsRecentsDisabled: + return .MobileAllChatsRecentsDisabled + case .allChatsFiltersEnabled: + return .MobileAllChatsFiltersEnabled + case .allChatsFiltersDisabled: + return .MobileAllChatsFiltersDisabled + case .allChatsFilterAll: + return .MobileAllChatsFilterAll + case .allChatsFilterFavourites: + return .MobileAllChatsFilterFavourites + case .allChatsFilterUnreads: + return .MobileAllChatsFilterUnreads + case .allChatsFilterPeople: + return .MobileAllChatsFilterPeople + case .spaceCreationValidated: + return .MobileSpaceCreationValidated } } } diff --git a/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift index 65c6b4429..eba8c90cc 100644 --- a/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift +++ b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift @@ -44,6 +44,7 @@ import AnalyticsEvents case linkShare case exploreRooms case spaceMembers + case spaceBottomSheet var trigger: AnalyticsEvent.ViewRoom.Trigger? { switch self { @@ -99,6 +100,8 @@ import AnalyticsEvents return .MobileExploreRooms case .spaceMembers: return .MobileSpaceMembers + case .spaceBottomSheet: + return .MobileSpaceBottomSheet } } } diff --git a/Riot/Modules/Analytics/Helpers/AllChatsActiveFilter+Analytics.swift b/Riot/Modules/Analytics/Helpers/AllChatsActiveFilter+Analytics.swift new file mode 100644 index 000000000..f2519f679 --- /dev/null +++ b/Riot/Modules/Analytics/Helpers/AllChatsActiveFilter+Analytics.swift @@ -0,0 +1,33 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AnalyticsEvents + +extension UserSessionProperties.AllChatsActiveFilter { + var analyticsName: AnalyticsEvent.UserProperties.AllChatsActiveFilter { + switch self { + case .all: + return .All + case .unreads: + return .Unreads + case .favourites: + return .Favourites + case .people: + return .People + } + } +} diff --git a/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift b/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift index 341f0789a..e0d219279 100644 --- a/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift +++ b/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift @@ -20,12 +20,13 @@ import AnalyticsEvents extension AnalyticsEvent.UserProperties { // Initializer for Element. Strips all Web properties. - public init(ftueUseCaseSelection: FtueUseCaseSelection?, numFavouriteRooms: Int?, numSpaces: Int?) { + public init(ftueUseCaseSelection: FtueUseCaseSelection?, numFavouriteRooms: Int?, numSpaces: Int?, allChatsActiveFilter: AllChatsActiveFilter?) { self.init(WebMetaSpaceFavouritesEnabled: nil, WebMetaSpaceHomeAllRooms: nil, WebMetaSpaceHomeEnabled: nil, WebMetaSpaceOrphansEnabled: nil, WebMetaSpacePeopleEnabled: nil, + allChatsActiveFilter: allChatsActiveFilter, ftueUseCaseSelection: ftueUseCaseSelection, numFavouriteRooms: numFavouriteRooms, numSpaces: numSpaces) diff --git a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift index 8d6dd4d50..6b27affea 100644 --- a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift +++ b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift @@ -81,7 +81,8 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { // Merge the updated user properties with the existing ones self.pendingUserProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection, numFavouriteRooms: userProperties.numFavouriteRooms ?? pendingUserProperties.numFavouriteRooms, - numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces) + numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces, + allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter) } // MARK: - Private diff --git a/Riot/Modules/Analytics/SentryMonitoringClient.swift b/Riot/Modules/Analytics/SentryMonitoringClient.swift index 32b2169f2..b848c02a9 100644 --- a/Riot/Modules/Analytics/SentryMonitoringClient.swift +++ b/Riot/Modules/Analytics/SentryMonitoringClient.swift @@ -32,7 +32,9 @@ struct SentryMonitoringClient { MXLog.debug("[SentryMonitoringClient] Started") SentrySDK.start { options in options.dsn = Self.sentryDSN - options.tracesSampleRate = 1.0 + + // Collecting only 10% of all events + options.tracesSampleRate = 0.1 options.beforeSend = { event in MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)") diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 90038c046..8a8545a4c 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -620,13 +620,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [MXSDKOptions.sharedInstance.profiler resume]; - // Force each session to refresh here their publicised groups by user dictionary. - // When these publicised groups are retrieved for a user, they are cached and reused until the app is backgrounded and enters in the foreground again - for (MXSession *session in mxSessionArray) - { - [session markOutdatedPublicisedGroupsByUserData]; - } - _isAppForeground = YES; } @@ -1319,7 +1312,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni NSString *roomIdOrAlias; NSString *eventId; NSString *userId; - NSString *groupId; // Check permalink to room or event if ([pathParams[0] isEqualToString:@"room"] && pathParams.count >= 2) @@ -1330,11 +1322,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Is it a link to an event of a room? eventId = (pathParams.count >= 3) ? pathParams[2] : nil; } - else if ([pathParams[0] isEqualToString:@"group"] && pathParams.count >= 2) - { - // The link is the form of "/group/[groupId]" - groupId = pathParams[1]; - } else if (([pathParams[0] hasPrefix:@"#"] || [pathParams[0] hasPrefix:@"!"]) && pathParams.count >= 1) { // The link is the form of "/#/[roomIdOrAlias]" or "/#/[roomIdOrAlias]/[eventId]" @@ -1413,7 +1400,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni event = eventFromServer; dispatch_group_leave(eventDispatchGroup); } failure:^(NSError *error) { - MXLogError(@"[LegacyAppDelegate] handleUniversalLinkWithParameters: event fetch failed: %@", error); + MXLogErrorDetails(@"[LegacyAppDelegate] handleUniversalLinkWithParameters: event fetch failed", @{ + @"error": error ?: @"unknown" + }); dispatch_group_leave(eventDispatchGroup); }]; } @@ -1641,44 +1630,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni continueUserActivity = YES; } - else if (groupId) - { - // @FIXME: In case of multi-account, ask the user which one to use - MXKAccount* account = accountManager.activeAccounts.firstObject; - if (account) - { - MXGroup *group = [account.mxSession groupWithGroupId:groupId]; - - if (!group) - { - // Create a group instance to display its preview - group = [[MXGroup alloc] initWithGroupId:groupId]; - } - - // Display the group details - [self showGroup:group withMatrixSession:account.mxSession presentationParamters:presentationParameters]; - - continueUserActivity = YES; - } - else - { - // There is no account. The app will display the AuthenticationVC. - // Wait for a successful login - MXLogDebug(@"[AppDelegate] Universal link: The user is not logged in. Wait for a successful login"); - universalLinkFragmentPending = fragment; - - // Register an observer in order to handle new account - universalLinkWaitingObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountManagerDidAddAccountNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - // Check that 'fragment' has not been cancelled - if ([self->universalLinkFragmentPending isEqualToString:fragment]) - { - MXLogDebug(@"[AppDelegate] Universal link: The user is now logged in. Retry the link"); - [self handleUniversalLinkWithParameters:universalLinkParameters]; - } - }]; - } - } else { // Unknown command: Do nothing except coming back to the main screen @@ -3074,26 +3025,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -#pragma mark - Matrix Groups handling - -- (void)showGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession presentationParamters:(ScreenPresentationParameters*)presentationParameters -{ - void(^showGroup)(void) = ^{ - // Select group to display its details (dispatch this action in order to let TabBarController end its refresh) - [self.masterTabBarController selectGroup:group inMatrixSession:mxSession presentationParameters:presentationParameters]; - }; - - if (presentationParameters.restoreInitialDisplay) - { - [self restoreInitialDisplay:^{ - showGroup(); - }]; - } - else - { - showGroup(); - } -} +#pragma mark - VoIP - (void)promptForStunServerFallback { diff --git a/Riot/Modules/Authentication/SessionVerificationListener.swift b/Riot/Modules/Authentication/SessionVerificationListener.swift index f8973d45a..7d5d7f48b 100644 --- a/Riot/Modules/Authentication/SessionVerificationListener.swift +++ b/Riot/Modules/Authentication/SessionVerificationListener.swift @@ -99,7 +99,7 @@ class SessionVerificationListener { MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Bootstrap succeeded") self.completion?(.authenticationIsComplete) } failure: { error in - MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed. Error: \(error)") + MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error) crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } @@ -128,7 +128,7 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } } failure: { [weak self] error in - MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state with error: \(error)") + MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error) crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self?.completion?(.authenticationIsComplete) } diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h index 7f0f4a447..eb1b0c6c6 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h @@ -35,6 +35,7 @@ typedef NS_ENUM(NSInteger, RecentsDataSourceMode) RecentsDataSourceModeFavourites, RecentsDataSourceModePeople, RecentsDataSourceModeRooms, + RecentsDataSourceModeRoomInvites, RecentsDataSourceModeAllChats }; diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index dcf25b5cd..18ea65eca 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -174,6 +174,12 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (RecentsDataSourceSections *)makeDataSourceSections { NSMutableArray *types = [NSMutableArray array]; + if (self.recentsDataSourceMode == RecentsDataSourceModeRoomInvites) + { + [types addObject:@(RecentsDataSourceSectionTypeInvites)]; + return [[RecentsDataSourceSections alloc] initWithSectionTypes:types.copy]; + } + if (self.crossSigningBannerDisplay != CrossSigningBannerDisplayNone) { [types addObject:@(RecentsDataSourceSectionTypeCrossSigningBanner)]; @@ -183,7 +189,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [types addObject:@(RecentsDataSourceSectionTypeSecureBackupBanner)]; } - if (!BuildSettings.newAppLayoutEnabled && self.invitesCellDataArray.count > 0) + if (self.invitesCellDataArray.count > 0) { [types addObject:@(RecentsDataSourceSectionTypeInvites)]; } @@ -229,11 +235,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [types addObject:@(RecentsDataSourceSectionTypeAllChats)]; } - if (self.currentSpace == nil && BuildSettings.newAppLayoutEnabled && self.invitesCellDataArray.count > 0) - { - [types addObject:@(RecentsDataSourceSectionTypeInvites)]; - } - if (self.currentSpace != nil && self.suggestedRoomCellDataArray.count > 0) { [types addObject:@(RecentsDataSourceSectionTypeSuggestedRooms)]; @@ -625,7 +626,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } else if (sectionType == RecentsDataSourceSectionTypeInvites && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_INVITES)) { - count = self.invitesCellDataArray.count; + if (self.recentsDataSourceMode == RecentsDataSourceModeAllChats) + { + count = 1; + } + else { + count = self.invitesCellDataArray.count; + } } else if (sectionType == RecentsDataSourceSectionTypeSuggestedRooms && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_SUGGESTED)) { @@ -637,7 +644,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } else if (sectionType == RecentsDataSourceSectionTypeAllChats && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_ALL_CHATS)) { - count = self.allChatsRoomCellDataArray.count; + count = self.allChatsRoomCellDataArray.count ?: 1; } // Adjust this count according to the potential dragged cell. @@ -660,6 +667,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou if (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner || sectionType == RecentsDataSourceSectionTypeCrossSigningBanner || sectionType == RecentsDataSourceSectionTypeBreadcrumbs || + (sectionType == RecentsDataSourceSectionTypeInvites && self.recentsDataSourceMode == RecentsDataSourceModeAllChats) || (sectionType == RecentsDataSourceSectionTypeAllChats && !self.allChatsFilterOptions.optionsCount)) { return 0.0; @@ -859,6 +867,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou if (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner || sectionType == RecentsDataSourceSectionTypeCrossSigningBanner || sectionType == RecentsDataSourceSectionTypeBreadcrumbs || + (sectionType == RecentsDataSourceSectionTypeInvites && self.recentsDataSourceMode == RecentsDataSourceModeRoomInvites) || (sectionType == RecentsDataSourceSectionTypeAllChats && !self.allChatsFilterOptions.optionsCount)) { return nil; @@ -1052,8 +1061,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return cell; } else if ((sectionType == RecentsDataSourceSectionTypeConversation && !self.conversationCellDataArray.count) - || (sectionType == RecentsDataSourceSectionTypePeople && !self.peopleCellDataArray.count) - || (sectionType == RecentsDataSourceSectionTypeAllChats && !self.allChatsRoomCellDataArray.count)) + || (sectionType == RecentsDataSourceSectionTypePeople && !self.peopleCellDataArray.count)) { MXKTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCell defaultReuseIdentifier]]; if (!tableViewCell) @@ -1080,6 +1088,23 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return tableViewCell; } + else if (sectionType == RecentsDataSourceSectionTypeAllChats && !self.allChatsRoomCellDataArray.count) { + RecentEmptySectionTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[RecentEmptySectionTableViewCell defaultReuseIdentifier]]; + + tableViewCell.iconView.image = self.searchPatternsList ? [UIImage systemImageNamed:@"magnifyingglass"] : AssetImages.allChatsEmptyListPlaceholderIcon.image; + tableViewCell.titleLabel.text = self.searchPatternsList ? VectorL10n.allChatsNothingFoundPlaceholderTitle : VectorL10n.allChatsEmptyListPlaceholderTitle; + tableViewCell.messageLabel.text = self.searchPatternsList ? VectorL10n.allChatsNothingFoundPlaceholderMessage : VectorL10n.allChatsEmptyUnreadsPlaceholderMessage; + + return tableViewCell; + } + else if (sectionType == RecentsDataSourceSectionTypeInvites && self.recentsDataSourceMode == RecentsDataSourceModeAllChats) + { + RecentsInvitesTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[RecentsInvitesTableViewCell defaultReuseIdentifier]]; + + tableViewCell.invitesCount = self.recentsListService.invitedRoomListData.counts.numberOfRooms; + + return tableViewCell; + } return [super tableView:tableView cellForRowAtIndexPath:indexPath]; } @@ -1184,10 +1209,17 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return self.droppingCellBackGroundView.frame.size.height; } if ((sectionType == RecentsDataSourceSectionTypeConversation && !self.conversationCellDataArray.count) - || (sectionType == RecentsDataSourceSectionTypePeople && !self.peopleCellDataArray.count)) + || (sectionType == RecentsDataSourceSectionTypePeople && !self.peopleCellDataArray.count)) { return 50.0; } + if (sectionType == RecentsDataSourceSectionTypeAllChats && !self.allChatsRoomCellDataArray.count) { + return 300.0; + } + if (sectionType == RecentsDataSourceSectionTypeInvites && self.recentsDataSourceMode == RecentsDataSourceModeAllChats) + { + return 32.0; + } // Override this method here to use our own cellDataAtIndexPath id cellData = [self cellDataAtIndexPath:indexPath]; @@ -1498,7 +1530,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (BOOL)isDraggableCellAt:(NSIndexPath*)path { - if (_recentsDataSourceMode == RecentsDataSourceModePeople || _recentsDataSourceMode == RecentsDataSourceModeRooms) + if (_recentsDataSourceMode == RecentsDataSourceModePeople || _recentsDataSourceMode == RecentsDataSourceModeRooms || _recentsDataSourceMode == RecentsDataSourceModeRoomInvites) { return NO; } diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index ba23d57c4..3ff5775d0 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1505,6 +1505,10 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } } +- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath { + return [VectorL10n leave]; +} + #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView @@ -2042,7 +2046,11 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } else if (section >= self.recentsTableView.numberOfSections) { - MXLogFailure(@"[RecentsViewController] Section %ld is invalid in a table view with only %ld sections", section, self.recentsTableView.numberOfSections); + NSDictionary *details = @{ + @"section": @(section), + @"number_of_sections": @(self.recentsTableView.numberOfSections) + }; + MXLogFailureDetails(@"[RecentsViewController] Section in a table view is invalid", details); } } } diff --git a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift index 64c19bb1a..7370f0f94 100644 --- a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift +++ b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift @@ -35,7 +35,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol { private var invitedRoomListDataFetcher: MXRoomListDataFetcher? { switch mode { - case .home, .allChats: + case .home, .allChats, .roomInvites: return invitedRoomListDataFetcherForHome case .people: return invitedRoomListDataFetcherForPeople @@ -87,6 +87,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol { .favourites: [.favorited], .people: [.invited, .directPeople], .rooms: [.invited, .conversationRooms, .suggested], + .roomInvites: [.invited], .allChats: [.breadcrumbs, .favorited, .directHome, .invited, .allChats, .lowPriority, .serverNotice, .suggested] ] @@ -140,7 +141,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol { if space != nil, let fetcher = suggestedRoomListDataFetcher, fetcherTypes.contains(.suggested) { result.append(fetcher) } - if let fetcher = breadcrumbsRoomListDataFetcher, fetcherTypes.contains(.breadcrumbs) { + if let fetcher = breadcrumbsRoomListDataFetcher, fetcherTypes.contains(.breadcrumbs), shouldShowBreadcrumbs { result.append(fetcher) } if let fetcher = allChatsRoomListDataFetcher, fetcherTypes.contains(.allChats) { @@ -493,7 +494,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol { } private var shouldShowBreadcrumbs: Bool { - return fetcherTypesForMode[mode]?.contains(.breadcrumbs) ?? false + return AllChatsLayoutSettingsManager.shared.allChatLayoutSettings.sections.contains(.recents) && (fetcherTypesForMode[mode]?.contains(.breadcrumbs) ?? false) } private var shouldShowAllChats: Bool { diff --git a/Riot/Modules/Common/Recents/Views/RecentEmptySectionTableViewCell.swift b/Riot/Modules/Common/Recents/Views/RecentEmptySectionTableViewCell.swift new file mode 100644 index 000000000..11a7c794c --- /dev/null +++ b/Riot/Modules/Common/Recents/Views/RecentEmptySectionTableViewCell.swift @@ -0,0 +1,59 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable + +/// `RecentEmptySectionTableViewCell` can be used as a placeholder for empty sections. +class RecentEmptySectionTableViewCell: UITableViewCell, NibReusable, Themable { + + @IBOutlet private var iconBackgroundView: UIView! + @IBOutlet var iconView: UIImageView! + @IBOutlet var titleLabel: UILabel! + @IBOutlet var messageLabel: UILabel! + + @objc static func defaultReuseIdentifier() -> String { + return reuseIdentifier + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.iconBackgroundView.layer.cornerRadius = self.iconBackgroundView.bounds.height / 2 + self.iconBackgroundView.layer.masksToBounds = true + + self.selectionStyle = .none + + update(theme: ThemeService.shared().theme) + } + + // MARK: - Themable + + func update(theme: Theme) { + self.backgroundColor = theme.colors.background + + self.iconBackgroundView.backgroundColor = theme.colors.quinaryContent + self.iconView.tintColor = theme.colors.secondaryContent + + self.titleLabel.textColor = theme.colors.primaryContent + self.titleLabel.font = theme.fonts.title3SB + + self.messageLabel.textColor = theme.colors.secondaryContent + self.messageLabel.font = theme.fonts.callout + } +} diff --git a/Riot/Modules/Common/Recents/Views/RecentEmptySectionTableViewCell.xib b/Riot/Modules/Common/Recents/Views/RecentEmptySectionTableViewCell.xib new file mode 100644 index 000000000..bcae1f769 --- /dev/null +++ b/Riot/Modules/Common/Recents/Views/RecentEmptySectionTableViewCell.xib @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index 2e3052fc0..6cd41134f 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -95,7 +95,7 @@ // Notify unreads and bing if (roomCellData.hasUnread) { - self.missedNotifAndUnreadIndicator.hidden = BuildSettings.newAppLayoutEnabled; + self.missedNotifAndUnreadIndicator.hidden = NO; if (0 < roomCellData.notificationCount) { diff --git a/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.swift b/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.swift new file mode 100644 index 000000000..17ff6e49c --- /dev/null +++ b/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.swift @@ -0,0 +1,71 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable + +/// `RecentsInvitesTableViewCell` can be used as a placeholder to show invites number +class RecentsInvitesTableViewCell: UITableViewCell, NibReusable, Themable { + + // MARK: - Outlet + + @IBOutlet weak private var badgeLabel: BadgeLabel! + @IBOutlet weak private var titleLabel: UILabel! + + // MARK: - Properties + + @objc var invitesCount: Int = 0 { + didSet { + badgeLabel.text = "\(invitesCount)" + } + } + + // MARK: - NibReusable + + @objc static func defaultReuseIdentifier() -> String { + return reuseIdentifier + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + setupView() + update(theme: ThemeService.shared().theme) + } + + // MARK: - Themable + + func update(theme: Theme) { + self.backgroundColor = theme.colors.background + + badgeLabel.badgeColor = theme.colors.alert + badgeLabel.textColor = theme.colors.background + badgeLabel.font = theme.fonts.footnoteSB + + titleLabel.textColor = theme.colors.accent + } + + // MARK: - Private + + private func setupView() { + self.selectionStyle = .none + + titleLabel.text = VectorL10n.roomRecentsInvitesSection.capitalized + update(theme: ThemeService.shared().theme) + } +} diff --git a/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.xib b/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.xib new file mode 100644 index 000000000..25102939a --- /dev/null +++ b/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.xib @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Common/Recents/Views/RootTabEmptyView.swift b/Riot/Modules/Common/Recents/Views/RootTabEmptyView.swift index 9bda2067c..3d14f605e 100644 --- a/Riot/Modules/Common/Recents/Views/RootTabEmptyView.swift +++ b/Riot/Modules/Common/Recents/Views/RootTabEmptyView.swift @@ -17,6 +17,14 @@ import Foundation import Reusable +/// `RootTabEmptyViewDisplayMode` defines the way image and text should be displayed +enum RootTabEmptyViewDisplayMode { + /// Default display: fitted for big images + case `default` + /// The image is shrinked to fit icon size and is rendered as templated. + case icon +} + /// `RootTabEmptyView` is a view to display when there is no UI item to display on a screen. @objcMembers final class RootTabEmptyView: UIView, NibLoadable { @@ -25,11 +33,13 @@ final class RootTabEmptyView: UIView, NibLoadable { // MARK: Outlets + @IBOutlet private weak var iconBackgroundView: UIView! + @IBOutlet private weak var iconView: UIImageView! @IBOutlet private weak var imageView: UIImageView! @IBOutlet private weak var titleLabel: UILabel! @IBOutlet private weak var informationLabel: UILabel! @IBOutlet private(set) weak var contentView: UIView! - + // MARK: Private private var theme: Theme! @@ -50,14 +60,25 @@ final class RootTabEmptyView: UIView, NibLoadable { super.awakeFromNib() self.informationLabel.text = VectorL10n.homeEmptyViewInformation + + self.iconBackgroundView.layer.masksToBounds = true + self.iconBackgroundView.layer.cornerRadius = self.iconBackgroundView.bounds.width / 2 + self.iconBackgroundView.isHidden = true } // MARK: - Public func fill(with image: UIImage, title: String, informationText: String) { + fill(with: image, title: title, informationText: informationText, displayMode: .default) + } + + func fill(with image: UIImage, title: String, informationText: String, displayMode: RootTabEmptyViewDisplayMode) { self.imageView.image = image + self.iconView.image = image.withRenderingMode(.alwaysTemplate) self.titleLabel.text = title self.informationLabel.text = informationText + self.imageView.isHidden = displayMode != .default + self.iconBackgroundView.isHidden = displayMode != .icon } } @@ -71,5 +92,7 @@ extension RootTabEmptyView: Themable { self.titleLabel.textColor = theme.textPrimaryColor self.informationLabel.textColor = theme.textSecondaryColor + self.iconBackgroundView.backgroundColor = theme.colors.quinaryContent + self.iconView.tintColor = theme.textSecondaryColor } } diff --git a/Riot/Modules/Common/Recents/Views/RootTabEmptyView.xib b/Riot/Modules/Common/Recents/Views/RootTabEmptyView.xib index fc840087c..39bcea11a 100644 --- a/Riot/Modules/Common/Recents/Views/RootTabEmptyView.xib +++ b/Riot/Modules/Common/Recents/Views/RootTabEmptyView.xib @@ -1,9 +1,9 @@ - + - + @@ -11,13 +11,30 @@ - + + + + + + + + + + + + + + + + + + @@ -37,9 +54,11 @@ + + @@ -66,11 +85,13 @@ + + - + diff --git a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift index a13015853..0fe8d1fee 100644 --- a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift +++ b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift @@ -18,6 +18,14 @@ import UIKit class AllChatsFilterOptionListView: UIView, Themable { + // MARK: - Constants + + private enum Constants { + static let separatorHeight: Double = 1 + } + + // MARK: - Option definition + class Option { let type: AllChatsLayoutFilterType let name: String @@ -84,11 +92,11 @@ class AllChatsFilterOptionListView: UIView, Themable { func update(theme: Theme) { backgroundColor = theme.colors.background.withAlphaComponent(0.7) - tabListView.itemFont = theme.fonts.callout + tabListView.itemFont = theme.fonts.calloutSB tabListView.tintColor = theme.colors.accent - tabListView.unselectedItemColor = theme.colors.secondaryContent + tabListView.unselectedItemColor = theme.colors.tertiaryContent - separator.backgroundColor = theme.colors.tertiaryContent + separator.backgroundColor = theme.colors.system } // MARK: - Private @@ -99,10 +107,11 @@ class AllChatsFilterOptionListView: UIView, Themable { addSubview(separator) separator.translatesAutoresizingMaskIntoConstraints = false - separator.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true + separator.bottomAnchor.constraint(equalTo: self.bottomAnchor, + constant: -(TabListView.Constants.cursorHeight - Constants.separatorHeight) / 2).isActive = true separator.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true separator.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true - separator.heightAnchor.constraint(equalToConstant: 0.5).isActive = true + separator.heightAnchor.constraint(equalToConstant: Constants.separatorHeight).isActive = true tabListView.delegate = self vc_addSubViewMatchingParent(tabListView) diff --git a/Riot/Modules/Common/SectionHeaders/TabListView.swift b/Riot/Modules/Common/SectionHeaders/TabListView.swift index 4287a4ed0..2690c90ff 100644 --- a/Riot/Modules/Common/SectionHeaders/TabListView.swift +++ b/Riot/Modules/Common/SectionHeaders/TabListView.swift @@ -22,6 +22,16 @@ protocol TabListViewDelegate: AnyObject { class TabListView: UIView { + // MARK: - Constants + + enum Constants { + static let cursorHeight: Double = 3 + static let itemSpacing: Double = 30 + static let cursorPadding: Double = 6 + } + + // MARK: - Item definition + class Item { let id: Any let text: String? @@ -36,6 +46,8 @@ class TabListView: UIView { } } + // MARK: - Properties + weak var delegate: TabListViewDelegate? var items: [Item] = [] { didSet { @@ -62,11 +74,6 @@ class TabListView: UIView { // MARK: - Private - private enum Constants { - static let cursorHeight: Double = 2 - static let itemSpacing: Double = 30 - } - private var itemViews: [UIButton] = [] private let scrollView = UIScrollView(frame: .zero) private let cursorView = UIView(frame: .zero) @@ -149,6 +156,7 @@ class TabListView: UIView { cursorView.backgroundColor = tintColor cursorView.isUserInteractionEnabled = false + cursorView.layer.masksToBounds = true scrollView.addSubview(cursorView) } @@ -190,21 +198,23 @@ class TabListView: UIView { let focusedButton = itemViews[Int(integral)] let nextButtonIndex = Int(integral) + 1 - let x: Double - let width: Double + let x: CGFloat + let width: CGFloat + let focusedButtonFrame: CGRect = titleLabelFrame(with: focusedButton).insetBy(dx: -Constants.cursorPadding, dy: 0) if nextButtonIndex < itemViews.count { - let nextButton = itemViews[nextButtonIndex] - x = focusedButton.frame.minX + (nextButton.frame.minX - focusedButton.frame.minX) * fractional - width = focusedButton.frame.width + (nextButton.frame.width - focusedButton.frame.width) * fractional + let nextButtonFrame = titleLabelFrame(with: itemViews[nextButtonIndex]).insetBy(dx: -Constants.cursorPadding, dy: 0) + x = focusedButtonFrame.minX + (nextButtonFrame.minX - focusedButtonFrame.minX) * fractional + width = focusedButtonFrame.width + (nextButtonFrame.width - focusedButtonFrame.width) * fractional } else { - x = focusedButton.frame.minX - width = focusedButton.frame.width + x = focusedButtonFrame.minX + width = focusedButtonFrame.width } cursorView.frame = CGRect(x: x, y: bounds.height - Constants.cursorHeight, width: width, height: Constants.cursorHeight) + cursorView.layer.cornerRadius = cursorView.bounds.height / 2 for button in self.itemViews { if button == focusedButton { @@ -214,5 +224,16 @@ class TabListView: UIView { } } } + + private func titleLabelFrame(with button: UIButton) -> CGRect { + guard let titleLabel = button.titleLabel else { + return button.frame + } + + return CGRect(x: button.frame.minX + titleLabel.frame.minX, + y: button.frame.minY + titleLabel.frame.minY, + width: titleLabel.frame.width, + height: titleLabel.frame.height) + } } diff --git a/Riot/Modules/Communities/DataSources/GroupsDataSource.h b/Riot/Modules/Communities/DataSources/GroupsDataSource.h deleted file mode 100644 index 836f6fdaa..000000000 --- a/Riot/Modules/Communities/DataSources/GroupsDataSource.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MatrixKit.h" - -/** - 'GroupsDataSource' class inherits from 'MXKSessionGroupsDataSource' to define the Riot groups source. - */ -@interface GroupsDataSource : MXKSessionGroupsDataSource - -@end diff --git a/Riot/Modules/Communities/DataSources/GroupsDataSource.m b/Riot/Modules/Communities/DataSources/GroupsDataSource.m deleted file mode 100644 index 69b0100f5..000000000 --- a/Riot/Modules/Communities/DataSources/GroupsDataSource.m +++ /dev/null @@ -1,126 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupsDataSource.h" -#import "GeneratedInterface-Swift.h" - -@interface GroupsDataSource() - -@property (nonatomic) NSInteger betaAnnounceSection; -@property (nonatomic) BOOL showBetaAnnounce; - -@end - -@implementation GroupsDataSource - -- (instancetype)initWithMatrixSession:(MXSession *)matrixSession -{ - self = [super initWithMatrixSession:matrixSession]; - if (self) - { - // TODO: Hide the banner for the moment. Wait for iterations on it. -// _showBetaAnnounce = !RiotSettings.shared.hideSpaceBetaAnnounce; - _showBetaAnnounce = NO; - } - return self; -} - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - NSInteger count = 0; - self.betaAnnounceSection = self.groupInvitesSection = self.joinedGroupsSection = -1; - - // Check whether all data sources are ready before rendering groups. - if (self.state == MXKDataSourceStateReady) - { - if (self.showBetaAnnounce) - { - self.betaAnnounceSection = count++; - } - if (groupsInviteCellDataArray.count) - { - self.groupInvitesSection = count++; - } - if (groupsCellDataArray.count) - { - self.joinedGroupsSection = count++; - } - } - - return count; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section == self.betaAnnounceSection) - { - BetaAnnounceCell *cell = [tableView dequeueReusableCellWithIdentifier:BetaAnnounceCell.reuseIdentifier forIndexPath:indexPath]; - [cell vc_hideSeparator]; - [cell updateWithTheme:ThemeService.shared.theme]; - cell.delegate = self; - return cell; - - } - - return [super tableView:tableView cellForRowAtIndexPath:indexPath]; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - if (section == self.betaAnnounceSection) - { - return 1; - } - - return [super tableView:tableView numberOfRowsInSection:section]; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - NSString* sectionTitle = nil; - - // Check whether there are more than 1 section. - if (self.groupInvitesSection != -1) - { - if (section == self.groupInvitesSection) - { - sectionTitle = [VectorL10n groupInviteSection]; - } - else if (section == self.joinedGroupsSection) - { - sectionTitle = [VectorL10n groupSection]; - } - } - - return sectionTitle; -} - -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - // Enable edition only for the joined groups. - return (indexPath.section == self.joinedGroupsSection); -} - -#pragma mark - BetaAnnounceCellDelegate - -- (void)betaAnnounceCellDidTapCloseButton:(BetaAnnounceCell *)cell -{ - self.showBetaAnnounce = NO; - RiotSettings.shared.hideSpaceBetaAnnounce = YES; - [self.delegate dataSource:self didCellChange:nil]; -} - -@end diff --git a/Riot/Modules/Communities/GroupsViewController.h b/Riot/Modules/Communities/GroupsViewController.h deleted file mode 100644 index ef2a4c486..000000000 --- a/Riot/Modules/Communities/GroupsViewController.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MatrixKit.h" - -/** - The `GroupsViewController` screen is the view controller displayed when `Groups` tab is selected. - */ -@interface GroupsViewController : MXKGroupListViewController -{ -@protected - /** - The group identifier related to the cell which is in editing mode (if any). - */ - NSString *editedGroupId; - - /** - Current alert (if any). - */ - UIAlertController *currentAlert; - - /** - The image view of the (+) button. - */ - UIImageView* plusButtonImageView; -} - -/** - If YES, the table view will scroll at the top on the next data source refresh. - It comes back to NO after each refresh. - */ -@property (nonatomic) BOOL shouldScrollToTopOnRefresh; - -/** - Tell whether the search bar at the top of the groups table is enabled. YES by default. - */ -@property (nonatomic) BOOL enableSearchBar; - - -+ (instancetype)instantiate; - -@end diff --git a/Riot/Modules/Communities/GroupsViewController.m b/Riot/Modules/Communities/GroupsViewController.m deleted file mode 100644 index 3f954e814..000000000 --- a/Riot/Modules/Communities/GroupsViewController.m +++ /dev/null @@ -1,646 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupsViewController.h" - -#import "GroupTableViewCell.h" -#import "GroupInviteTableViewCell.h" - -#import "GeneratedInterface-Swift.h" - -@interface GroupsViewController () -{ - // Tell whether a groups refresh is pending (suspended during editing mode). - BOOL isRefreshPending; - - // Observe UIApplicationDidEnterBackgroundNotification to cancel editing mode when app leaves the foreground state. - __weak id UIApplicationDidEnterBackgroundNotificationObserver; - - // Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar. - __weak id kAppDelegateDidTapStatusBarNotificationObserver; - - MXHTTPOperation *currentRequest; - - // The fake search bar displayed at the top of the recents table. We switch on the actual search bar (self.groupsSearchBar) - // when the user selects it. - UISearchBar *tableSearchBar; - - // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. - __weak id kThemeServiceDidChangeThemeNotificationObserver; -} - -@property (nonatomic) AnalyticsScreenTracker *screenTracker; - -@end - -@implementation GroupsViewController - -+ (instancetype)instantiate -{ - UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; - GroupsViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"GroupsViewController"]; - return viewController; -} - -- (void)finalizeInit -{ - [super finalizeInit]; - - // Setup `MXKViewControllerHandling` properties - self.enableBarTintColorStatusChange = NO; - self.rageShakeManager = [RageShakeManager sharedManager]; - - // Enable the search bar in the recents table, and remove the search option from the navigation bar. - _enableSearchBar = YES; - self.enableBarButtonSearch = NO; - - // Create the fake search bar - tableSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 600, 44)]; - tableSearchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth; - tableSearchBar.showsCancelButton = NO; - tableSearchBar.placeholder = [VectorL10n searchDefaultPlaceholder]; - tableSearchBar.delegate = self; - - // Set itself as delegate by default. - self.delegate = self; - - self.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenMyGroups]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.view.accessibilityIdentifier = @"GroupsVCView"; - self.groupsTableView.accessibilityIdentifier = @"GroupsVCTableView"; - - //Register here the customized cell view class used to render groups - [self.groupsTableView registerNib:GroupTableViewCell.nib forCellReuseIdentifier:GroupTableViewCell.defaultReuseIdentifier]; - [self.groupsTableView registerNib:GroupInviteTableViewCell.nib forCellReuseIdentifier:GroupInviteTableViewCell.defaultReuseIdentifier]; - [self.groupsTableView registerNib:BetaAnnounceCell.nib forCellReuseIdentifier:BetaAnnounceCell.reuseIdentifier]; - - // Hide line separators of empty cells - self.groupsTableView.tableFooterView = [[UIView alloc] init]; - - // Enable self-sizing cells and section headers. - self.groupsTableView.rowHeight = UITableViewAutomaticDimension; - self.groupsTableView.estimatedRowHeight = 74; - self.groupsTableView.sectionHeaderHeight = UITableViewAutomaticDimension; - self.groupsTableView.estimatedSectionHeaderHeight = 30; - self.groupsTableView.estimatedSectionFooterHeight = 0; - - MXWeakify(self); - - // Observe UIApplicationDidEnterBackgroundNotification to refresh bubbles when app leaves the foreground state. - UIApplicationDidEnterBackgroundNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - MXStrongifyAndReturnIfNil(self); - - // Leave potential editing mode - [self cancelEditionMode:self->isRefreshPending]; - - }]; - - self.groupsSearchBar.autocapitalizationType = UITextAutocapitalizationTypeNone; - self.groupsSearchBar.placeholder = [VectorL10n searchDefaultPlaceholder]; - - // @TODO: Add programmatically the (+) button. -// plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"plus_floating_action"] -// target:self -// action:@selector(onPlusButtonPressed)]; - - // Observe user interface theme change. - kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - MXStrongifyAndReturnIfNil(self); - - [self userInterfaceThemeDidChange]; - - }]; - [self userInterfaceThemeDidChange]; -} - -- (void)userInterfaceThemeDidChange -{ - [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar]; - - self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor; - - // Use the primary bg color for the recents table view in plain style. - self.groupsTableView.backgroundColor = ThemeService.shared.theme.backgroundColor; - topview.backgroundColor = ThemeService.shared.theme.headerBackgroundColor; - self.view.backgroundColor = ThemeService.shared.theme.backgroundColor; - - [ThemeService.shared.theme applyStyleOnSearchBar:tableSearchBar]; - [ThemeService.shared.theme applyStyleOnSearchBar:self.groupsSearchBar]; - - if (self.groupsTableView.dataSource) - { - // Force table refresh - [self cancelEditionMode:YES]; - } - - [self setNeedsStatusBarAppearanceUpdate]; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return ThemeService.shared.theme.statusBarStyle; -} - -- (void)destroy -{ - [super destroy]; - - if (currentRequest) - { - [currentRequest cancel]; - currentRequest = nil; - } - - if (currentAlert) - { - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - currentAlert = nil; - } - - if (UIApplicationDidEnterBackgroundNotificationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:UIApplicationDidEnterBackgroundNotificationObserver]; - UIApplicationDidEnterBackgroundNotificationObserver = nil; - } - - if (kThemeServiceDidChangeThemeNotificationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver]; - kThemeServiceDidChangeThemeNotificationObserver = nil; - } -} - -- (void)setEditing:(BOOL)editing animated:(BOOL)animated -{ - [super setEditing:editing animated:animated]; - - self.groupsTableView.editing = editing; -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self.screenTracker trackScreen]; - - // Deselect the current selected row, it will be restored on viewDidAppear (if any) - NSIndexPath *indexPath = [self.groupsTableView indexPathForSelectedRow]; - if (indexPath) - { - [self.groupsTableView deselectRowAtIndexPath:indexPath animated:NO]; - } - - MXWeakify(self); - - // Observe kAppDelegateDidTapStatusBarNotificationObserver. - kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - MXStrongifyAndReturnIfNil(self); - - [self scrollToTop:YES]; - - }]; - [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = ThemeService.shared.theme.tintColor; -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - // Leave potential editing mode - [self cancelEditionMode:NO]; - - if (kAppDelegateDidTapStatusBarNotificationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver]; - kAppDelegateDidTapStatusBarNotificationObserver = nil; - } -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - - // Release the current selected item (if any) except if the second view controller is still visible. - if (self.splitViewController.isCollapsed) - { - // Release the current selected group (if any). - [[AppDelegate theDelegate].masterTabBarController releaseSelectedItem]; - } - else - { - // In case of split view controller where the primary and secondary view controllers are displayed side-by-side onscreen, - // the selected group (if any) is highlighted. - [self refreshCurrentSelectedCell:YES]; - } -} - -#pragma mark - Override MXKGroupListViewController - -- (void)refreshGroupsTable -{ - // Refresh the tabBar icon badges - [[AppDelegate theDelegate].masterTabBarController refreshTabBarBadges]; - - isRefreshPending = NO; - - if (editedGroupId) - { - // Check whether the user didn't leave the room - if ([self.dataSource cellIndexPathWithGroupId:editedGroupId]) - { - isRefreshPending = YES; - return; - } - else - { - // Cancel the editing mode, a new refresh will be triggered. - [self cancelEditionMode:YES]; - return; - } - } - - [self.groupsTableView reloadData]; - - // Check conditions to display the fake search bar into the table header - if (_enableSearchBar && self.groupsSearchBar.isHidden && self.groupsTableView.tableHeaderView == nil) - { - // Add the search bar by hiding it by default. - self.groupsTableView.tableHeaderView = tableSearchBar; - self.groupsTableView.contentOffset = CGPointMake(0, self.groupsTableView.contentOffset.y + tableSearchBar.frame.size.height); - } - - if (_shouldScrollToTopOnRefresh) - { - [self scrollToTop:NO]; - _shouldScrollToTopOnRefresh = NO; - } - - // In case of split view controller where the primary and secondary view controllers are displayed side-by-side on screen, - // the selected group (if any) is updated. - if (!self.splitViewController.isCollapsed) - { - [self refreshCurrentSelectedCell:NO]; - } -} - -- (void)hideSearchBar:(BOOL)hidden -{ - [super hideSearchBar:hidden]; - - if (!hidden) - { - // Remove the fake table header view if any - self.groupsTableView.tableHeaderView = nil; - self.groupsTableView.contentInset = UIEdgeInsetsZero; - } -} - -#pragma mark - - -- (void)refreshCurrentSelectedCell:(BOOL)forceVisible -{ - // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. - NSIndexPath *currentSelectedCellIndexPath = nil; - MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; - if (masterTabBarController.selectedGroup) - { - // Look for the rank of this selected group in displayed groups - currentSelectedCellIndexPath = [self.dataSource cellIndexPathWithGroupId:masterTabBarController.selectedGroup.groupId]; - } - - if (currentSelectedCellIndexPath) - { - // Select the right row - [self.groupsTableView selectRowAtIndexPath:currentSelectedCellIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; - - if (forceVisible) - { - // Scroll table view to make the selected row appear at second position - NSInteger topCellIndexPathRow = currentSelectedCellIndexPath.row ? currentSelectedCellIndexPath.row - 1: currentSelectedCellIndexPath.row; - NSIndexPath* indexPath = [NSIndexPath indexPathForRow:topCellIndexPathRow inSection:currentSelectedCellIndexPath.section]; - [self.groupsTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO]; - } - } - else - { - NSIndexPath *indexPath = [self.groupsTableView indexPathForSelectedRow]; - if (indexPath) - { - [self.groupsTableView deselectRowAtIndexPath:indexPath animated:NO]; - } - } -} - -- (void)cancelEditionMode:(BOOL)forceRefresh -{ - if (self.groupsTableView.isEditing || self.isEditing) - { - // Leave editing mode first - isRefreshPending = forceRefresh; - [self setEditing:NO]; - } - else if (forceRefresh) - { - // Clean - editedGroupId = nil; - - [self refreshGroupsTable]; - } -} - -#pragma mark - MXKDataSourceDelegate - -- (Class)cellViewClassForCellData:(MXKCellData*)cellData -{ - id cellDataStoring = (id )cellData; - - if (cellDataStoring.group.membership != MXMembershipInvite) - { - return GroupTableViewCell.class; - } - else - { - return GroupInviteTableViewCell.class; - } -} - -- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData -{ - Class class = [self cellViewClassForCellData:cellData]; - - if ([class respondsToSelector:@selector(defaultReuseIdentifier)]) - { - return [class defaultReuseIdentifier]; - } - - return nil; -} - -- (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo -{ - // Handle here user actions on groups for Riot app - if ([actionIdentifier isEqualToString:kGroupInviteTableViewCellPreviewButtonPressed]) - { - // Retrieve the invited group - MXGroup *invitedGroup = userInfo[kGroupInviteTableViewCellRoomKey]; - - // Display the room preview - [[AppDelegate theDelegate].masterTabBarController selectGroup:invitedGroup inMatrixSession:self.mainSession]; - } - else if ([actionIdentifier isEqualToString:kGroupInviteTableViewCellDeclineButtonPressed]) - { - // Retrieve the invited group - MXGroup *invitedGroup = userInfo[kGroupInviteTableViewCellRoomKey]; - - NSIndexPath *indexPath = [self.dataSource cellIndexPathWithGroupId:invitedGroup.groupId]; - if (indexPath) - { - [self.dataSource leaveGroupAtIndexPath:indexPath]; - } - } - else - { - // Keep default implementation for other actions if any - if ([super respondsToSelector:@selector(cell:didRecognizeAction:userInfo:)]) - { - [super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo]; - } - } -} - -#pragma mark - UITableView delegate - -- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath; -{ - [super tableView:tableView willDisplayCell:cell forRowAtIndexPath:indexPath]; - - cell.backgroundColor = ThemeService.shared.theme.backgroundColor; - - // Update the selected background view - if (ThemeService.shared.theme.selectedBackgroundColor) - { - cell.selectedBackgroundView = [[UIView alloc] init]; - cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor; - } - else - { - if (tableView.style == UITableViewStylePlain) - { - cell.selectedBackgroundView = nil; - } - else - { - cell.selectedBackgroundView.backgroundColor = nil; - } - } -} - -- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section -{ - MXKTableViewHeaderFooterWithLabel *sectionHeader; - - if (tableView.numberOfSections > 1) - { - sectionHeader = [tableView dequeueReusableHeaderFooterViewWithIdentifier:MXKTableViewHeaderFooterWithLabel.defaultReuseIdentifier]; - sectionHeader.mxkContentView.backgroundColor = ThemeService.shared.theme.headerBackgroundColor; - sectionHeader.mxkLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - sectionHeader.mxkLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; - - NSString* title = [self.dataSource tableView:tableView titleForHeaderInSection:section]; - NSUInteger count = [self.dataSource tableView:tableView numberOfRowsInSection:section]; - if (count) - { - NSString *roomCount = [NSString stringWithFormat:@" %tu", count]; - NSMutableAttributedString *mutableSectionTitle = [[NSMutableAttributedString alloc] initWithString:title - attributes:@{NSForegroundColorAttributeName: ThemeService.shared.theme.headerTextPrimaryColor}]; - [mutableSectionTitle appendAttributedString:[[NSMutableAttributedString alloc] initWithString:roomCount - attributes:@{NSForegroundColorAttributeName: ThemeService.shared.theme.headerTextSecondaryColor}]]; - - sectionHeader.mxkLabel.attributedText = mutableSectionTitle; - } - else - { - sectionHeader.mxkLabel.text = title; - } - } - - return sectionHeader; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - UITableViewCell* cell = [self.groupsTableView cellForRowAtIndexPath:indexPath]; - - if ([cell isKindOfClass:[GroupInviteTableViewCell class]]) - { - // hide the selection - [tableView deselectRowAtIndexPath:indexPath animated:NO]; - } - else - { - [super tableView:tableView didSelectRowAtIndexPath:indexPath]; - } -} - -- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSMutableArray* actions; - - // add the swipe to delete only on joined group - if (indexPath.section == self.dataSource.joinedGroupsSection) - { - // Store the identifier of the room related to the edited cell. - id cellData = [self.dataSource cellDataAtIndex:indexPath]; - editedGroupId = cellData.group.groupId; - - actions = [[NSMutableArray alloc] init]; - - // Patch: Force the width of the button by adding whitespace characters into the title string. - UITableViewRowAction *leaveAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:@" " handler:^(UITableViewRowAction *action, NSIndexPath *indexPath){ - - [self.dataSource leaveGroupAtIndexPath:indexPath]; - - }]; - - leaveAction.backgroundColor = [MXKTools convertImageToPatternColor:@"remove_icon_blue" backgroundColor:ThemeService.shared.theme.headerBackgroundColor patternSize:CGSizeMake(74, 74) resourceSize:CGSizeMake(24, 24)]; - [actions insertObject:leaveAction atIndex:0]; - } - - return actions; -} - -- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath -{ - [self cancelEditionMode:isRefreshPending]; -} - -#pragma mark - UIScrollViewDelegate - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView -{ - [super scrollViewDidScroll:scrollView]; - - if (scrollView == self.groupsTableView) - { - if (!self.groupsSearchBar.isHidden) - { - if (!self.groupsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.adjustedContentInset.top > self.groupsSearchBar.frame.size.height)) - { - // Hide the search bar - [self hideSearchBar:YES]; - - // Refresh display - [self refreshGroupsTable]; - } - } - } -} - -#pragma mark - Room handling - -- (void)onPlusButtonPressed -{ - __weak typeof(self) weakSelf = self; - - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - currentAlert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert popoverPresentationController].sourceView = plusButtonImageView; - [currentAlert popoverPresentationController].sourceRect = plusButtonImageView.bounds; - - [currentAlert mxk_setAccessibilityIdentifier:@"GroupsVCCreateRoomAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; -} - -#pragma mark - Table view scrolling - -- (void)scrollToTop:(BOOL)animated -{ - [self.groupsTableView setContentOffset:CGPointMake(-self.groupsTableView.adjustedContentInset.left, -self.groupsTableView.adjustedContentInset.top) animated:animated]; -} - -#pragma mark - MXKGroupListViewControllerDelegate - -- (void)groupListViewController:(MXKGroupListViewController *)groupListViewController didSelectGroup:(MXGroup *)group inMatrixSession:(MXSession *)mxSession -{ - // Open the room - [[AppDelegate theDelegate].masterTabBarController selectGroup:group inMatrixSession:mxSession]; -} - -#pragma mark - UISearchBarDelegate - -- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar -{ - if (searchBar == tableSearchBar) - { - [self hideSearchBar:NO]; - [self.groupsSearchBar becomeFirstResponder]; - return NO; - } - - return YES; - -} - -- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar -{ - dispatch_async(dispatch_get_main_queue(), ^{ - - [self.groupsSearchBar setShowsCancelButton:YES animated:NO]; - - }); -} - -- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar -{ - [self.groupsSearchBar setShowsCancelButton:NO animated:NO]; -} - -#pragma mark - MasterTabBarItemDisplayProtocol - -- (NSString *)masterTabBarItemTitle -{ - return [VectorL10n titleGroups]; -} - -@end diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.h b/Riot/Modules/Communities/Home/GroupHomeViewController.h deleted file mode 100644 index 510e1dfcc..000000000 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MatrixKit.h" - -@interface GroupHomeViewController : MXKViewController - -@property (weak, nonatomic) IBOutlet UIView *mainHeaderContainer; -@property (weak, nonatomic) IBOutlet MXKImageView *groupAvatar; -@property (weak, nonatomic) IBOutlet UIView *groupAvatarMask; -@property (weak, nonatomic) IBOutlet UILabel *groupName; -@property (weak, nonatomic) IBOutlet UIView *groupNameMask; -@property (weak, nonatomic) IBOutlet UILabel *groupDescription; -@property (weak, nonatomic) IBOutlet UIView *countsContainer; -@property (weak, nonatomic) IBOutlet UIView *membersCountContainer; -@property (weak, nonatomic) IBOutlet UIView *roomsCountContainer; -@property (weak, nonatomic) IBOutlet UILabel *membersCountLabel; -@property (weak, nonatomic) IBOutlet UILabel *roomsCountLabel; - -@property (weak, nonatomic) IBOutlet UIView *inviteContainer; -@property (weak, nonatomic) IBOutlet UILabel *inviteLabel; -@property (weak, nonatomic) IBOutlet UIView *buttonsContainer; -@property (weak, nonatomic) IBOutlet UIButton *leftButton; -@property (weak, nonatomic) IBOutlet UIButton *rightButton; - -@property (weak, nonatomic) IBOutlet UIView *separatorView; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *separatorViewTopConstraint; - -@property (weak, nonatomic) IBOutlet UITextView *groupLongDescription; - -@property (strong, readonly, nonatomic) MXGroup *group; -@property (strong, readonly, nonatomic) MXSession *mxSession; - -/** - Returns the `UINib` object initialized for a `GroupHomeViewController`. - - @return The initialized `UINib` object or `nil` if there were errors during initialization - or the nib file could not be located. - */ -+ (UINib *)nib; - -/** - Creates and returns a new `GroupHomeViewController` object. - - @discussion This is the designated initializer for programmatic instantiation. - @return An initialized `GroupHomeViewController` object if successful, `nil` otherwise. - */ -+ (instancetype)groupHomeViewController; - -/** - Set the group for which the details are displayed. - Provide the related matrix session. - */ -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession; - -@end - diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.m b/Riot/Modules/Communities/Home/GroupHomeViewController.m deleted file mode 100644 index 1821eff53..000000000 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ /dev/null @@ -1,903 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - Copyright 2018 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupHomeViewController.h" - -#import "GeneratedInterface-Swift.h" - -#import "ThemeService.h" -#import "Tools.h" - -#import "MXGroup+Riot.h" - -#import "DTCoreText.h" - -@interface GroupHomeViewController () -{ - MXHTTPOperation *currentRequest; - - /** - The current visibility of the status bar in this view controller. - */ - BOOL isStatusBarHidden; - - // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. - id kThemeServiceDidChangeThemeNotificationObserver; - - // The options used to load long description html content. - NSDictionary *options; - NSString *groupLongDescriptionString; - - // The current pushed view controller - UIViewController *pushedViewController; -} - -@property (nonatomic, readonly) DTHTMLAttributedStringBuilderWillFlushCallback longDescriptionSanitizationCallback; - -@property (nonatomic) AnalyticsScreenTracker *screenTracker; - -@end - -@implementation GroupHomeViewController - -#pragma mark - Class methods - -+ (UINib *)nib -{ - return [UINib nibWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -+ (instancetype)groupHomeViewController -{ - return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -#pragma mark - - -- (void)finalizeInit -{ - [super finalizeInit]; - - // Setup `MXKViewControllerHandling` properties - self.enableBarTintColorStatusChange = NO; - self.rageShakeManager = [RageShakeManager sharedManager]; - - // Keep visible the status bar by default. - isStatusBarHidden = NO; - - // Set up sanitization for the long description - NSArray *allowedHTMLTags = @[ - @"font", // custom to matrix for IRC-style font coloring - @"del", // for markdown - @"body", // added internally by DTCoreText - @"h1", @"h2", @"h3", @"h4", @"h5", @"h6", @"blockquote", @"p", @"a", @"ul", @"ol", - @"nl", @"li", @"b", @"i", @"u", @"strong", @"em", @"strike", @"code", @"hr", @"br", @"div", - @"table", @"thead", @"caption", @"tbody", @"tr", @"th", @"td", @"pre", - @"img" - ]; - - MXWeakify(self); - _longDescriptionSanitizationCallback = ^(DTHTMLElement *element) { - MXStrongifyAndReturnIfNil(self); - [element sanitizeWith:allowedHTMLTags bodyFont:self->_groupLongDescription.font imageHandler:[self groupLongDescriptionImageHandler]]; - }; - - self.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenGroup]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - [self.leftButton setTitle:[VectorL10n decline] forState:UIControlStateNormal]; - [self.leftButton setTitle:[VectorL10n decline] forState:UIControlStateHighlighted]; - [self.rightButton setTitle:[VectorL10n join] forState:UIControlStateNormal]; - [self.rightButton setTitle:[VectorL10n join] forState:UIControlStateHighlighted]; - - UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; - [tap setNumberOfTouchesRequired:1]; - [tap setNumberOfTapsRequired:1]; - [tap setDelegate:self]; - [_groupNameMask addGestureRecognizer:tap]; - _groupNameMask.userInteractionEnabled = YES; - - // Add tap to show the group avatar in fullscreen - tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; - [tap setNumberOfTouchesRequired:1]; - [tap setNumberOfTapsRequired:1]; - [tap setDelegate:self]; - [_groupAvatarMask addGestureRecognizer:tap]; - _groupAvatarMask.userInteractionEnabled = YES; - - // Observe user interface theme change. - kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - [self userInterfaceThemeDidChange]; - - }]; - [self userInterfaceThemeDidChange]; -} - -- (void)userInterfaceThemeDidChange -{ - [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar]; - - self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor; - - self.view.backgroundColor = ThemeService.shared.theme.backgroundColor; - self.mainHeaderContainer.backgroundColor = ThemeService.shared.theme.headerBackgroundColor; - - _groupName.textColor = ThemeService.shared.theme.textPrimaryColor; - - _groupDescription.textColor = ThemeService.shared.theme.baseTextSecondaryColor; - _groupDescription.numberOfLines = 0; - - self.inviteLabel.textColor = ThemeService.shared.theme.baseTextSecondaryColor; - self.inviteLabel.numberOfLines = 0; - - self.separatorView.backgroundColor = ThemeService.shared.theme.lineBreakColor; - - [self.leftButton.layer setCornerRadius:5]; - self.leftButton.clipsToBounds = YES; - self.leftButton.backgroundColor = ThemeService.shared.theme.tintColor; - - [self.rightButton.layer setCornerRadius:5]; - self.rightButton.clipsToBounds = YES; - self.rightButton.backgroundColor = ThemeService.shared.theme.tintColor; - - if (_groupLongDescription) - { - _groupLongDescription.textColor = ThemeService.shared.theme.textSecondaryColor; - _groupLongDescription.tintColor = ThemeService.shared.theme.tintColor; - - // Update HTML loading options - NSUInteger bgColor = [MXKTools rgbValueWithColor:ThemeService.shared.theme.headerBackgroundColor]; - NSString *defaultCSS = [NSString stringWithFormat:@" \ - pre,code { \ - background-color: #%06lX; \ - display: inline; \ - font-family: monospace; \ - white-space: pre; \ - -coretext-fontname: Menlo-Regular; \ - font-size: small; \ - }", (unsigned long)bgColor]; - - // Apply the css style with some sanitisation. - options = @{ - DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView - DTDefaultFontFamily: _groupLongDescription.font.familyName, - DTDefaultFontName: _groupLongDescription.font.fontName, - DTDefaultFontSize: @(_groupLongDescription.font.pointSize), - DTDefaultTextColor: _groupLongDescription.textColor, - DTDefaultLinkDecoration: @(NO), - DTDefaultStyleSheet: [[DTCSSStylesheet alloc] initWithStyleBlock:defaultCSS], - DTWillFlushBlockCallBack: self.longDescriptionSanitizationCallback - }; - } - - [self setNeedsStatusBarAppearanceUpdate]; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return ThemeService.shared.theme.statusBarStyle; -} - -- (BOOL)prefersStatusBarHidden -{ - // Return the current status bar visibility. - return isStatusBarHidden; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self.screenTracker trackScreen]; - - // Release the potential pushed view controller - [self releasePushedViewController]; - - if (_group) - { - // Restore the listeners on the group update. - [self registerOnGroupChangeNotifications]; - - // Check whether the selected group is stored in the user's session, or if it is a group preview. - // Replace the displayed group instance with the one stored in the session (if any). - MXGroup *storedGroup = [_mxSession groupWithGroupId:_group.groupId]; - BOOL isPreview = (!storedGroup); - - // Force refresh - [self refreshDisplayWithGroup:(isPreview ? _group : storedGroup)]; - - // Prepare a block called on successful update in case of a group preview. - // Indeed the group update notifications are triggered by the matrix session only for the user's groups. - void (^success)(void) = ^void(void) - { - [self refreshDisplayWithGroup:self->_group]; - }; - - // Trigger a refresh on the group summary. - [self.mxSession updateGroupSummary:self->_group success:(isPreview ? success : nil) failure:^(NSError *error) { - - MXLogDebug(@"[GroupHomeViewController] viewWillAppear: group summary update failed %@", self->_group.groupId); - - }]; - // Trigger a refresh on the group members (ignore here the invited users). - [self.mxSession updateGroupUsers:self->_group success:(isPreview ? success : nil) failure:^(NSError *error) { - - MXLogDebug(@"[GroupHomeViewController] viewWillAppear: group members update failed %@", self->_group.groupId); - - }]; - // Trigger a refresh on the group rooms. - [self.mxSession updateGroupRooms:self->_group success:(isPreview ? success : nil) failure:^(NSError *error) { - - MXLogDebug(@"[GroupHomeViewController] viewWillAppear: group rooms update failed %@", self->_group.groupId); - - }]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - [self cancelRegistrationOnGroupChangeNotifications]; -} - -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - - // Scroll to the top the long group description. - _groupLongDescription.contentOffset = CGPointZero; -} - -- (void)destroy -{ - // Release the potential pushed view controller - [self releasePushedViewController]; - - // Note: all observers are removed during super call. - [super destroy]; - - _group = nil; - _mxSession = nil; - - [currentRequest cancel]; - currentRequest = nil; - - if (kThemeServiceDidChangeThemeNotificationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver]; - kThemeServiceDidChangeThemeNotificationObserver = nil; - } -} - -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession -{ - if (_mxSession != mxSession) - { - [self cancelRegistrationOnGroupChangeNotifications]; - _mxSession = mxSession; - - [self registerOnGroupChangeNotifications]; - } - - [self addMatrixSession:mxSession]; - - [self refreshDisplayWithGroup:group]; -} - -#pragma mark - - -- (void)pushViewController:(UIViewController*)viewController -{ - // Keep ref on pushed view controller - pushedViewController = viewController; - - // Check whether the view controller is displayed inside a segmented one. - if (self.parentViewController.navigationController) - { - // Hide back button title - [self.parentViewController vc_removeBackTitle]; - - [self.parentViewController.navigationController pushViewController:viewController animated:YES]; - } - else - { - // Hide back button title - [self vc_removeBackTitle]; - - [self.navigationController pushViewController:viewController animated:YES]; - } -} - -- (void)releasePushedViewController -{ - if (pushedViewController) - { - if ([pushedViewController isKindOfClass:[UINavigationController class]]) - { - UINavigationController *navigationController = (UINavigationController*)pushedViewController; - for (id subViewController in navigationController.viewControllers) - { - if ([subViewController respondsToSelector:@selector(destroy)]) - { - [subViewController destroy]; - } - } - } - else if ([pushedViewController respondsToSelector:@selector(destroy)]) - { - [(id)pushedViewController destroy]; - } - - pushedViewController = nil; - } -} - -- (void)registerOnGroupChangeNotifications -{ - if (_mxSession) - { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGroupDetails:) name:kMXSessionDidUpdateGroupSummaryNotification object:_mxSession]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGroupDetails:) name:kMXSessionDidUpdateGroupUsersNotification object:_mxSession]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGroupDetails:) name:kMXSessionDidUpdateGroupRoomsNotification object:_mxSession]; - } -} - -- (void)cancelRegistrationOnGroupChangeNotifications -{ - // Remove any pending observers - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdateGroupSummaryNotification object:_mxSession]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdateGroupUsersNotification object:_mxSession]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdateGroupRoomsNotification object:_mxSession]; -} - -- (void)didUpdateGroupDetails:(NSNotification *)notif -{ - MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey]; - if (group && [group.groupId isEqualToString:_group.groupId]) - { - // Update the current displayed group instance with the one stored in the session - [self refreshDisplayWithGroup:group]; - } -} - -- (void)refreshDisplayWithGroup:(MXGroup*)group -{ - _group = group; - - // Check whether the view controller has been loaded - if (!self.isViewLoaded) - { - return; - } - - if (_group) - { - [_group setGroupAvatarImageIn:_groupAvatar matrixSession:self.mxSession]; - - _groupName.text = _group.summary.profile.name; - if (!_groupName.text.length) - { - _groupName.text = _group.groupId; - } - - _groupDescription.text = _group.summary.profile.shortDescription; - - if (_group.users.totalUserCountEstimate == 1) - { - _membersCountLabel.text = [VectorL10n groupHomeOneMemberFormat]; - _membersCountContainer.hidden = NO; - } - else if (_group.users.totalUserCountEstimate > 1) - { - _membersCountLabel.text = [VectorL10n groupHomeMultiMembersFormat:_group.users.totalUserCountEstimate]; - _membersCountContainer.hidden = NO; - } - else - { - _membersCountLabel.text = nil; - _membersCountContainer.hidden = YES; - } - - if (_group.rooms.totalRoomCountEstimate == 1) - { - _roomsCountLabel.text = [VectorL10n groupHomeOneRoomFormat]; - _roomsCountContainer.hidden = NO; - } - else if (_group.rooms.totalRoomCountEstimate > 1) - { - _roomsCountLabel.text = [VectorL10n groupHomeMultiRoomsFormat:_group.rooms.totalRoomCountEstimate]; - _roomsCountContainer.hidden = NO; - } - else - { - _roomsCountLabel.text = nil; - _roomsCountContainer.hidden = YES; - } - - _countsContainer.hidden = (_membersCountContainer.isHidden && _roomsCountContainer.isHidden); - - if (_group.membership == MXMembershipInvite) - { - self.inviteContainer.hidden = NO; - - if (_group.inviter) - { - NSString *inviter = _group.inviter; - - if ([MXTools isMatrixUserIdentifier:inviter]) - { - // Get the user that corresponds to this member - MXUser *user = [self.mxSession userWithUserId:inviter]; - if (user.displayname.length) - { - inviter = user.displayname; - } - } - - self.inviteLabel.text = [VectorL10n groupInvitationFormat:inviter]; - } - else - { - self.inviteLabel.text = nil; - } - - [self.inviteContainer layoutIfNeeded]; - - if (_separatorViewTopConstraint.constant != self.inviteContainer.frame.size.height) - { - _separatorViewTopConstraint.constant = self.inviteContainer.frame.size.height; - [self.view setNeedsLayout]; - } - } - else - { - self.inviteContainer.hidden = YES; - if (_separatorViewTopConstraint.constant != 0) - { - _separatorViewTopConstraint.constant = 0; - [self.view setNeedsLayout]; - } - } - - [self refreshGroupLongDescription]; - } - else - { - _groupAvatar.image = nil; - - _groupName.text = nil; - _groupDescription.text = nil; - - self.inviteLabel.text = nil; - _groupLongDescription.text = nil; - - self.inviteContainer.hidden = YES; - - _separatorViewTopConstraint.constant = 0; - - _membersCountLabel.text = nil; - _roomsCountLabel.text = nil; - _countsContainer.hidden = YES; - } - - // Round image view for thumbnail - _groupAvatar.layer.cornerRadius = _groupAvatar.frame.size.width / 2; - _groupAvatar.clipsToBounds = YES; - - _groupAvatar.defaultBackgroundColor = ThemeService.shared.theme.headerBackgroundColor; -} - -- (void)refreshGroupLongDescription -{ - if (_group.summary.profile.longDescription.length) - { - groupLongDescriptionString = _group.summary.profile.longDescription; - } - else - { - groupLongDescriptionString = nil; - } - - [self renderGroupLongDescription]; -} - -- (void)renderGroupLongDescription -{ - if (groupLongDescriptionString) - { - // Using DTCoreText, which renders static string, helps to avoid code injection attacks - // that could happen with the default HTML renderer of NSAttributedString which is a - // webview. - // The supplied options include a callback to sanitize html tags and load image data. - NSAttributedString *attributedString = [[NSAttributedString alloc] initWithHTMLData:[groupLongDescriptionString dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL]; - - // Apply additional treatments - NSInteger mxIdsBitMask = (MXKTOOLS_USER_IDENTIFIER_BITWISE | MXKTOOLS_ROOM_IDENTIFIER_BITWISE | MXKTOOLS_ROOM_ALIAS_BITWISE | MXKTOOLS_EVENT_IDENTIFIER_BITWISE | MXKTOOLS_GROUP_IDENTIFIER_BITWISE); - - NSMutableAttributedString *mutableStr = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString]; - [MXKTools createLinksInMutableAttributedString:mutableStr forEnabledMatrixIds:mxIdsBitMask]; - [MXKTools removeDTCoreTextArtifacts:mutableStr]; - - // Finalize the attributed string by removing DTCoreText artifacts (Trim trailing newlines, replace DTImageTextAttachments...) - _groupLongDescription.attributedText = mutableStr; - _groupLongDescription.contentOffset = CGPointZero; - } - else - { - _groupLongDescription.text = nil; - } -} - -- (NSURL *(^)(NSString *sourceURL, CGFloat width, CGFloat height))groupLongDescriptionImageHandler -{ - MXWeakify(self); - return ^NSURL *(NSString *sourceURL, CGFloat width, CGFloat height) { - - MXStrongifyAndReturnValueIfNil(self, nil); - NSURL *localSourceURL; - - if (width != -1 && height != -1) - { - CGSize size = CGSizeMake(width, height); - // Build the cache path for the a thumbnail of this image. - NSString *cacheFilePath = [MXMediaManager thumbnailCachePathForMatrixContentURI:sourceURL - andType:nil - inFolder:kMXMediaManagerDefaultCacheFolder - toFitViewSize:size - withMethod:MXThumbnailingMethodScale]; - // Check whether the provided URL is a valid Matrix Content URI. - if (cacheFilePath) - { - // Download the thumbnail if it is not already stored in the cache. - if (![[NSFileManager defaultManager] fileExistsAtPath:cacheFilePath]) - { - MXWeakify(self); - [self.mxSession.mediaManager downloadThumbnailFromMatrixContentURI:sourceURL - withType:nil - inFolder:kMXMediaManagerDefaultCacheFolder - toFitViewSize:size - withMethod:MXThumbnailingMethodScale - success:^(NSString *outputFilePath) { - MXStrongifyAndReturnIfNil(self); - [self refreshGroupLongDescription]; - } - failure:nil]; - } - else - { - // Update the local url - localSourceURL = [NSURL fileURLWithPath:cacheFilePath]; - } - } - } - else - { - // Build the cache path for this image. - NSString* cacheFilePath = [MXMediaManager cachePathForMatrixContentURI:sourceURL - andType:nil - inFolder:kMXMediaManagerDefaultCacheFolder]; - - // Check whether the provided URL is a valid Matrix Content URI. - if (cacheFilePath) - { - // Download the image if it is not already stored in the cache. - if (![[NSFileManager defaultManager] fileExistsAtPath:cacheFilePath]) - { - MXWeakify(self); - [self.mxSession.mediaManager downloadMediaFromMatrixContentURI:sourceURL - withType:nil - inFolder:kMXMediaManagerDefaultCacheFolder - success:^(NSString *outputFilePath) { - MXStrongifyAndReturnIfNil(self); - [self refreshGroupLongDescription]; - } - failure:nil]; - } - else - { - // Update the local path - localSourceURL = [NSURL fileURLWithPath:cacheFilePath]; - } - } - } - return localSourceURL; - - }; -} - -- (void)didSelectRoomId:(NSString*)roomId -{ - // Check first if the user already joined this room. - if ([self.mxSession roomWithRoomId:roomId]) - { - MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mxSession]; - [roomDataSourceManager roomDataSourceForRoom:roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) { - // Open this room - RoomViewController *roomViewController = [RoomViewController roomViewController]; - roomViewController.showMissedDiscussionsBadge = NO; - [roomViewController displayRoom:roomDataSource]; - [self pushViewController:roomViewController]; - }]; - } - else - { - // Prepare a preview - RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomId andSession:self.mxSession]; - __weak typeof(self) weakSelf = self; - [self startActivityIndicator]; - - // Try to get more information about the room before opening its preview - [roomPreviewData peekInRoom:^(BOOL succeeded) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - - // Display the room preview - RoomViewController *roomViewController = [RoomViewController roomViewController]; - roomViewController.showMissedDiscussionsBadge = NO; - [roomViewController displayRoomPreview:roomPreviewData]; - [self pushViewController:roomViewController]; - } - - }]; - } -} - -#pragma mark - Action - -- (IBAction)onButtonPressed:(id)sender -{ - if (!currentRequest) - { - if (sender == self.rightButton) - { - // Accept the invite - __weak typeof(self) weakSelf = self; - [self startActivityIndicator]; - - currentRequest = [self.mxSession acceptGroupInvite:_group.groupId success:^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentRequest = nil; - [self stopActivityIndicator]; - - [self refreshDisplayWithGroup:[self->_mxSession groupWithGroupId:self->_group.groupId]]; - } - - } failure:^(NSError *error) { - - MXLogDebug(@"[GroupDetailsViewController] join group (%@) failed", self->_group.groupId); - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentRequest = nil; - [self stopActivityIndicator]; - } - - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - } - else if (sender == self.leftButton) - { - // Decline the invite - __weak typeof(self) weakSelf = self; - [self startActivityIndicator]; - - currentRequest = [self.mxSession leaveGroup:_group.groupId success:^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentRequest = nil; - [self stopActivityIndicator]; - - [self withdrawViewControllerAnimated:YES completion:nil]; - } - - } failure:^(NSError *error) { - - MXLogDebug(@"[GroupDetailsViewController] leave group (%@) failed", self->_group.groupId); - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentRequest = nil; - [self stopActivityIndicator]; - } - - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - } - } -} - -- (void)handleTapGesture:(UITapGestureRecognizer*)tapGestureRecognizer -{ - UIView *view = tapGestureRecognizer.view; - - if (view == _groupNameMask && _group.summary.profile.name) - { - if ([_groupName.text isEqualToString:_group.summary.profile.name]) - { - // Display group's matrix id - _groupName.text = _group.groupId; - } - else - { - // Restore display name - _groupName.text = _group.summary.profile.name; - } - } - else if (view == _groupAvatarMask) - { - // Show the avatar in full screen - __block MXKImageView * avatarFullScreenView = [[MXKImageView alloc] initWithFrame:CGRectZero]; - avatarFullScreenView.stretchable = YES; - - MXWeakify(self); - [avatarFullScreenView setRightButtonTitle:[VectorL10n ok] handler:^(MXKImageView* imageView, NSString* buttonTitle) { - - MXStrongifyAndReturnIfNil(self); - [avatarFullScreenView dismissSelection]; - [avatarFullScreenView removeFromSuperview]; - - avatarFullScreenView = nil; - - self->isStatusBarHidden = NO; - // Trigger status bar update - [self setNeedsStatusBarAppearanceUpdate]; - }]; - - [avatarFullScreenView setImageURI:_group.summary.profile.avatarUrl - withType:nil - andImageOrientation:UIImageOrientationUp - previewImage:self.groupAvatar.image - mediaManager:_mxSession.mediaManager]; - - [avatarFullScreenView showFullScreen]; - isStatusBarHidden = YES; - - // Trigger status bar update - [self setNeedsStatusBarAppearanceUpdate]; - } -} - -#pragma mark - UITextView delegate - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange -{ - BOOL shouldInteractWithURL = YES; - // Try to catch universal link supported by the app - - // When a link refers to a room alias/id, a user id or an event id, the non-ASCII characters (like '#' in room alias) has been escaped - // to be able to convert it into a legal URL string. - NSString *absoluteURLString = [URL.absoluteString stringByRemovingPercentEncoding]; - - // If the link can be open it by the app, let it do - if ([Tools isUniversalLink:URL]) - { - shouldInteractWithURL = NO; - - [[AppDelegate theDelegate] handleUniversalLinkURL:URL]; - } - // Open a detail screen about the clicked user - else if ([MXTools isMatrixUserIdentifier:absoluteURLString]) - { - shouldInteractWithURL = NO; - - NSString *userId = absoluteURLString; - MXKContact *contact; - // Use the contact detail VC for other users - MXUser *user = [self.mxSession userWithUserId:userId]; - if (user) - { - contact = [[MXKContact alloc] initMatrixContactWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) andMatrixID:user.userId]; - } - else - { - contact = [[MXKContact alloc] initMatrixContactWithDisplayName:userId andMatrixID:userId]; - } - - ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController instantiate]; - contactDetailsViewController.enableVoipCall = NO; - contactDetailsViewController.contact = contact; - - [self pushViewController:contactDetailsViewController]; - } - // Open the clicked room - else if ([MXTools isMatrixRoomIdentifier:absoluteURLString] || [MXTools isMatrixRoomAlias:absoluteURLString]) - { - shouldInteractWithURL = NO; - - NSString *roomIdOrAlias = absoluteURLString; - NSString *roomId; - - if ([roomIdOrAlias hasPrefix:@"#"]) - { - // Check whether the room alias can be translated locally into the room id. - MXRoom *room = [self.mxSession roomWithAlias:roomIdOrAlias]; - if (room) - { - roomId = room.roomId; - } - } - else - { - roomId = roomIdOrAlias; - } - - if (roomId) - { - [self didSelectRoomId:roomId]; - } - else - { - // The alias may be not part of user's rooms states - // Ask the HS to resolve the room alias into a room id and then retry - __weak typeof(self) weakSelf = self; - [self startActivityIndicator]; - - [self.mxSession.matrixRestClient resolveRoomAlias:roomIdOrAlias success:^(MXRoomAliasResolution *resolution) { - if (roomId && weakSelf) - { - typeof(self) self = weakSelf; - - [self stopActivityIndicator]; - [self didSelectRoomId:resolution.roomId]; - } - - } failure:^(NSError *error) { - MXLogDebug(@"[GroupHomeViewController] Error: The homeserver failed to resolve the room alias (%@)", roomIdOrAlias); - }]; - } - } - // Preview the clicked group - else if ([MXTools isMatrixGroupIdentifier:absoluteURLString]) - { - shouldInteractWithURL = NO; - - // Open the group or preview it - NSString *fragment = [NSString stringWithFormat:@"/group/%@", - [MXTools encodeURIComponent:absoluteURLString]]; - UniversalLink *link = [[UniversalLink alloc] initWithUrl:URL]; - [[AppDelegate theDelegate] handleUniversalLinkFragment:fragment fromLink:link]; - } - - return shouldInteractWithURL; -} -#pragma clang diagnostic pop - -@end diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.xib b/Riot/Modules/Communities/Home/GroupHomeViewController.xib deleted file mode 100644 index 4b9a27b42..000000000 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.xib +++ /dev/null @@ -1,285 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Communities/Members/GroupParticipantsViewController.h b/Riot/Modules/Communities/Members/GroupParticipantsViewController.h deleted file mode 100644 index e5b4a9ba8..000000000 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.h +++ /dev/null @@ -1,83 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "ContactsTableViewController.h" -#import "MatrixKit.h" - -@class Contact; - -/** - 'GroupParticipantsViewController' instance is used to list members of the group defined by the property 'mxGroup'. - When this property is nil, the view controller is empty. - */ -@interface GroupParticipantsViewController : MXKViewController -{ -@protected - /** - Section indexes - */ - NSInteger participantsSection; - NSInteger invitedSection; - - /** - The current list of joined members. - */ - NSMutableArray *actualParticipants; - - /** - The current list of invited members. - */ - NSMutableArray *invitedParticipants; -} - -@property (weak, nonatomic) IBOutlet UITableView *tableView; -@property (weak, nonatomic) IBOutlet UIView *searchBarHeader; -@property (weak, nonatomic) IBOutlet UISearchBar *searchBarView; -@property (weak, nonatomic) IBOutlet UIView *searchBarHeaderBorder; - -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *searchBarTopConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *tableViewBottomConstraint; - -/** - A matrix group (nil by default). - */ -@property (strong, readonly, nonatomic) MXGroup *group; -@property (strong, readonly, nonatomic) MXSession *mxSession; - -/** - Returns the `UINib` object initialized for a `GroupParticipantsViewController`. - - @return The initialized `UINib` object or `nil` if there were errors during initialization - or the nib file could not be located. - */ -+ (UINib *)nib; - -/** - Creates and returns a new `GroupParticipantsViewController` object. - - @discussion This is the designated initializer for programmatic instantiation. - @return An initialized `GroupParticipantsViewController` object if successful, `nil` otherwise. - */ -+ (instancetype)groupParticipantsViewController; - -/** - Set the group for which the details are displayed. - Provide the related matrix session. - */ -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession; - -@end - diff --git a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m deleted file mode 100644 index fc05a9d12..000000000 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m +++ /dev/null @@ -1,1324 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupParticipantsViewController.h" - -#import "GeneratedInterface-Swift.h" - -#import "Contact.h" -#import "ContactTableViewCell.h" - -#import "RageShakeManager.h" - -@interface GroupParticipantsViewController () -{ - // Search result - NSString *currentSearchText; - NSMutableArray *filteredActualParticipants; - NSMutableArray *filteredInvitedParticipants; - - // Mask view while processing a request - UIActivityIndicatorView *pendingMaskSpinnerView; - - // The current pushed view controller - UIViewController *pushedViewController; - - // Display a gradient view above the screen. - CAGradientLayer* tableViewMaskLayer; - - // Display a button to invite new member. - UIImageView* addParticipantButtonImageView; - NSLayoutConstraint *addParticipantButtonImageViewBottomConstraint; - - UIAlertController *currentAlert; - - // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. - id kThemeServiceDidChangeThemeNotificationObserver; -} - -@end - -@implementation GroupParticipantsViewController - -#pragma mark - Class methods - -+ (UINib *)nib -{ - return [UINib nibWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -+ (instancetype)groupParticipantsViewController -{ - return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -#pragma mark - - -- (void)finalizeInit -{ - [super finalizeInit]; - - // Setup `MXKViewControllerHandling` properties - self.enableBarTintColorStatusChange = NO; - self.rageShakeManager = [RageShakeManager sharedManager]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - // Do any additional setup after loading the view, typically from a nib. - - // Check whether the view controller has been pushed via storyboard - if (!self.tableView) - { - // Instantiate view controller objects - [[[self class] nib] instantiateWithOwner:self options:nil]; - } - - // Adjust Top and Bottom constraints to take into account potential navBar and tabBar. - [NSLayoutConstraint deactivateConstraints:@[_searchBarTopConstraint, _tableViewBottomConstraint]]; - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated" - _searchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.searchBarHeader - attribute:NSLayoutAttributeTop - multiplier:1.0f - constant:0.0f]; - - _tableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.tableView - attribute:NSLayoutAttributeBottom - multiplier:1.0f - constant:0.0f]; - #pragma clang diagnostic pop - - [NSLayoutConstraint activateConstraints:@[_searchBarTopConstraint, _tableViewBottomConstraint]]; - - _searchBarView.placeholder = [VectorL10n groupParticipantsFilterMembers]; - _searchBarView.returnKeyType = UIReturnKeyDone; - _searchBarView.autocapitalizationType = UITextAutocapitalizationTypeNone; - - // Search bar header is hidden when no group is provided - _searchBarHeader.hidden = (self.group == nil); - - // Enable self-sizing cells and section headers. - self.tableView.rowHeight = UITableViewAutomaticDimension; - self.tableView.estimatedRowHeight = 74; - self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension; - self.tableView.estimatedSectionHeaderHeight = 30; - - // Hide line separators of empty cells - self.tableView.tableFooterView = [[UIView alloc] init]; - - [self.tableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"]; - [self.tableView registerNib:MXKTableViewHeaderFooterWithLabel.nib forHeaderFooterViewReuseIdentifier:MXKTableViewHeaderFooterWithLabel.defaultReuseIdentifier]; - - // @TODO: Add programmatically the button to add participant. - //[self addAddParticipantButton]; - - // Observe user interface theme change. - kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - [self userInterfaceThemeDidChange]; - - }]; - [self userInterfaceThemeDidChange]; -} - -- (void)userInterfaceThemeDidChange -{ - [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar]; - - self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor; - - [self refreshSearchBarItemsColor:_searchBarView]; - - _searchBarHeaderBorder.backgroundColor = ThemeService.shared.theme.headerBorderColor; - - // Check the table view style to select its bg color. - self.tableView.backgroundColor = ((self.tableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.backgroundColor : ThemeService.shared.theme.headerBackgroundColor); - self.view.backgroundColor = self.tableView.backgroundColor; - self.tableView.separatorColor = ThemeService.shared.theme.lineBreakColor; - - // Update the gradient view above the screen - CGFloat white = 1.0; - [ThemeService.shared.theme.backgroundColor getWhite:&white alpha:nil]; - CGColorRef opaqueWhiteColor = [UIColor colorWithWhite:white alpha:1.0].CGColor; - CGColorRef transparentWhiteColor = [UIColor colorWithWhite:white alpha:0].CGColor; - tableViewMaskLayer.colors = @[(__bridge id) transparentWhiteColor, (__bridge id) transparentWhiteColor, (__bridge id) opaqueWhiteColor]; - - if (self.tableView.dataSource) - { - [self.tableView reloadData]; - } - - [self setNeedsStatusBarAppearanceUpdate]; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return ThemeService.shared.theme.statusBarStyle; -} - -// This method is called when the viewcontroller is added or removed from a container view controller. -- (void)didMoveToParentViewController:(nullable UIViewController *)parent -{ - [super didMoveToParentViewController:parent]; -} - -- (void)destroy -{ - // Release the potential pushed view controller - [self releasePushedViewController]; - - if (kThemeServiceDidChangeThemeNotificationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver]; - kThemeServiceDidChangeThemeNotificationObserver = nil; - } - - if (currentAlert) - { - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - currentAlert = nil; - } - - _group = nil; - _mxSession = nil; - - filteredActualParticipants = nil; - filteredInvitedParticipants = nil; - - actualParticipants = nil; - invitedParticipants = nil; - - [self removePendingActionMask]; - - // Note: all observers are removed during super call. - [super destroy]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Release the potential pushed view controller - [self releasePushedViewController]; - - if (_group) - { - // Restore the listeners on the group update. - [self registerOnGroupChangeNotifications]; - - // Check whether the selected group is stored in the user's session, or if it is a group preview. - // Replace the displayed group instance with the one stored in the session (if any). - MXGroup *storedGroup = [_mxSession groupWithGroupId:_group.groupId]; - BOOL isPreview = (!storedGroup); - - // Force refresh - [self refreshDisplayWithGroup:(isPreview ? _group : storedGroup)]; - - // Prepare a block called on successful update in case of a group preview. - // Indeed the group update notifications are triggered by the matrix session only for the user's groups. - void (^success)(void) = ^void(void) - { - [self refreshDisplayWithGroup:self->_group]; - }; - - // Trigger a refresh on the group members and the invited users. - [self.mxSession updateGroupUsers:_group success:(isPreview ? success : nil) failure:^(NSError *error) { - - MXLogDebug(@"[GroupParticipantsViewController] viewWillAppear: group members update failed %@", self->_group.groupId); - - }]; - [self.mxSession updateGroupInvitedUsers:_group success:(isPreview ? success : nil) failure:^(NSError *error) { - - MXLogDebug(@"[GroupParticipantsViewController] viewWillAppear: invited users update failed %@", self->_group.groupId); - - }]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - [self cancelRegistrationOnGroupChangeNotifications]; - - if (currentAlert) - { - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - currentAlert = nil; - } - - // cancel any pending search - [self searchBarCancelButtonClicked:_searchBarView]; -} - -- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion -{ - // Check whether the current view controller is displayed inside a segmented view controller in order to withdraw the right item - if (self.parentViewController && [self.parentViewController isKindOfClass:SegmentedViewController.class]) - { - [((SegmentedViewController*)self.parentViewController) withdrawViewControllerAnimated:animated completion:completion]; - } - else - { - [super withdrawViewControllerAnimated:animated completion:completion]; - } -} - -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - - // Sanity check - if (tableViewMaskLayer) - { - CGRect currentBounds = tableViewMaskLayer.bounds; - CGRect newBounds = CGRectIntegral(self.view.frame); - - newBounds.size.height -= self.keyboardHeight; - - // Check if there is an update - if (!CGSizeEqualToSize(currentBounds.size, newBounds.size)) - { - newBounds.origin = CGPointZero; - - [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn - animations:^{ - - self->tableViewMaskLayer.bounds = newBounds; - - } - completion:^(BOOL finished){ - }]; - - } - - // Hide the addParticipants button on landscape when keyboard is visible - BOOL isLandscapeOriented = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation); - addParticipantButtonImageView.hidden = tableViewMaskLayer.hidden = (isLandscapeOriented && self.keyboardHeight); - } -} - -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession -{ - // Cancel any pending search - [self searchBarCancelButtonClicked:_searchBarView]; - - if (_mxSession != mxSession) - { - [self cancelRegistrationOnGroupChangeNotifications]; - _mxSession = mxSession; - - [self registerOnGroupChangeNotifications]; - } - - [self addMatrixSession:mxSession]; - - [self refreshDisplayWithGroup:group]; -} - -#pragma mark - - -- (void)registerOnGroupChangeNotifications -{ - if (_mxSession) - { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGroupUsers:) name:kMXSessionDidUpdateGroupUsersNotification object:_mxSession]; - } -} - -- (void)cancelRegistrationOnGroupChangeNotifications -{ - // Remove any pending observers - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdateGroupUsersNotification object:_mxSession]; -} - -- (void)didUpdateGroupUsers:(NSNotification *)notif -{ - MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey]; - if (group && [group.groupId isEqualToString:_group.groupId]) - { - // Update the current displayed group instance with the one stored in the session - [self refreshDisplayWithGroup:group]; - } -} - -- (void)refreshDisplayWithGroup:(MXGroup *)group -{ - _group = group; - - if (_group) - { - _searchBarHeader.hidden = NO; - } - else - { - // Search bar header is hidden when no group is provided - _searchBarHeader.hidden = YES; - } - - // Refresh the members list. - [self refreshParticipantsList]; -} - -- (void)startActivityIndicator -{ - // Check whether the current view controller is displayed inside a segmented view controller in order to run the right activity view - if (self.parentViewController && [self.parentViewController isKindOfClass:SegmentedViewController.class]) - { - [((SegmentedViewController*)self.parentViewController) startActivityIndicator]; - - // Force stop the activity view of the view controller - [self.activityIndicator stopAnimating]; - } - else - { - [super startActivityIndicator]; - } -} - -- (void)stopActivityIndicator -{ - // Check whether the current view controller is displayed inside a segmented view controller in order to stop the right activity view - if (self.parentViewController && [self.parentViewController isKindOfClass:SegmentedViewController.class]) - { - [((SegmentedViewController*)self.parentViewController) stopActivityIndicator]; - - // Force stop the activity view of the view controller - [self.activityIndicator stopAnimating]; - } - else - { - [super stopActivityIndicator]; - } -} - -- (void)setKeyboardHeight:(CGFloat)keyboardHeight -{ - super.keyboardHeight = keyboardHeight; - - // Update addParticipants button position with animation - [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn - animations:^{ - - self->addParticipantButtonImageViewBottomConstraint.constant = keyboardHeight + 9; - - // Force to render the view - [self.view layoutIfNeeded]; - - } - completion:^(BOOL finished){ - }]; -} - -#pragma mark - Internals - -- (void)refreshTableView -{ - [self.tableView reloadData]; -} - -- (void)addAddParticipantButton -{ - // Add blur mask programmatically - tableViewMaskLayer = [CAGradientLayer layer]; - - // Consider the grayscale components of the ThemeService.shared.theme.backgroundColor. - CGFloat white = 1.0; - [ThemeService.shared.theme.backgroundColor getWhite:&white alpha:nil]; - - CGColorRef opaqueWhiteColor = [UIColor colorWithWhite:white alpha:1.0].CGColor; - CGColorRef transparentWhiteColor = [UIColor colorWithWhite:white alpha:0].CGColor; - - tableViewMaskLayer.colors = @[(__bridge id) transparentWhiteColor, (__bridge id) transparentWhiteColor, (__bridge id) opaqueWhiteColor]; - - // display a gradient to the rencents bottom (20% of the bottom of the screen) - tableViewMaskLayer.locations = @[@0.0F, - @0.85F, - @1.0F]; - - tableViewMaskLayer.bounds = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); - tableViewMaskLayer.anchorPoint = CGPointZero; - - // CAConstraint is not supported on IOS. - // it seems only being supported on Mac OS. - // so viewDidLayoutSubviews will refresh the layout bounds. - [self.view.layer addSublayer:tableViewMaskLayer]; - - // Add + button - addParticipantButtonImageView = [[UIImageView alloc] init]; - [addParticipantButtonImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.view addSubview:addParticipantButtonImageView]; - - addParticipantButtonImageView.backgroundColor = [UIColor clearColor]; - addParticipantButtonImageView.contentMode = UIViewContentModeCenter; - addParticipantButtonImageView.image = AssetImages.addGroupParticipant.image; - - CGFloat side = 78.0f; - NSLayoutConstraint* widthConstraint = [NSLayoutConstraint constraintWithItem:addParticipantButtonImageView - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:side]; - - NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:addParticipantButtonImageView - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:side]; - - NSLayoutConstraint* centerXConstraint = [NSLayoutConstraint constraintWithItem:addParticipantButtonImageView - attribute:NSLayoutAttributeCenterX - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeCenterX - multiplier:1 - constant:0]; - - addParticipantButtonImageViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.view - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:addParticipantButtonImageView - attribute:NSLayoutAttributeBottom - multiplier:1 - constant:self.keyboardHeight + 9]; - - // Available on iOS 8 and later - [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, centerXConstraint, addParticipantButtonImageViewBottomConstraint]]; - - addParticipantButtonImageView.userInteractionEnabled = YES; - - // Handle tap gesture - UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onAddParticipantButtonPressed)]; - [tap setNumberOfTouchesRequired:1]; - [tap setNumberOfTapsRequired:1]; - [tap setDelegate:self]; - [addParticipantButtonImageView addGestureRecognizer:tap]; -} - -- (void)onAddParticipantButtonPressed -{ - // Push the contacts picker. - ContactsTableViewController *contactsPickerViewController = [ContactsTableViewController contactsTableViewController]; - - // Set delegate to handle action on member (start chat, mention) - contactsPickerViewController.contactsTableViewControllerDelegate = self; - - // Prepare its data source - ContactsDataSource *contactsDataSource = [[ContactsDataSource alloc] initWithMatrixSession:self.mxSession]; - contactsDataSource.areSectionsShrinkable = YES; - contactsDataSource.displaySearchInputInContactsList = YES; - contactsDataSource.forceMatrixIdInDisplayName = YES; - // Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user. - contactsDataSource.contactCellAccessoryImage = [AssetImages.plusIcon.image vc_tintedImageUsingColor:ThemeService.shared.theme.textPrimaryColor]; - - // List all the participants matrix user id to ignore them during the contacts search. - for (Contact *contact in actualParticipants) - { - contactsDataSource.ignoredContactsByMatrixId[contact.mxGroupUser.userId] = contact; - } - for (Contact *contact in invitedParticipants) - { - contactsDataSource.ignoredContactsByMatrixId[contact.mxGroupUser.userId] = contact; - } - - [contactsPickerViewController showSearch:YES]; - contactsPickerViewController.searchBar.placeholder = [VectorL10n groupParticipantsInviteAnotherUser]; - - // Apply the search pattern if any - if (currentSearchText) - { - contactsPickerViewController.searchBar.text = currentSearchText; - [contactsDataSource searchWithPattern:currentSearchText forceReset:YES]; - } - - [contactsPickerViewController displayList:contactsDataSource]; - - [self pushViewController:contactsPickerViewController]; -} - -- (void)refreshParticipantsList -{ - if (_group) - { - actualParticipants = [[NSMutableArray alloc] initWithCapacity:_group.users.chunk.count]; - for (MXGroupUser *groupUser in _group.users.chunk) - { - Contact *contact = [[Contact alloc] initMatrixContactWithDisplayName:groupUser.displayname andMatrixID:groupUser.userId]; - contact.mxGroupUser = groupUser; - - [actualParticipants addObject:contact]; - } - - invitedParticipants = [[NSMutableArray alloc] initWithCapacity:_group.invitedUsers.chunk.count]; - for (MXGroupUser *groupUser in _group.invitedUsers.chunk) - { - Contact *contact = [[Contact alloc] initMatrixContactWithDisplayName:groupUser.displayname andMatrixID:groupUser.userId]; - contact.mxGroupUser = groupUser; - - [invitedParticipants addObject:contact]; - } - } - else - { - actualParticipants = nil; - invitedParticipants = nil; - } - - [self finalizeParticipantsList]; -} - -- (void)finalizeParticipantsList -{ - // Sort group participants by power and then alphabetically. - NSComparator comparator = ^NSComparisonResult(Contact *userA, Contact *userB) { - - if (userA.mxGroupUser.isPrivileged && userB.mxGroupUser.isPrivileged) - { - return [userA.mxGroupUser.displayname compare:userB.mxGroupUser.displayname options:NSCaseInsensitiveSearch]; - } - if (userA.mxGroupUser.isPrivileged) - { - return NSOrderedAscending; - } - if (userB.mxGroupUser.isPrivileged) - { - return NSOrderedDescending; - } - - return [userA.mxGroupUser.displayname compare:userB.mxGroupUser.displayname options:NSCaseInsensitiveSearch]; - }; - - // Sort each participants list in alphabetical order - [actualParticipants sortUsingComparator:comparator]; - [invitedParticipants sortUsingComparator:comparator]; - - // Reload search result if any - if (currentSearchText.length) - { - NSString *searchText = currentSearchText; - currentSearchText = nil; - - [self searchBar:_searchBarView textDidChange:searchText]; - } - else - { - [self refreshTableView]; - } -} - -- (void)addPendingActionMask -{ - // Remove potential existing mask - [self removePendingActionMask]; - - // Add a spinner above the tableview to avoid that the user tap on any other button - pendingMaskSpinnerView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; - pendingMaskSpinnerView.backgroundColor = [UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:0.5]; - pendingMaskSpinnerView.frame = self.tableView.frame; - pendingMaskSpinnerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin; - - // append it - [self.tableView.superview addSubview:pendingMaskSpinnerView]; - - // animate it - [pendingMaskSpinnerView startAnimating]; - - // Show the spinner after a delay so that if it is removed in a short future, - // it is not displayed to the end user. - pendingMaskSpinnerView.alpha = 0; - [UIView animateWithDuration:0.3 delay:0.3 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ - - self->pendingMaskSpinnerView.alpha = 1; - - } completion:^(BOOL finished) { - }]; -} - -- (void)removePendingActionMask -{ - if (pendingMaskSpinnerView) - { - [pendingMaskSpinnerView removeFromSuperview]; - pendingMaskSpinnerView = nil; - } -} - -- (void)pushViewController:(UIViewController*)viewController -{ - // Keep ref on pushed view controller - pushedViewController = viewController; - - // Check whether the view controller is displayed inside a segmented one. - if (self.parentViewController.navigationController) - { - // Hide back button title - [self.parentViewController vc_removeBackTitle]; - - [self.parentViewController.navigationController pushViewController:viewController animated:YES]; - } - else - { - // Hide back button title - [self vc_removeBackTitle]; - - [self.navigationController pushViewController:viewController animated:YES]; - } -} - -- (void)releasePushedViewController -{ - if (pushedViewController) - { - if ([pushedViewController isKindOfClass:[UINavigationController class]]) - { - UINavigationController *navigationController = (UINavigationController*)pushedViewController; - for (id subViewController in navigationController.viewControllers) - { - if ([subViewController respondsToSelector:@selector(destroy)]) - { - [subViewController destroy]; - } - } - } - else if ([pushedViewController respondsToSelector:@selector(destroy)]) - { - [(id)pushedViewController destroy]; - } - - pushedViewController = nil; - } -} - -#pragma mark - UITableView data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - NSInteger count = 0; - - participantsSection = invitedSection = -1; - - if (currentSearchText.length) - { - if (filteredActualParticipants.count) - { - participantsSection = count++; - } - - if (filteredInvitedParticipants.count) - { - invitedSection = count++; - } - } - else - { - if (actualParticipants.count) - { - participantsSection = count++; - } - - if (invitedParticipants.count) - { - invitedSection = count++; - } - } - - return count; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - NSInteger count = 0; - - if (section == participantsSection) - { - if (currentSearchText.length) - { - count = filteredActualParticipants.count; - } - else - { - count = actualParticipants.count; - } - } - else if (section == invitedSection) - { - if (currentSearchText.length) - { - count = filteredInvitedParticipants.count; - } - else - { - count = invitedParticipants.count; - } - } - - return count; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - UITableViewCell *cell; - - if (indexPath.section == participantsSection || indexPath.section == invitedSection) - { - ContactTableViewCell* participantCell = [tableView dequeueReusableCellWithIdentifier:@"ParticipantTableViewCellId" forIndexPath:indexPath]; - participantCell.selectionStyle = UITableViewCellSelectionStyleNone; - - Contact *contact; - NSArray *participants; - - if (indexPath.section == participantsSection) - { - if (currentSearchText.length) - { - participants = filteredActualParticipants; - } - else - { - participants = actualParticipants; - } - } - else - { - if (currentSearchText.length) - { - participants = filteredInvitedParticipants; - } - else - { - participants = invitedParticipants; - } - } - - if (indexPath.row < participants.count) - { - contact = participants[indexPath.row]; - } - - if (contact) - { - [participantCell render:contact]; - - NSString *powerLevelText; - - // Update power level label - if (contact.mxGroupUser.isPrivileged) - { - powerLevelText = [VectorL10n roomMemberPowerLevelShortAdmin]; - } - - participantCell.powerLevelLabel.text = powerLevelText; - } - - cell = participantCell; - } - else - { - // Return a fake cell to prevent app from crashing. - cell = [[UITableViewCell alloc] init]; - } - - return cell; -} - -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section == participantsSection || indexPath.section == invitedSection) - { - return YES; - } - return NO; -} - -- (void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath -{ - // iOS8 requires this method to enable editing (see editActionsForRowAtIndexPath). -} - -#pragma mark - UITableView delegate - -- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - return tableView.estimatedRowHeight; -} - -- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section -{ - if (section == invitedSection) - { - return tableView.estimatedSectionHeaderHeight; - } - - return 0; -} - -- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath -{ - cell.backgroundColor = ThemeService.shared.theme.backgroundColor; - - // Update the selected background view - if (ThemeService.shared.theme.selectedBackgroundColor) - { - cell.selectedBackgroundView = [[UIView alloc] init]; - cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor; - } - else - { - if (tableView.style == UITableViewStylePlain) - { - cell.selectedBackgroundView = nil; - } - else - { - cell.selectedBackgroundView.backgroundColor = nil; - } - } - - // Refresh here the estimated row height - tableView.estimatedRowHeight = cell.frame.size.height; -} - -- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(nonnull UIView *)view forSection:(NSInteger)section -{ - // Refresh here the estimated header height - tableView.estimatedSectionHeaderHeight = view.frame.size.height; -} - -- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section -{ - MXKTableViewHeaderFooterWithLabel *sectionHeader; - - if (section == invitedSection) - { - sectionHeader = [tableView dequeueReusableHeaderFooterViewWithIdentifier:MXKTableViewHeaderFooterWithLabel.defaultReuseIdentifier]; - sectionHeader.mxkContentView.backgroundColor = ThemeService.shared.theme.headerBackgroundColor; - sectionHeader.mxkLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - sectionHeader.mxkLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; - - sectionHeader.mxkLabel.text = [VectorL10n groupParticipantsInvitedSection]; - } - - return sectionHeader; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - Contact *contact; - NSArray *participants; - - if (indexPath.section == participantsSection) - { - if (currentSearchText.length) - { - participants = filteredActualParticipants; - } - else - { - participants = actualParticipants; - } - } - else if (indexPath.section == invitedSection) - { - if (currentSearchText.length) - { - participants = filteredInvitedParticipants; - } - else - { - participants = invitedParticipants; - } - } - - if (indexPath.row < participants.count) - { - contact = participants[indexPath.row]; - } - - if (contact) - { - ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController instantiate]; - contactDetailsViewController.enableVoipCall = NO; - contactDetailsViewController.contact = contact; - - [self pushViewController:contactDetailsViewController]; - } - - [tableView deselectRowAtIndexPath:indexPath animated:YES]; -} - -- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSMutableArray* actions; - - // add the swipe to delete only on participants sections - if (indexPath.section == participantsSection || indexPath.section == invitedSection) - { - actions = [[NSMutableArray alloc] init]; - - // Patch: Force the width of the button by adding whitespace characters into the title string. - UITableViewRowAction *leaveAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:@" " handler:^(UITableViewRowAction *action, NSIndexPath *indexPath){ - - [self onDeleteAt:indexPath]; - - }]; - - leaveAction.backgroundColor = [MXKTools convertImageToPatternColor:@"remove_icon_blue" backgroundColor:ThemeService.shared.theme.headerBackgroundColor patternSize:CGSizeMake(74, 74) resourceSize:CGSizeMake(24, 24)]; - [actions insertObject:leaveAction atIndex:0]; - } - - return actions; -} - -#pragma mark - ContactsTableViewControllerDelegate - -- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact -{ - [self didSelectInvitableContact:contact]; -} - -#pragma mark - Actions - -- (void)onDeleteAt:(NSIndexPath*)path -{ - if (path.section == participantsSection || path.section == invitedSection) - { - __weak typeof(self) weakSelf = self; - - if (currentAlert) - { - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - currentAlert = nil; - } - - NSMutableArray *participants; - Contact *contact; - - if (path.section == participantsSection) - { - if (currentSearchText.length) - { - participants = filteredActualParticipants; - } - else - { - participants = actualParticipants; - } - } - else - { - if (currentSearchText.length) - { - participants = filteredInvitedParticipants; - } - else - { - participants = invitedParticipants; - } - } - - if (path.row < participants.count) - { - contact = participants[path.row]; - } - - if (contact && [contact.mxGroupUser.userId isEqualToString:self.mxSession.myUser.userId]) - { - // Leave this group? - currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n groupParticipantsLeavePromptTitle] - message:[VectorL10n groupParticipantsLeavePromptMsg] - preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n leave] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - - [self addPendingActionMask]; - [self.mxSession leaveGroup:self->_group.groupId success:^{ - - [self withdrawViewControllerAnimated:YES completion:nil]; - - } failure:^(NSError *error) { - - [self removePendingActionMask]; - MXLogDebug(@"[GroupParticipantsVC] Leave group %@ failed", self->_group.groupId); - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - } - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"GroupParticipantsVCLeaveAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; - } - else if (contact) - { - NSString *memberUserId = contact.mxGroupUser.userId; - - // Kick ? - NSString *promptMsg = [VectorL10n groupParticipantsRemovePromptMsg:(contact.mxGroupUser.displayname.length ? contact.mxGroupUser.displayname : memberUserId)]; - currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n groupParticipantsRemovePromptTitle] - message:promptMsg - preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n remove] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - - MXLogDebug(@"[GroupParticipantsVC] Kick %@ failed", memberUserId); - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:[NSError errorWithDomain:@"GroupDomain" code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n notSupportedYet]}]]; - } - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"GroupParticipantsVCKickAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; - } - } -} - -#pragma mark - - -- (void)didSelectInvitableContact:(MXKContact*)contact -{ - __weak typeof(self) weakSelf = self; - - if (currentAlert) - { - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - currentAlert = nil; - } - - // Invite ? - NSString *promptMsg = [VectorL10n groupParticipantsInvitePromptMsg:contact.displayName]; - currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n groupParticipantsInvitePromptTitle] - message:promptMsg - preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n invite] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - - NSArray *identifiers = contact.matrixIdentifiers; - NSString *participantId; - - if (identifiers.count) - { - participantId = identifiers.firstObject; - - MXLogDebug(@"[GroupParticipantsVC] Invite %@ failed", participantId); - [[AppDelegate theDelegate] showErrorAsAlert:[NSError errorWithDomain:@"GroupDomain" code:0 userInfo:@{NSLocalizedDescriptionKey:[VectorL10n notSupportedYet]}]]; - } - } - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"GroupParticipantsVCInviteAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; -} - -#pragma mark - UISearchBar delegate - -- (void)refreshSearchBarItemsColor:(UISearchBar *)searchBar -{ - // bar tint color - searchBar.barTintColor = searchBar.tintColor = ThemeService.shared.theme.tintColor; - searchBar.tintColor = ThemeService.shared.theme.tintColor; - - // FIXME: this all seems incredibly fragile and tied to gutwrenching the current UISearchBar internals. - - // text color - UITextField *searchBarTextField = searchBar.vc_searchTextField; - searchBarTextField.textColor = ThemeService.shared.theme.textSecondaryColor; - - // Magnifying glass icon. - UIImageView *leftImageView = (UIImageView *)searchBarTextField.leftView; - leftImageView.image = [leftImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - leftImageView.tintColor = ThemeService.shared.theme.tintColor; - - // remove the gray background color - UIView *effectBackgroundTop = [searchBarTextField valueForKey:@"_effectBackgroundTop"]; - UIView *effectBackgroundBottom = [searchBarTextField valueForKey:@"_effectBackgroundBottom"]; - effectBackgroundTop.hidden = YES; - effectBackgroundBottom.hidden = YES; - - // place holder - searchBarTextField.textColor = ThemeService.shared.theme.placeholderTextColor; -} - -- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText -{ - // Update search results. - NSUInteger index; - MXKContact *contact; - - searchText = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - - if (!currentSearchText.length || [searchText hasPrefix:currentSearchText] == NO) - { - // Copy participants and invited participants - filteredActualParticipants = [NSMutableArray arrayWithArray:actualParticipants]; - filteredInvitedParticipants = [NSMutableArray arrayWithArray:invitedParticipants]; - } - - currentSearchText = searchText; - - // Filter group participants - if (currentSearchText.length) - { - for (index = 0; index < filteredActualParticipants.count;) - { - contact = filteredActualParticipants[index]; - if (![contact matchedWithPatterns:@[currentSearchText]]) - { - [filteredActualParticipants removeObjectAtIndex:index]; - } - else - { - index++; - } - } - - for (index = 0; index < filteredInvitedParticipants.count;) - { - contact = filteredInvitedParticipants[index]; - if (![contact matchedWithPatterns:@[currentSearchText]]) - { - [filteredInvitedParticipants removeObjectAtIndex:index]; - } - else - { - index++; - } - } - } - else - { - filteredActualParticipants = nil; - filteredInvitedParticipants = nil; - } - - // Refresh display - [self refreshTableView]; -} - -- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar -{ - searchBar.showsCancelButton = YES; - - return YES; -} - -- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar -{ - searchBar.showsCancelButton = NO; - - return YES; -} - -- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar -{ - // "Done" key has been pressed. - - // Dismiss keyboard - [_searchBarView resignFirstResponder]; -} - -- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar -{ - if (currentSearchText) - { - currentSearchText = nil; - filteredActualParticipants = nil; - filteredInvitedParticipants = nil; - - [self refreshTableView]; - } - - searchBar.text = nil; - // Leave search - [searchBar resignFirstResponder]; -} - -@end diff --git a/Riot/Modules/Communities/Members/GroupParticipantsViewController.xib b/Riot/Modules/Communities/Members/GroupParticipantsViewController.xib deleted file mode 100644 index d9ecc8be3..000000000 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.xib +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.h b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.h deleted file mode 100644 index fe0a504a7..000000000 --- a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MatrixKit.h" - -/** - 'GroupRoomsViewController' instance is used to list the rooms of the group defined by the property 'mxGroup'. - When this property is nil, the view controller is empty. - */ -@interface GroupRoomsViewController : MXKViewController -{ -@protected - - /** - The current list of the rooms. - */ - NSArray *groupRooms; -} - -@property (weak, nonatomic) IBOutlet UITableView *tableView; -@property (weak, nonatomic) IBOutlet UIView *searchBarHeader; -@property (weak, nonatomic) IBOutlet UISearchBar *searchBarView; -@property (weak, nonatomic) IBOutlet UIView *searchBarHeaderBorder; - -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *searchBarTopConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *tableViewBottomConstraint; - -/** - A matrix group (nil by default). - */ -@property (strong, readonly, nonatomic) MXGroup *group; -@property (strong, readonly, nonatomic) MXSession *mxSession; - -/** - Returns the `UINib` object initialized for a `GroupRoomsViewController`. - - @return The initialized `UINib` object or `nil` if there were errors during initialization - or the nib file could not be located. - */ -+ (UINib *)nib; - -/** - Creates and returns a new `GroupRoomsViewController` object. - - @discussion This is the designated initializer for programmatic instantiation. - @return An initialized `GroupRoomsViewController` object if successful, `nil` otherwise. - */ -+ (instancetype)groupRoomsViewController; - -/** - Set the group for which the rooms are listed. - Provide the related matrix session. - */ -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession; - -@end - diff --git a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m deleted file mode 100644 index 4fa0a6cb4..000000000 --- a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m +++ /dev/null @@ -1,698 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupRoomsViewController.h" - -#import "GeneratedInterface-Swift.h" - -#import "GroupRoomTableViewCell.h" - -#import "RageShakeManager.h" - -@interface GroupRoomsViewController () -{ - // Search result - NSString *currentSearchText; - NSMutableArray *filteredGroupRooms; - - // The current pushed view controller - UIViewController *pushedViewController; - - // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. - id kThemeServiceDidChangeThemeNotificationObserver; -} - -@end - -@implementation GroupRoomsViewController - -#pragma mark - Class methods - -+ (UINib *)nib -{ - return [UINib nibWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -+ (instancetype)groupRoomsViewController -{ - return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -#pragma mark - - -- (void)finalizeInit -{ - [super finalizeInit]; - - // Setup `MXKViewControllerHandling` properties - self.enableBarTintColorStatusChange = NO; - self.rageShakeManager = [RageShakeManager sharedManager]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - // Do any additional setup after loading the view, typically from a nib. - - // Check whether the view controller has been pushed via storyboard - if (!self.tableView) - { - // Instantiate view controller objects - [[[self class] nib] instantiateWithOwner:self options:nil]; - } - - // Adjust Top and Bottom constraints to take into account potential navBar and tabBar. - [NSLayoutConstraint deactivateConstraints:@[_searchBarTopConstraint, _tableViewBottomConstraint]]; - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated" - _searchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.searchBarHeader - attribute:NSLayoutAttributeTop - multiplier:1.0f - constant:0.0f]; - - _tableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.tableView - attribute:NSLayoutAttributeBottom - multiplier:1.0f - constant:0.0f]; - #pragma clang diagnostic pop - - [NSLayoutConstraint activateConstraints:@[_searchBarTopConstraint, _tableViewBottomConstraint]]; - - _searchBarView.placeholder = [VectorL10n groupRoomsFilterRooms]; - _searchBarView.returnKeyType = UIReturnKeyDone; - _searchBarView.autocapitalizationType = UITextAutocapitalizationTypeNone; - - // Search bar header is hidden when no group is provided - _searchBarHeader.hidden = (self.group == nil); - - // Enable self-sizing cells and section headers. - self.tableView.rowHeight = UITableViewAutomaticDimension; - self.tableView.estimatedRowHeight = 74; - self.tableView.sectionHeaderHeight = 0; - - // Hide line separators of empty cells - self.tableView.tableFooterView = [[UIView alloc] init]; - - [self.tableView registerClass:GroupRoomTableViewCell.class forCellReuseIdentifier:@"RoomTableViewCellId"]; - - // Observe user interface theme change. - kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - [self userInterfaceThemeDidChange]; - - }]; - [self userInterfaceThemeDidChange]; -} - -- (void)userInterfaceThemeDidChange -{ - [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar]; - - self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor; - - [self refreshSearchBarItemsColor:_searchBarView]; - - _searchBarHeaderBorder.backgroundColor = ThemeService.shared.theme.headerBorderColor; - - // Check the table view style to select its bg color. - self.tableView.backgroundColor = ((self.tableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.backgroundColor : ThemeService.shared.theme.headerBackgroundColor); - self.view.backgroundColor = self.tableView.backgroundColor; - self.tableView.separatorColor = ThemeService.shared.theme.lineBreakColor; - - if (self.tableView.dataSource) - { - [self.tableView reloadData]; - } - - [self setNeedsStatusBarAppearanceUpdate]; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return ThemeService.shared.theme.statusBarStyle; -} - -// This method is called when the viewcontroller is added or removed from a container view controller. -- (void)didMoveToParentViewController:(nullable UIViewController *)parent -{ - [super didMoveToParentViewController:parent]; -} - -- (void)destroy -{ - // Release the potential pushed view controller - [self releasePushedViewController]; - - if (kThemeServiceDidChangeThemeNotificationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver]; - kThemeServiceDidChangeThemeNotificationObserver = nil; - } - - _group = nil; - _mxSession = nil; - - filteredGroupRooms = nil; - - groupRooms = nil; - - // Note: all observers are removed during super call. - [super destroy]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Release the potential pushed view controller - [self releasePushedViewController]; - - if (_group) - { - // Restore the listeners on the group update. - [self registerOnGroupChangeNotifications]; - - // Check whether the selected group is stored in the user's session, or if it is a group preview. - // Replace the displayed group instance with the one stored in the session (if any). - MXGroup *storedGroup = [_mxSession groupWithGroupId:_group.groupId]; - BOOL isPreview = (!storedGroup); - - // Force refresh - [self refreshDisplayWithGroup:(isPreview ? _group : storedGroup)]; - - // Prepare a block called on successful update in case of a group preview. - // Indeed the group update notifications are triggered by the matrix session only for the user's groups. - void (^success)(void) = ^void(void) - { - [self refreshDisplayWithGroup:self->_group]; - }; - - // Trigger a refresh on the group rooms. - [self.mxSession updateGroupRooms:_group success:(isPreview ? success : nil) failure:^(NSError *error) { - - MXLogDebug(@"[GroupRoomsViewController] viewWillAppear: group rooms update failed %@", self->_group.groupId); - - }]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - [self cancelRegistrationOnGroupChangeNotifications]; - - // cancel any pending search - [self searchBarCancelButtonClicked:_searchBarView]; -} - -- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion -{ - // Check whether the current view controller is displayed inside a segmented view controller in order to withdraw the right item - if (self.parentViewController && [self.parentViewController isKindOfClass:SegmentedViewController.class]) - { - [((SegmentedViewController*)self.parentViewController) withdrawViewControllerAnimated:animated completion:completion]; - } - else - { - [super withdrawViewControllerAnimated:animated completion:completion]; - } -} - -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession -{ - // Cancel any pending search - [self searchBarCancelButtonClicked:_searchBarView]; - - if (_mxSession != mxSession) - { - [self cancelRegistrationOnGroupChangeNotifications]; - _mxSession = mxSession; - - [self registerOnGroupChangeNotifications]; - } - - [self addMatrixSession:mxSession]; - - [self refreshDisplayWithGroup:group]; -} - -#pragma mark - - -- (void)registerOnGroupChangeNotifications -{ - if (_mxSession) - { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGroupRooms:) name:kMXSessionDidUpdateGroupRoomsNotification object:_mxSession]; - } -} - -- (void)cancelRegistrationOnGroupChangeNotifications -{ - // Remove any pending observers - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdateGroupRoomsNotification object:_mxSession]; -} - -- (void)didUpdateGroupRooms:(NSNotification *)notif -{ - MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey]; - if (group && [group.groupId isEqualToString:_group.groupId]) - { - // Update the current displayed group instance with the one stored in the session. - [self refreshDisplayWithGroup:group]; - } -} - -- (void)refreshDisplayWithGroup:(MXGroup *)group -{ - _group = group; - - if (_group) - { - _searchBarHeader.hidden = NO; - groupRooms = _group.rooms.chunk; - } - else - { - // Search bar header is hidden when no group is provided - _searchBarHeader.hidden = YES; - groupRooms = nil; - } - - // Reload search result if any - if (currentSearchText.length) - { - NSString *searchText = currentSearchText; - currentSearchText = nil; - - [self searchBar:_searchBarView textDidChange:searchText]; - } - else - { - [self refreshTableView]; - } -} - -- (void)startActivityIndicator -{ - // Check whether the current view controller is displayed inside a segmented view controller in order to run the right activity view - if (self.parentViewController && [self.parentViewController isKindOfClass:SegmentedViewController.class]) - { - [((SegmentedViewController*)self.parentViewController) startActivityIndicator]; - - // Force stop the activity view of the view controller - [self.activityIndicator stopAnimating]; - } - else - { - [super startActivityIndicator]; - } -} - -- (void)stopActivityIndicator -{ - // Check whether the current view controller is displayed inside a segmented view controller in order to stop the right activity view - if (self.parentViewController && [self.parentViewController isKindOfClass:SegmentedViewController.class]) - { - [((SegmentedViewController*)self.parentViewController) stopActivityIndicator]; - - // Force stop the activity view of the view controller - [self.activityIndicator stopAnimating]; - } - else - { - [super stopActivityIndicator]; - } -} - -#pragma mark - Internals - -- (void)refreshTableView -{ - [self.tableView reloadData]; -} - -- (void)pushViewController:(UIViewController*)viewController -{ - // Keep ref on pushed view controller - pushedViewController = viewController; - - // Check whether the view controller is displayed inside a segmented one. - if (self.parentViewController.navigationController) - { - // Hide back button title - [self.parentViewController vc_removeBackTitle]; - - [self.parentViewController.navigationController pushViewController:viewController animated:YES]; - } - else - { - // Hide back button title - [self vc_removeBackTitle]; - - [self.navigationController pushViewController:viewController animated:YES]; - } -} - -- (void)releasePushedViewController -{ - if (pushedViewController) - { - if ([pushedViewController isKindOfClass:[UINavigationController class]]) - { - UINavigationController *navigationController = (UINavigationController*)pushedViewController; - for (id subViewController in navigationController.viewControllers) - { - if ([subViewController respondsToSelector:@selector(destroy)]) - { - [subViewController destroy]; - } - } - } - else if ([pushedViewController respondsToSelector:@selector(destroy)]) - { - [(id)pushedViewController destroy]; - } - - pushedViewController = nil; - } -} - -#pragma mark - UITableView data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - NSInteger count = 0; - - if (currentSearchText.length) - { - if (filteredGroupRooms.count) - { - count++; - } - } - else - { - if (groupRooms.count) - { - count++; - } - } - - return count; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - NSInteger count = 0; - - if (currentSearchText.length) - { - count = filteredGroupRooms.count; - } - else - { - count = groupRooms.count; - } - - return count; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - GroupRoomTableViewCell* roomCell = [tableView dequeueReusableCellWithIdentifier:@"RoomTableViewCellId" forIndexPath:indexPath]; - roomCell.selectionStyle = UITableViewCellSelectionStyleNone; - - MXGroupRoom *room; - NSArray *rooms; - - if (currentSearchText.length) - { - rooms = filteredGroupRooms; - } - else - { - rooms = groupRooms; - } - - if (indexPath.row < rooms.count) - { - room = rooms[indexPath.row]; - } - - if (room) - { - [roomCell render:room withMatrixSession:self.mxSession]; - } - - return roomCell; -} - -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - return NO; -} - -- (void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath -{ - // iOS8 requires this method to enable editing (see editActionsForRowAtIndexPath). -} - -#pragma mark - UITableView delegate - -- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - return tableView.estimatedRowHeight; -} - -- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath -{ - cell.backgroundColor = ThemeService.shared.theme.backgroundColor; - - // Update the selected background view - if (ThemeService.shared.theme.selectedBackgroundColor) - { - cell.selectedBackgroundView = [[UIView alloc] init]; - cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor; - } - else - { - if (tableView.style == UITableViewStylePlain) - { - cell.selectedBackgroundView = nil; - } - else - { - cell.selectedBackgroundView.backgroundColor = nil; - } - } - - // Refresh here the estimated row height - tableView.estimatedRowHeight = cell.frame.size.height; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - MXGroupRoom *room; - NSArray *rooms; - - if (currentSearchText.length) - { - rooms = filteredGroupRooms; - } - else - { - rooms = groupRooms; - } - - if (indexPath.row < rooms.count) - { - room = rooms[indexPath.row]; - } - - if (room) - { - // Check first if the user already joined this room. - if ([self.mxSession roomWithRoomId:room.roomId]) - { - // Open this room - MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mxSession]; - [roomDataSourceManager roomDataSourceForRoom:room.roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) { - - RoomViewController *roomViewController = [RoomViewController roomViewController]; - roomViewController.showMissedDiscussionsBadge = NO; - [roomViewController displayRoom:roomDataSource]; - [self pushViewController:roomViewController]; - }]; - } - else - { - // Prepare a preview - RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:room.roomId andSession:self.mxSession]; - [self startActivityIndicator]; - - // Try to get more information about the room before opening its preview - [roomPreviewData peekInRoom:^(BOOL succeeded) { - - [self stopActivityIndicator]; - - // If no data is available for this room, we name it with the known information (if any). - if (!succeeded) - { - roomPreviewData.roomName = (room.name.length ? room.name : room.canonicalAlias); - } - - // Display the room preview - RoomViewController *roomViewController = [RoomViewController roomViewController]; - roomViewController.showMissedDiscussionsBadge = NO; - [roomViewController displayRoomPreview:roomPreviewData]; - [self pushViewController:roomViewController]; - }]; - } - } - - [tableView deselectRowAtIndexPath:indexPath animated:YES]; -} - - -#pragma mark - UISearchBar delegate - -- (void)refreshSearchBarItemsColor:(UISearchBar *)searchBar -{ - // bar tint color - searchBar.barTintColor = searchBar.tintColor = ThemeService.shared.theme.tintColor; - searchBar.tintColor = ThemeService.shared.theme.tintColor; - - // FIXME: this all seems incredibly fragile and tied to gutwrenching the current UISearchBar internals. - - // text color - UITextField *searchBarTextField = searchBar.vc_searchTextField; - searchBarTextField.textColor = ThemeService.shared.theme.textSecondaryColor; - - // Magnifying glass icon. - UIImageView *leftImageView = (UIImageView *)searchBarTextField.leftView; - leftImageView.image = [leftImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - leftImageView.tintColor = ThemeService.shared.theme.tintColor; - - // remove the gray background color - UIView *effectBackgroundTop = [searchBarTextField valueForKey:@"_effectBackgroundTop"]; - UIView *effectBackgroundBottom = [searchBarTextField valueForKey:@"_effectBackgroundBottom"]; - effectBackgroundTop.hidden = YES; - effectBackgroundBottom.hidden = YES; - - // place holder - searchBarTextField.textColor = ThemeService.shared.theme.placeholderTextColor; -} - -- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText -{ - // Update search results. - NSUInteger index; - MXGroupRoom *groupRoom; - - searchText = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - - if (!currentSearchText.length || [searchText hasPrefix:currentSearchText] == NO) - { - // Copy participants and invited participants - filteredGroupRooms = [NSMutableArray arrayWithArray:groupRooms]; - } - - currentSearchText = searchText; - - // Filter group participants - if (currentSearchText.length) - { - for (index = 0; index < filteredGroupRooms.count;) - { - groupRoom = filteredGroupRooms[index]; - - NSString *displayName = groupRoom.name; - if (!displayName) - { - displayName = groupRoom.canonicalAlias; - } - if (!displayName) - { - displayName = groupRoom.roomId; - } - - if ([displayName rangeOfString:currentSearchText options:NSCaseInsensitiveSearch].location == NSNotFound) - { - [filteredGroupRooms removeObjectAtIndex:index]; - } - else - { - index++; - } - } - } - else - { - filteredGroupRooms = nil; - } - - // Refresh display - [self refreshTableView]; -} - -- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar -{ - searchBar.showsCancelButton = YES; - - return YES; -} - -- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar -{ - searchBar.showsCancelButton = NO; - - return YES; -} - -- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar -{ - // "Done" key has been pressed. - - // Dismiss keyboard - [_searchBarView resignFirstResponder]; -} - -- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar -{ - if (currentSearchText) - { - currentSearchText = nil; - filteredGroupRooms = nil; - - [self refreshTableView]; - } - - searchBar.text = nil; - // Leave search - [searchBar resignFirstResponder]; -} - -@end diff --git a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.xib b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.xib deleted file mode 100644 index 5e2d861bb..000000000 --- a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.xib +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.h b/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.h deleted file mode 100644 index e140b3585..000000000 --- a/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MatrixKit.h" - -@interface GroupRoomTableViewCell : MXKTableViewCell - -@property (weak, nonatomic) IBOutlet UILabel *roomDisplayName; -@property (weak, nonatomic) IBOutlet UILabel *roomTopic; -@property (weak, nonatomic) IBOutlet MXKImageView *roomAvatar; - -/** - Configure the cell in order to display a room of a group. - - @param groupRoom the room to render. - */ -- (void)render:(MXGroupRoom*)groupRoom withMatrixSession:(MXSession*)mxSession; - -@end diff --git a/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.m b/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.m deleted file mode 100644 index 80d84a128..000000000 --- a/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.m +++ /dev/null @@ -1,96 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - Copyright 2018 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupRoomTableViewCell.h" - -#import "AvatarGenerator.h" - -#import "ThemeService.h" -#import "GeneratedInterface-Swift.h" - -@implementation GroupRoomTableViewCell - -- (void)awakeFromNib -{ - [super awakeFromNib]; - - // Round image view - [_roomAvatar.layer setCornerRadius:_roomAvatar.frame.size.width / 2]; - _roomAvatar.clipsToBounds = YES; -} - -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.roomDisplayName.textColor = ThemeService.shared.theme.textPrimaryColor; - self.roomTopic.textColor = ThemeService.shared.theme.textSecondaryColor; - - _roomAvatar.defaultBackgroundColor = [UIColor clearColor]; -} - -- (void)render:(MXGroupRoom *)groupRoom withMatrixSession:(MXSession*)mxSession -{ - // Set room display name - self.roomDisplayName.text = groupRoom.name; - if (!self.roomDisplayName.text) - { - self.roomDisplayName.text = groupRoom.canonicalAlias; - } - if (!self.roomDisplayName.text) - { - self.roomDisplayName.text = groupRoom.roomId; - } - - // Check whether this room has topic - if (groupRoom.topic) - { - _roomTopic.hidden = NO; - _roomTopic.text = [MXTools stripNewlineCharacters:groupRoom.topic]; - } - else - { - // Hide and fill the label with a fake description to harmonize the height of all the cells. - // This is a drawback of the self-sizing cell. - _roomTopic.hidden = YES; - _roomTopic.text = @"No topic"; - } - - // Set the avatar - UIImage* avatarImage = [AvatarGenerator generateAvatarForMatrixItem:groupRoom.roomId withDisplayName:self.roomDisplayName.text]; - - if (groupRoom.avatarUrl) - { - _roomAvatar.enableInMemoryCache = YES; - - [_roomAvatar setImageURI:groupRoom.avatarUrl - withType:nil - andImageOrientation:UIImageOrientationUp - toFitViewSize:_roomAvatar.frame.size - withMethod:MXThumbnailingMethodCrop - previewImage:avatarImage - mediaManager:mxSession.mediaManager]; - } - else - { - _roomAvatar.image = avatarImage; - } - - _roomAvatar.contentMode = UIViewContentModeScaleAspectFill; -} - -@end diff --git a/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.xib b/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.xib deleted file mode 100644 index 33dbfa4c6..000000000 --- a/Riot/Modules/Communities/Rooms/Views/GroupRoomTableViewCell.xib +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinator.swift b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinator.swift deleted file mode 100644 index 976c188ef..000000000 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinator.swift +++ /dev/null @@ -1,59 +0,0 @@ -// File created from ScreenTemplate -// $ createScreen.sh Communities GroupDetails -/* - Copyright 2021 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import Foundation -import UIKit - -final class GroupDetailsCoordinator: GroupDetailsCoordinatorProtocol { - - // MARK: - Properties - - // MARK: Private - - private let parameters: GroupDetailsCoordinatorParameters - private let groupDetailsViewController: GroupDetailsViewController - - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] - - weak var delegate: GroupDetailsCoordinatorDelegate? - - // MARK: - Setup - - init(parameters: GroupDetailsCoordinatorParameters) { - self.parameters = parameters - let groupDetailsViewController: GroupDetailsViewController = GroupDetailsViewController.instantiate() - self.groupDetailsViewController = groupDetailsViewController - } - - deinit { - groupDetailsViewController.destroy() - } - - // MARK: - Public - - func start() { - self.groupDetailsViewController.setGroup(self.parameters.group, withMatrixSession: self.parameters.session) - } - - func toPresentable() -> UIViewController { - return self.groupDetailsViewController - } -} diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift deleted file mode 100644 index 8ff866d7a..000000000 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift +++ /dev/null @@ -1,29 +0,0 @@ -// File created from ScreenTemplate -// $ createScreen.sh Communities GroupDetails -/* - Copyright 2021 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import Foundation - -/// GroupDetailsCoordinator input parameters -struct GroupDetailsCoordinatorParameters { - - /// The Matrix session - let session: MXSession - - /// The group for which the details are displayed - let group: MXGroup -} diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorProtocol.swift b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorProtocol.swift deleted file mode 100644 index fbb3f9ff8..000000000 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorProtocol.swift +++ /dev/null @@ -1,28 +0,0 @@ -// File created from ScreenTemplate -// $ createScreen.sh Communities GroupDetails -/* - Copyright 2021 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import Foundation - -protocol GroupDetailsCoordinatorDelegate: AnyObject { - func groupDetailsCoordinatorDidCancel(_ coordinator: GroupDetailsCoordinatorProtocol) -} - -/// `GroupDetailsCoordinatorProtocol` is a protocol describing a Coordinator that handle communities navigation flow. -protocol GroupDetailsCoordinatorProtocol: Coordinator, Presentable { - var delegate: GroupDetailsCoordinatorDelegate? { get } -} diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h deleted file mode 100644 index faaab6b94..000000000 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "SegmentedViewController.h" - -@interface GroupDetailsViewController : SegmentedViewController - -@property (strong, readonly, nonatomic) MXGroup *group; -@property (strong, readonly, nonatomic) MXSession *mxSession; - -/** - Returns the `UINib` object initialized for a `GroupDetailsViewController`. - - @return The initialized `UINib` object or `nil` if there were errors during initialization - or the nib file could not be located. - */ -+ (UINib *)nib; - -/** - Creates and returns a new `GroupDetailsViewController` object. - - @discussion This is the designated initializer for programmatic instantiation. - @return An initialized `GroupDetailsViewController` object if successful, `nil` otherwise. - */ -+ (instancetype)instantiate; - -/** - Set the group for which the details are displayed. - Provide the related matrix session. - */ -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession; - -@end - diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m deleted file mode 100644 index 339af8584..000000000 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m +++ /dev/null @@ -1,181 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupDetailsViewController.h" - -#import "GroupHomeViewController.h" -#import "GroupParticipantsViewController.h" -#import "GroupRoomsViewController.h" - -#import "GeneratedInterface-Swift.h" - -@interface GroupDetailsViewController () -{ - GroupHomeViewController *groupHomeViewController; - GroupParticipantsViewController *groupParticipantsViewController; - GroupRoomsViewController *groupRoomsViewController; - - /** - mask view while processing a request - */ - UIActivityIndicatorView * pendingMaskSpinnerView; - - /** - Current alert (if any). - */ - UIAlertController *currentAlert; - - /** - The current visibility of the status bar in this view controller. - */ - BOOL isStatusBarHidden; -} -@end - -@implementation GroupDetailsViewController - -#pragma mark - Class methods - -+ (UINib *)nib -{ - return [UINib nibWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -+ (instancetype)instantiate -{ - return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; -} - -#pragma mark - - -- (void)finalizeInit -{ - [super finalizeInit]; - - // Setup `MXKViewControllerHandling` properties - self.enableBarTintColorStatusChange = NO; - self.rageShakeManager = [RageShakeManager sharedManager]; - - self.sectionHeaderTintColor = ThemeService.shared.theme.tintColor; - - // Keep visible the status bar by default. - isStatusBarHidden = NO; -} - -- (void)viewDidLoad -{ - NSMutableArray* viewControllers = [[NSMutableArray alloc] init]; - NSMutableArray* titles = [[NSMutableArray alloc] init]; - - // home tab - [titles addObject:[VectorL10n groupDetailsHome]]; - groupHomeViewController = [GroupHomeViewController groupHomeViewController]; - if (_group) - { - [groupHomeViewController setGroup:_group withMatrixSession:_mxSession]; - } - [viewControllers addObject:groupHomeViewController]; - - // People tab - [titles addObject:[VectorL10n groupDetailsPeople]]; - groupParticipantsViewController = [GroupParticipantsViewController groupParticipantsViewController]; - if (_group) - { - [groupParticipantsViewController setGroup:_group withMatrixSession:_mxSession]; - } - [viewControllers addObject:groupParticipantsViewController]; - - // Rooms tab - [titles addObject:[VectorL10n groupDetailsRooms]]; - groupRoomsViewController = [GroupRoomsViewController groupRoomsViewController]; - if (_group) - { - [groupRoomsViewController setGroup:_group withMatrixSession:_mxSession]; - } - [viewControllers addObject:groupRoomsViewController]; - - if (!self.title.length) - { - self.title = [VectorL10n groupDetailsTitle]; - } - - [self initWithTitles:titles viewControllers:viewControllers defaultSelected:0]; - - [super viewDidLoad]; - - // Display leftBarButtonItems or leftBarButtonItem to the right of the Back button - self.navigationItem.leftItemsSupplementBackButton = YES; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return ThemeService.shared.theme.statusBarStyle; -} - -- (BOOL)prefersStatusBarHidden -{ - // Return the current status bar visibility. - return isStatusBarHidden; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; -} - -- (void)setGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession -{ - _group = group; - _mxSession = mxSession; - - self.title = group.summary.profile.name.length ? group.summary.profile.name : group.groupId; - - [self addMatrixSession:mxSession]; - - if (groupHomeViewController) - { - [groupHomeViewController setGroup:group withMatrixSession:mxSession]; - } - if (groupParticipantsViewController) - { - [groupParticipantsViewController setGroup:group withMatrixSession:mxSession]; - } - if (groupRoomsViewController) - { - [groupRoomsViewController setGroup:group withMatrixSession:mxSession]; - } -} - -- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion -{ - [super withdrawViewControllerAnimated:animated completion:completion]; - - // Fill the secondary navigation view controller of the split view controller if it is empty. - UINavigationController *secondaryNavigationController = [AppDelegate theDelegate].secondaryNavigationController; - if (secondaryNavigationController && !secondaryNavigationController.viewControllers.count) - { - [[AppDelegate theDelegate] restoreEmptyDetailsViewController]; - } -} - -@end diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.xib b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.xib deleted file mode 100644 index 189a88ca4..000000000 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.xib +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Communities/Views/GroupInviteTableViewCell.h b/Riot/Modules/Communities/Views/GroupInviteTableViewCell.h deleted file mode 100644 index b746d6ba1..000000000 --- a/Riot/Modules/Communities/Views/GroupInviteTableViewCell.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupTableViewCell.h" - -/** - Action identifier used when the user pressed 'preview' button displayed on group invitation. - - The `userInfo` dictionary contains an `MXGroup` object under the `kGroupInviteTableViewCellRoomKey` key, representing the room of the invitation. - */ -extern NSString *const kGroupInviteTableViewCellPreviewButtonPressed; - -/** - Action identifier used when the user pressed 'decline' button displayed on group invitation. - - The `userInfo` dictionary contains an `MXGroup` object under the `kGroupInviteTableViewCellRoomKey` key, representing the room of the invitation. - */ -extern NSString *const kGroupInviteTableViewCellDeclineButtonPressed; - -/** - Notifications `userInfo` keys - */ -extern NSString *const kGroupInviteTableViewCellRoomKey; - -/** - `GroupInviteTableViewCell` instances display an invite to a group in the context of the groups list. - */ -@interface GroupInviteTableViewCell : GroupTableViewCell - -@property (weak, nonatomic) IBOutlet UIButton *leftButton; -@property (weak, nonatomic) IBOutlet UIButton *rightButton; - -@property (weak, nonatomic) IBOutlet UIView *noticeBadgeView; - - -@end diff --git a/Riot/Modules/Communities/Views/GroupInviteTableViewCell.m b/Riot/Modules/Communities/Views/GroupInviteTableViewCell.m deleted file mode 100644 index 87b965266..000000000 --- a/Riot/Modules/Communities/Views/GroupInviteTableViewCell.m +++ /dev/null @@ -1,95 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupInviteTableViewCell.h" - -#import "ThemeService.h" -#import "GeneratedInterface-Swift.h" - -#pragma mark - Constant definitions - -NSString *const kGroupInviteTableViewCellPreviewButtonPressed = @"kGroupInviteTableViewCellPreviewButtonPressed"; -NSString *const kGroupInviteTableViewCellDeclineButtonPressed = @"kGroupInviteTableViewCellDeclineButtonPressed"; - -NSString *const kGroupInviteTableViewCellRoomKey = @"kGroupInviteTableViewCellRoomKey"; - -@implementation GroupInviteTableViewCell - -#pragma mark - Class methods - -- (void)awakeFromNib -{ - [super awakeFromNib]; - - [self.leftButton.layer setCornerRadius:5]; - self.leftButton.clipsToBounds = YES; - [self.leftButton setTitle:[VectorL10n decline] forState:UIControlStateNormal]; - [self.leftButton setTitle:[VectorL10n decline] forState:UIControlStateHighlighted]; - [self.leftButton addTarget:self action:@selector(onDeclinePressed:) forControlEvents:UIControlEventTouchUpInside]; - - [self.rightButton.layer setCornerRadius:5]; - self.rightButton.clipsToBounds = YES; - [self.rightButton setTitle:[VectorL10n preview] forState:UIControlStateNormal]; - [self.rightButton setTitle:[VectorL10n preview] forState:UIControlStateHighlighted]; - [self.rightButton addTarget:self action:@selector(onPreviewPressed:) forControlEvents:UIControlEventTouchUpInside]; - - [self.noticeBadgeView.layer setCornerRadius:10]; - - self.selectionStyle = UITableViewCellSelectionStyleNone; -} - -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.leftButton.backgroundColor = ThemeService.shared.theme.tintColor; - self.rightButton.backgroundColor = ThemeService.shared.theme.tintColor; - - self.noticeBadgeView.backgroundColor = ThemeService.shared.theme.noticeColor; -} - -- (void)onDeclinePressed:(id)sender -{ - if (self.delegate) - { - MXGroup *group = groupCellData.group; - - if (group) - { - [self.delegate cell:self didRecognizeAction:kGroupInviteTableViewCellDeclineButtonPressed userInfo:@{kGroupInviteTableViewCellRoomKey:group}]; - } - } -} - -- (void)onPreviewPressed:(id)sender -{ - if (self.delegate) - { - MXGroup *group = groupCellData.group; - - if (group) - { - [self.delegate cell:self didRecognizeAction:kGroupInviteTableViewCellPreviewButtonPressed userInfo:@{kGroupInviteTableViewCellRoomKey:group}]; - } - } -} - -- (void)render:(MXKCellData *)cellData -{ - [super render:cellData]; -} - -@end diff --git a/Riot/Modules/Communities/Views/GroupInviteTableViewCell.xib b/Riot/Modules/Communities/Views/GroupInviteTableViewCell.xib deleted file mode 100644 index 09629ff56..000000000 --- a/Riot/Modules/Communities/Views/GroupInviteTableViewCell.xib +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Communities/Views/GroupTableViewCell.h b/Riot/Modules/Communities/Views/GroupTableViewCell.h deleted file mode 100644 index 61f8f6849..000000000 --- a/Riot/Modules/Communities/Views/GroupTableViewCell.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MatrixKit.h" - -/** - `GroupTableViewCell` instances display a group in the context of the groups list. - */ -@interface GroupTableViewCell : MXKGroupTableViewCell - -@property (weak, nonatomic) IBOutlet MXKImageView *groupAvatar; - -/** - The optional unread badge - */ -@property (weak, nonatomic) IBOutlet UILabel *missedNotifAndUnreadBadgeLabel; -@property (weak, nonatomic) IBOutlet UIView *missedNotifAndUnreadBadgeBgView; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *missedNotifAndUnreadBadgeBgViewWidthConstraint; - -@end diff --git a/Riot/Modules/Communities/Views/GroupTableViewCell.m b/Riot/Modules/Communities/Views/GroupTableViewCell.m deleted file mode 100644 index 029a8cb0e..000000000 --- a/Riot/Modules/Communities/Views/GroupTableViewCell.m +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupTableViewCell.h" - -#import "ThemeService.h" -#import "GeneratedInterface-Swift.h" - -#import "MXGroup+Riot.h" - -@implementation GroupTableViewCell - -#pragma mark - Class methods - -- (void)awakeFromNib -{ - [super awakeFromNib]; - - if (self.missedNotifAndUnreadBadgeBgView) - { - // Initialize unread count badge - [_missedNotifAndUnreadBadgeBgView.layer setCornerRadius:10]; - _missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 0; - } -} - -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.groupName.textColor = ThemeService.shared.theme.textPrimaryColor; - self.groupDescription.textColor = ThemeService.shared.theme.textSecondaryColor; - self.memberCount.textColor = ThemeService.shared.theme.textSecondaryColor; - - if (self.missedNotifAndUnreadBadgeLabel) - { - self.missedNotifAndUnreadBadgeLabel.textColor = ThemeService.shared.theme.baseTextPrimaryColor; - } - - self.groupAvatar.defaultBackgroundColor = [UIColor clearColor]; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - // Round image view - [_groupAvatar.layer setCornerRadius:_groupAvatar.frame.size.width / 2]; - _groupAvatar.clipsToBounds = YES; -} - -- (void)render:(MXKCellData *)cellData -{ - [super render:cellData]; - - if (self.missedNotifAndUnreadBadgeBgView) - { - // Hide by default missed notifications and unread widgets - self.missedNotifAndUnreadBadgeBgView.hidden = YES; - self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 0; - } - - if (groupCellData) - { - [groupCellData.group setGroupAvatarImageIn:self.groupAvatar matrixSession:groupCellData.groupsDataSource.mxSession]; - } -} - -// @TODO: Remove this method required by `MXKCellRendering` protocol. -// It is not used for the groups table view. -+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth -{ - // The height is fixed - // @TODO change this to support dynamic fonts - return 74; -} - -@end diff --git a/Riot/Modules/Communities/Views/GroupTableViewCell.xib b/Riot/Modules/Communities/Views/GroupTableViewCell.xib deleted file mode 100644 index 4443598b2..000000000 --- a/Riot/Modules/Communities/Views/GroupTableViewCell.xib +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift index 620d903ea..7081c2b36 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift @@ -28,7 +28,7 @@ class AllChatsActionProvider { // MARK: - RoomActionProviderProtocol var menu: UIMenu { - return UIMenu(title: VectorL10n.allChatsEditLayout, children: [ + return UIMenu(title: "", children: [ self.recentsAction, self.filtersAction, UIMenu(title: "", options: .displayInline, children: [ @@ -50,6 +50,7 @@ class AllChatsActionProvider { filters: settings.filters, sorting: settings.sorting) AllChatsLayoutSettingsManager.shared.allChatLayoutSettings = newSettings + Analytics.shared.trackInteraction(action.state == .on ? .allChatsRecentsDisabled : .allChatsRecentsEnabled) } } @@ -62,6 +63,7 @@ class AllChatsActionProvider { filters: action.state == .on ? [] : [.unreads, .favourites, .people], sorting: settings.sorting) AllChatsLayoutSettingsManager.shared.allChatLayoutSettings = newSettings + Analytics.shared.trackInteraction(action.state == .on ? .allChatsFiltersDisabled : .allChatsFiltersEnabled) } } diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift index af296a4c2..c63b6318f 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift @@ -21,10 +21,6 @@ enum AllChatsEditActionProviderOption { case exploreRooms case createRoom case startChat - case invitePeople - case spaceMembers - case spaceSettings - case leaveSpace case createSpace } @@ -56,30 +52,26 @@ class AllChatsEditActionProvider { var menu: UIMenu { guard parentSpace != nil else { var createActions = [ - self.startChatAction, - self.createRoomAction + self.createRoomAction, + self.startChatAction ] if rootSpaceCount > 0 { - createActions.append(self.createSpaceAction) + createActions.insert(self.createSpaceAction, at: 0) } - return UIMenu(title: VectorL10n.allChatsTitle, children: [ + return UIMenu(title: "", children: [ self.exploreRoomsAction, UIMenu(title: "", options: .displayInline, children: createActions) ]) } - return UIMenu(title: parentName, children: [ + return UIMenu(title: "", children: [ UIMenu(title: "", options: .displayInline, children: [ - self.spaceMembersAction, - self.exploreRoomsAction, - self.spaceSettingsAction + self.exploreRoomsAction ]), UIMenu(title: "", options: .displayInline, children: [ - self.invitePeopleAction, - self.createRoomAction, - self.createSpaceAction - ]), - self.leaveSpaceAction + self.createSpaceAction, + self.createRoomAction + ]) ]) } @@ -139,8 +131,8 @@ class AllChatsEditActionProvider { // MARK: - Private private var exploreRoomsAction: UIAction { - UIAction(title: VectorL10n.spacesExploreRooms, - image: parentSpace == nil ? UIImage(systemName: "list.bullet") : UIImage(systemName: "square.fill.text.grid.1x2")) { [weak self] action in + UIAction(title: parentSpace == nil ? VectorL10n.spacesExploreRooms : VectorL10n.spacesExploreRoomsFormat(parentName), + image: UIImage(systemName: "list.bullet")) { [weak self] action in guard let self = self else { return } self.delegate?.allChatsEditActionProvider(self, didSelect: .exploreRooms) @@ -175,42 +167,4 @@ class AllChatsEditActionProvider { self.delegate?.allChatsEditActionProvider(self, didSelect: .createSpace) } } - - private var invitePeopleAction: UIAction { - UIAction(title: VectorL10n.spacesInvitePeople, - image: UIImage(systemName: "person.badge.plus"), - attributes: isInviteAvailable ? [] : .disabled) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .invitePeople) - } - } - - private var spaceMembersAction: UIAction { - UIAction(title: VectorL10n.roomDetailsPeople, - image: UIImage(systemName: "person.3")) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .spaceMembers) - } - } - - private var spaceSettingsAction: UIAction { - UIAction(title: VectorL10n.allChatsEditMenuSpaceSettings, - image: UIImage(systemName: "gearshape")) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .spaceSettings) - } - } - - private var leaveSpaceAction: UIAction { - UIAction(title: VectorL10n.allChatsEditMenuLeaveSpace(parentName), - image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"), - attributes: .destructive) { [weak self] action in - guard let self = self else { return } - - self.delegate?.allChatsEditActionProvider(self, didSelect: .leaveSpace) - } - } } diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift new file mode 100644 index 000000000..0d9e3d2fa --- /dev/null +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift @@ -0,0 +1,141 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import MatrixSDK + +enum AllChatsSpaceActionProviderOption { + case invitePeople + case spaceMembers + case spaceSettings + case leaveSpace +} + +protocol AllChatsSpaceActionProviderDelegate: AnyObject { + func allChatsSpaceActionProvider(_ actionProvider: AllChatsSpaceActionProvider, didSelect option: AllChatsSpaceActionProviderOption) +} + +/// `AllChatsSpaceActionProvider` provides the menu for accessing space options according to the current space +class AllChatsSpaceActionProvider { + + // MARK: - Properties + + weak var delegate: AllChatsSpaceActionProviderDelegate? + + // MARK: - Private + + private var currentSpace: MXSpace? { + didSet { + spaceName = currentSpace?.summary?.displayname ?? VectorL10n.spaceTag + } + } + private var spaceName: String = VectorL10n.spaceTag + private var isInviteAvailable: Bool = false + + // MARK: - RoomActionProviderProtocol + + var menu: UIMenu { + guard currentSpace != nil else { + return UIMenu(title: "", children: []) + } + + return UIMenu(title: "", children: [ + UIMenu(title: "", options: .displayInline, children: [ + self.spaceSettingsAction, + self.spaceMembersAction, + self.invitePeopleAction + ]), + self.leaveSpaceAction + ]) + } + + // MARK: - Public + + /// Returns an instance of the updated menu accordingly to the given parameters. + /// + /// Some menu items can be disabled depending on the required power levels of the `parentSpace`. Therefore, `updateMenu()` first returns a temporary context menu + /// with all sensible items disabled, asynchronously fetches power levels of the `parentSpace`, then gives a new instance of the menu with, potentially, all sensible items + /// enabled via the `completion` callback. + /// + /// - Parameters: + /// - session: The current `MXSession` instance + /// - space: The current space (`nil` for home space) + /// - completion: callback called once the power levels of the `parentSpace` have been fetched and the menu items have been computed accordingly. + /// - Returns: If the `parentSpace` is `nil`, the context menu, the temporary context menu otherwise. + func updateMenu(with session: MXSession?, space: MXSpace?, completion: @escaping (UIMenu) -> Void) -> UIMenu { + self.currentSpace = space + isInviteAvailable = false + + guard let currentSpace = currentSpace, let spaceRoom = currentSpace.room, let session = session else { + return self.menu + } + + spaceRoom.state { [weak self] roomState in + guard let self = self else { return } + + guard let powerLevels = roomState?.powerLevels, let userId = session.myUserId else { + return + } + let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: userId) + + self.isInviteAvailable = userPowerLevel >= powerLevels.invite + + completion(self.menu) + } + + return self.menu + } + + // MARK: - Private + + private var invitePeopleAction: UIAction { + UIAction(title: VectorL10n.inviteTo(spaceName), + image: UIImage(systemName: "square.and.arrow.up"), + attributes: isInviteAvailable ? [] : .disabled) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .invitePeople) + } + } + + private var spaceMembersAction: UIAction { + UIAction(title: VectorL10n.roomDetailsPeople, + image: UIImage(systemName: "person")) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .spaceMembers) + } + } + + private var spaceSettingsAction: UIAction { + UIAction(title: VectorL10n.allChatsEditMenuSpaceSettings, + image: UIImage(systemName: "gearshape")) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .spaceSettings) + } + } + + private var leaveSpaceAction: UIAction { + UIAction(title: VectorL10n.allChatsEditMenuLeaveSpace(spaceName), + image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"), + attributes: .destructive) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsSpaceActionProvider(self, didSelect: .leaveSpace) + } + } +} diff --git a/Riot/Modules/Home/AllChats/AllChatsFilterOptions.swift b/Riot/Modules/Home/AllChats/AllChatsFilterOptions.swift index b79aa59c1..14c63b79f 100644 --- a/Riot/Modules/Home/AllChats/AllChatsFilterOptions.swift +++ b/Riot/Modules/Home/AllChats/AllChatsFilterOptions.swift @@ -37,10 +37,22 @@ class AllChatsFilterOptions: NSObject { filterOptionListView.selectedOptionType = AllChatsLayoutSettingsManager.shared.activeFilters filterOptionListView.selectionChanged = { filter in guard filter != .all else { + Analytics.shared.trackInteraction(.allChatsFilterAll) AllChatsLayoutSettingsManager.shared.activeFilters = [] return } - + + switch filter { + case .all: + Analytics.shared.trackInteraction(.allChatsFilterAll) + case .favourites: + Analytics.shared.trackInteraction(.allChatsFilterFavourites) + case .people: + Analytics.shared.trackInteraction(.allChatsFilterPeople) + case .unreads: + Analytics.shared.trackInteraction(.allChatsFilterUnreads) + default: break + } AllChatsLayoutSettingsManager.shared.activeFilters = filter } diff --git a/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift b/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift index de55c71a5..065c54cb3 100644 --- a/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift +++ b/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift @@ -61,6 +61,8 @@ final class AllChatsLayoutSettingsManager: NSObject { set { RiotSettings.defaults.set(newValue.rawValue, forKey: Constants.activeFiltersKey) + track(activeFilters: newValue) + DispatchQueue.main.async { NotificationCenter.default.post(name: AllChatsLayoutSettingsManager.didUpdateActiveFilters, object: self) } @@ -84,6 +86,12 @@ final class AllChatsLayoutSettingsManager: NSObject { NotificationCenter.default.post(name: AllChatsLayoutSettingsManager.willUpdateSettings, object: self) } + if newValue.filters.isEmpty { + track(activeFilters: nil) + } else { + track(activeFilters: activeFilters) + } + guard let data = try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false) else { MXLog.warning("[AllChatsLayoutSettingsManager] set allChatLayoutSettings: failed to archive settings") return @@ -96,4 +104,26 @@ final class AllChatsLayoutSettingsManager: NSObject { } } } + + // MARK: - Private + + private func track(activeFilters: AllChatsLayoutFilterType?) { + guard let activeFilters = activeFilters else { + Analytics.shared.updateUserProperties(allChatsActiveFilter: nil) + return + } + + switch activeFilters { + case [], .all: + Analytics.shared.updateUserProperties(allChatsActiveFilter: .all) + case .unreads: + Analytics.shared.updateUserProperties(allChatsActiveFilter: .unreads) + case .favourites: + Analytics.shared.updateUserProperties(allChatsActiveFilter: .favourites) + case .people: + Analytics.shared.updateUserProperties(allChatsActiveFilter: .people) + default: + Analytics.shared.updateUserProperties(allChatsActiveFilter: nil) + } + } } diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index cf749e649..bad683d38 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -37,6 +37,8 @@ class AllChatsViewController: HomeViewController { private let searchController = UISearchController(searchResultsController: nil) + private let spaceActionProvider = AllChatsSpaceActionProvider() + private let editActionProvider = AllChatsEditActionProvider() private var spaceSelectorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter? @@ -49,17 +51,19 @@ class AllChatsViewController: HomeViewController { super.viewDidLoad() editActionProvider.delegate = self + spaceActionProvider.delegate = self recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue recentsTableView.clipsToBounds = false - + recentsTableView.register(RecentEmptySectionTableViewCell.nib, forCellReuseIdentifier: RecentEmptySectionTableViewCell.reuseIdentifier) + recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier) + updateUI() vc_setLargeTitleDisplayMode(.automatic) searchController.obscuresBackgroundDuringPresentation = false searchController.searchResultsUpdater = self - self.setupEditOptions() NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil) } @@ -117,6 +121,7 @@ class AllChatsViewController: HomeViewController { // MARK: - Actions @objc private func showSpaceSelectorAction(sender: AnyObject) { + Analytics.shared.viewRoomTrigger = .roomList let currentSpaceId = self.dataSource.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId let spaceSelectorBridgePresenter = SpaceSelectorBottomSheetCoordinatorBridgePresenter(session: self.mainSession, selectedSpaceId: currentSpaceId, showHomeSpace: true) spaceSelectorBridgePresenter.present(from: self, animated: true) @@ -124,6 +129,51 @@ class AllChatsViewController: HomeViewController { self.spaceSelectorBridgePresenter = spaceSelectorBridgePresenter } + // MARK: - UITableViewDataSource + + private func sectionType(forSectionAt index: Int) -> RecentsDataSourceSectionType? { + guard let recentsDataSource = dataSource as? RecentsDataSource else { + return nil + } + + return recentsDataSource.sections.sectionType(forSectionIndex: index) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let sectionType = sectionType(forSectionAt: section), sectionType == .invites else { + return super.tableView(tableView, numberOfRowsInSection: section) + } + + return dataSource.tableView(tableView, numberOfRowsInSection: section) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else { + return super.tableView(tableView, cellForRowAt: indexPath) + } + + return dataSource.tableView(tableView, cellForRowAt: indexPath) + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else { + return super.tableView(tableView, heightForRowAt: indexPath) + } + + return dataSource.cellHeight(at: indexPath) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else { + super.tableView(tableView, didSelectRowAt: indexPath) + return + } + + showRoomInviteList() + } + // MARK: - Toolbar animation private var lastScrollPosition: Double = 0 @@ -152,6 +202,57 @@ class AllChatsViewController: HomeViewController { lastScrollPosition = scrollPosition } + // MARK: - Empty view management + + override func updateEmptyView() { + guard let mainSession = self.mainSession else { + return + } + + let title: String + let informationText: String + if let currentSpace = self.dataSource?.currentSpace { + title = VectorL10n.allChatsEmptyViewTitle(currentSpace.summary?.displayname ?? VectorL10n.spaceTag) + informationText = VectorL10n.allChatsEmptySpaceInformation + } else { + let myUser = mainSession.myUser + let displayName = (myUser?.displayName ?? myUser?.userId) ?? "" + let appName = AppInfo.current.displayName + title = VectorL10n.homeEmptyViewTitle(appName, displayName) + informationText = VectorL10n.allChatsEmptyViewInformation + } + + self.emptyView?.fill(with: emptyViewArtwork, + title: title, + informationText: informationText, + displayMode: self.dataSource?.currentSpace == nil ? .default : .icon) + } + + private var emptyViewArtwork: UIImage { + if self.dataSource?.currentSpace == nil { + return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.peopleEmptyScreenArtworkDark.image : Asset.Images.peopleEmptyScreenArtwork.image + } else { + return Asset.Images.allChatsEditIcon.image + } + } + + override func shouldShowEmptyView() -> Bool { + let shouldShowEmptyView = super.shouldShowEmptyView() + + if shouldShowEmptyView { + self.tabBarController?.navigationItem.searchController = nil + navigationItem.largeTitleDisplayMode = .never + navigationController?.navigationBar.prefersLargeTitles = false + } else { + self.tabBarController?.navigationItem.searchController = searchController + navigationItem.largeTitleDisplayMode = .automatic + navigationController?.navigationBar.prefersLargeTitles = true + } + + return shouldShowEmptyView + } + + // MARK: - Theme management override func userInterfaceThemeDidChange() { @@ -171,26 +272,38 @@ class AllChatsViewController: HomeViewController { // MARK: - Private @objc private func setupEditOptions() { - self.tabBarController?.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: AllChatsActionProvider().menu) + guard let currentSpace = self.dataSource?.currentSpace else { + updateRightNavigationItem(with: AllChatsActionProvider().menu) + return + } + + updateRightNavigationItem(with: spaceActionProvider.updateMenu(with: mainSession, space: currentSpace) { [weak self] menu in + self?.updateRightNavigationItem(with: menu) + }) } private func updateUI() { let currentSpace = self.dataSource?.currentSpace self.tabBarController?.title = currentSpace?.summary?.displayname ?? VectorL10n.allChatsTitle + setupEditOptions() updateToolbar(with: editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in self?.updateToolbar(with: menu) })) + updateEmptyView() + } + + private func updateRightNavigationItem(with menu: UIMenu) { + self.tabBarController?.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: menu) } private func updateToolbar(with menu: UIMenu) { - let currentSpace = self.dataSource?.currentSpace self.navigationController?.isToolbarHidden = false self.update(with: ThemeService.shared().theme) self.tabBarController?.setToolbarItems([ - UIBarButtonItem(image: Asset.Images.homeMySpacesAction.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))), + UIBarButtonItem(image: Asset.Images.allChatsSpacesIcon.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))), UIBarButtonItem.flexibleSpace(), - UIBarButtonItem(image: UIImage(systemName: currentSpace == nil ? "square.and.pencil" : "ellipsis.circle"), menu: menu) + UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) ], animated: true) } @@ -314,12 +427,23 @@ class AllChatsViewController: HomeViewController { coordinator.start() add(childCoordinator: coordinator) coordinator.completion = { [weak self] result in + // switching to home space + self?.switchSpace(withId: nil) coordinator.toPresentable().dismiss(animated: true) { self?.remove(childCoordinator: coordinator) } } present(coordinator.toPresentable(), animated: true) } + + private func showRoomInviteList() { + let invitesViewController = RoomInvitesViewController.instantiate() + invitesViewController.userIndicatorStore = self.userIndicatorStore + let recentsListService = RecentsListService(withSession: mainSession) + let recentsDataSource = RecentsDataSource(matrixSession: mainSession, recentsListService: recentsListService) + invitesViewController.displayList(recentsDataSource) + self.navigationController?.pushViewController(invitesViewController, animated: true) + } } // MARK: - SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate @@ -393,6 +517,17 @@ extension AllChatsViewController: AllChatsEditActionProviderDelegate { createNewRoom() case .startChat: startChat() + case .createSpace: + showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId) + } + } + +} + +// MARK: - AllChatsSpaceActionProviderDelegate +extension AllChatsViewController: AllChatsSpaceActionProviderDelegate { + func allChatsSpaceActionProvider(_ actionProvider: AllChatsSpaceActionProvider, didSelect option: AllChatsSpaceActionProviderOption) { + switch option { case .invitePeople: showSpaceInvite() case .spaceMembers: @@ -401,11 +536,8 @@ extension AllChatsViewController: AllChatsEditActionProviderDelegate { showSpaceSettings() case .leaveSpace: showLeaveSpace() - case .createSpace: - showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId) } } - } // MARK: - ContactsPickerCoordinatorDelegate diff --git a/Riot/Modules/Home/AllChats/RoomInvitesViewController.swift b/Riot/Modules/Home/AllChats/RoomInvitesViewController.swift new file mode 100644 index 000000000..ead8dc802 --- /dev/null +++ b/Riot/Modules/Home/AllChats/RoomInvitesViewController.swift @@ -0,0 +1,120 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class RoomInvitesViewController: RecentsViewController { + + // MARK: - Class methods + + static override func nib() -> UINib! { + return UINib(nibName: String(describing: self), bundle: Bundle(for: self.classForCoder())) + } + + static func instantiate() -> Self { + let storyboard = UIStoryboard(name: "Main", bundle: .main) + guard let viewController = storyboard.instantiateViewController(withIdentifier: "RoomInvitesViewController") as? Self else { + fatalError("No view controller of type \(self) in the main storyboard") + } + return viewController + } + + // MARK: - Private + + private var recentsDataSource: RecentsDataSource? + private var tableViewPaginationThrottler: MXThrottler! + + // MARK: - Lifecycle + + override func awakeFromNib() { + super.awakeFromNib() + self.enableSearchBar = false + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.recentsTableView.clipsToBounds = false + self.recentsTableView.tag = RecentsDataSourceMode.roomInvites.rawValue + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + guard let recentsDataSource = self.dataSource as? RecentsDataSource else { + return + } + + self.recentsDataSource = recentsDataSource + + if recentsDataSource.recentsDataSourceMode != .roomInvites { + recentsDataSource.setDelegate(self, andRecentsDataSourceMode: .roomInvites) + recentsDataSource.search(withPatterns: nil) + recentsSearchBar?.text = nil + } + } + + // MARK: - RecentsViewController + + override func finalizeInit() { + super.finalizeInit() + + title = VectorL10n.roomRecentsInvitesSection.capitalized + self.screenTracker = AnalyticsScreenTracker(screen: .invites) + tableViewPaginationThrottler = MXThrottler(minimumDelay: 0.1) + } + + override func refreshCurrentSelectedCell(_ forceVisible: Bool) { + // Check whether the recents data source is correctly configured. + guard self.recentsDataSource?.recentsDataSourceMode == .roomInvites else { + return + } + + super.refreshCurrentSelectedCell(forceVisible) + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 0 + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + super.tableView(tableView, willDisplay: cell, forRowAt: indexPath) + + tableViewPaginationThrottler .throttle { [weak self] in + guard let self = self, tableView.numberOfSections > indexPath.section else { + return + } + + let numberOfRowsInSection = tableView.numberOfRows(inSection: indexPath.section) + if indexPath.row == numberOfRowsInSection - 1 { + self.recentsDataSource?.paginate(inSection: indexPath.section) + } + } + } + + // MARK: - Empty view management + + override func updateEmptyView() { + let image = UIImage(systemName: "envelope.open.fill") ?? UIImage() + emptyView?.fill(with: image, + title: VectorL10n.roomInvitesEmptyViewTitle, + informationText: VectorL10n.roomInvitesEmptyViewInformation, + displayMode: .icon) + } + +} diff --git a/Riot/Modules/Home/AllChats/RoomInvitesViewController.xib b/Riot/Modules/Home/AllChats/RoomInvitesViewController.xib new file mode 100644 index 000000000..0dbb0ef8a --- /dev/null +++ b/Riot/Modules/Home/AllChats/RoomInvitesViewController.xib @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift index e12cb2e69..d7d0a9a31 100644 --- a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift +++ b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift @@ -84,7 +84,7 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType { let coordinator: Coordinator & Presentable // Check if a passphrase has been set for given backup - if let megolmBackupAuthData = MXMegolmBackupAuthData(fromJSON: self.keyBackupVersion.authData), megolmBackupAuthData.privateKeySalt != nil { + if self.keyBackupVersion.authData["private_key_salt"] != nil { coordinator = self.createRecoverFromPassphraseCoordinator() } else { coordinator = self.createRecoverFromRecoveryKeyCoordinator() diff --git a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift index c14c56272..03171ebd2 100644 --- a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift +++ b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift @@ -134,7 +134,7 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType { return } - keyBackup.prepareKeyBackupVersion(withPassword: nil, success: { megolmBackupCreationInfo in + keyBackup.prepareKeyBackupVersion(withPassword: nil, algorithm: nil, success: { megolmBackupCreationInfo in keyBackup.createKeyBackupVersion(megolmBackupCreationInfo, success: { _ in recoveryService.updateRecovery(forSecrets: [MXSecretId.keyBackup.takeUnretainedValue() as String], withPrivateKey: privateKey) { completion(.success(Void())) diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModel.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModel.swift index ea2fd4752..b2dd9163c 100644 --- a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModel.swift +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModel.swift @@ -96,7 +96,7 @@ final class KeyBackupSetupPassphraseViewModel: KeyBackupSetupPassphraseViewModel self.update(viewState: .loading) - self.keyBackup.prepareKeyBackupVersion(withPassword: passphrase, success: { [weak self] (megolmBackupCreationInfo) in + self.keyBackup.prepareKeyBackupVersion(withPassword: passphrase, algorithm: nil, success: { [weak self] (megolmBackupCreationInfo) in guard let sself = self else { return } @@ -122,7 +122,7 @@ final class KeyBackupSetupPassphraseViewModel: KeyBackupSetupPassphraseViewModel private func setupRecoveryKey() { self.update(viewState: .loading) - self.keyBackup.prepareKeyBackupVersion(withPassword: nil, success: { [weak self] (megolmBackupCreationInfo) in + self.keyBackup.prepareKeyBackupVersion(withPassword: nil, algorithm: nil, success: { [weak self] (megolmBackupCreationInfo) in guard let sself = self else { return } diff --git a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift index c56d0fc0e..4c47e9e18 100644 --- a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift +++ b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift @@ -219,7 +219,7 @@ final class KeyVerificationVerifyByScanningViewController: UIViewController { do { return try codeGenerator.generateCode(from: data, with: codeImageView.frame.size) } catch { - MXLog.error("[KeyVerificationVerifyByScanningViewController] qrCodeImage: cannot generate QR code - \(error)") + MXLog.error("[KeyVerificationVerifyByScanningViewController] qrCodeImage: cannot generate QR code", context: error) return nil } } diff --git a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift index de4b9e189..d0d900290 100644 --- a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift +++ b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift @@ -173,7 +173,7 @@ final class KeyVerificationVerifyByScanningViewModel: KeyVerificationVerifyBySca // Remove pending QR code transaction, as we are going to use SAS verification self.removePendingQRCodeTransaction() - if keyVerificationTransaction is MXOutgoingSASTransaction == false { + if keyVerificationTransaction is MXSASTransaction == false || keyVerificationTransaction.isIncoming { MXLog.debug("[KeyVerificationVerifyByScanningViewModel] SAS transaction should be outgoing") self.unregisterTransactionDidStateChangeNotification() self.update(viewState: .error(KeyVerificationVerifyByScanningViewModelError.unknown)) diff --git a/Riot/Modules/KeyVerification/User/Start/UserVerificationStartViewModel.swift b/Riot/Modules/KeyVerification/User/Start/UserVerificationStartViewModel.swift index c49576c64..77a18de7c 100644 --- a/Riot/Modules/KeyVerification/User/Start/UserVerificationStartViewModel.swift +++ b/Riot/Modules/KeyVerification/User/Start/UserVerificationStartViewModel.swift @@ -122,7 +122,7 @@ final class UserVerificationStartViewModel: UserVerificationStartViewModelType { } @objc private func keyVerificationRequestDidChange(notification: Notification) { - guard let keyVerificationRequest = notification.object as? MXKeyVerificationByDMRequest else { + guard let keyVerificationRequest = notification.object as? MXKeyVerificationRequest else { return } diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index fb568f5af..5fb222493 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -214,6 +214,6 @@ extension LocationManager: CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - MXLog.error("[LocationManager] Did failed with error :\(error)") + MXLog.error("[LocationManager] Did failed", context: error) } } diff --git a/Riot/Modules/LocationSharing/UserLocationService.swift b/Riot/Modules/LocationSharing/UserLocationService.swift index 1a2abc0a5..40e9a4963 100644 --- a/Riot/Modules/LocationSharing/UserLocationService.swift +++ b/Riot/Modules/LocationSharing/UserLocationService.swift @@ -217,7 +217,7 @@ class UserLocationService: UserLocationServiceProtocol { case .success: break case .failure(let error): - MXLog.error("Fail to send location with error \(error)") + MXLog.error("Fail to send location", context: error) } } } diff --git a/Riot/Modules/MatrixKit/Controllers/MXKAuthenticationViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKAuthenticationViewController.m index 1eb9b5d66..1ec7972a0 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKAuthenticationViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKAuthenticationViewController.m @@ -1542,12 +1542,16 @@ if (retry) { - MXLogError(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device rehydration failed due to error: %@. Retrying", error); + MXLogErrorDetails(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device rehydration failed due to error: Retrying", @{ + @"error": error ?: @"unknown" + }); [self attemptDeviceRehydrationWithKeyData:keyData credentials:credentials retry:NO]; return; } - MXLogError(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device rehydration failed due to error: %@", error); + MXLogErrorDetails(@"[MXKAuthenticationViewController] attemptDeviceRehydration: device rehydration failed due to error", @{ + @"error": error ?: @"unknown" + }); [self _createAccountWithCredentials:credentials]; }]; diff --git a/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.h b/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.h deleted file mode 100644 index 020f78549..000000000 --- a/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.h +++ /dev/null @@ -1,118 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import - -#import "MXKViewController.h" -#import "MXKSessionGroupsDataSource.h" - -@class MXKGroupListViewController; - -/** - `MXKGroupListViewController` delegate. - */ -@protocol MXKGroupListViewControllerDelegate - -/** - Tells the delegate that the user selected a group. - - @param groupListViewController the `MXKGroupListViewController` instance. - @param group the selected group. - @param mxSession the matrix session in which the group is defined. - */ -- (void)groupListViewController:(MXKGroupListViewController *)groupListViewController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)mxSession; - -@end - - -/** - This view controller displays a group list. - */ -@interface MXKGroupListViewController : MXKViewController -{ -@protected - - /** - The fake top view displayed in case of vertical bounce. - */ - __weak UIView *topview; -} - -@property (weak, nonatomic) IBOutlet UISearchBar *groupsSearchBar; -@property (weak, nonatomic) IBOutlet UITableView *groupsTableView; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *groupsSearchBarTopConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *groupsSearchBarHeightConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *groupsTableViewBottomConstraint; - -/** - The current data source associated to the view controller. - */ -@property (nonatomic, readonly) MXKSessionGroupsDataSource *dataSource; - -/** - The delegate for the view controller. - */ -@property (nonatomic, weak) id delegate; - -/** - Enable the search option by adding a navigation item in the navigation bar (YES by default). - Set NO this property to disable this option and hide the related bar button. - */ -@property (nonatomic) BOOL enableBarButtonSearch; - -#pragma mark - Class methods - -/** - Returns the `UINib` object initialized for a `MXKGroupListViewController`. - - @return The initialized `UINib` object or `nil` if there were errors during initialization - or the nib file could not be located. - - @discussion You may override this method to provide a customized nib. If you do, - you should also override `groupListViewController` to return your - view controller loaded from your custom nib. - */ -+ (UINib *)nib; - -/** - Creates and returns a new `MXKGroupListViewController` object. - - @discussion This is the designated initializer for programmatic instantiation. - @return An initialized `MXKGroupListViewController` object if successful, `nil` otherwise. - */ -+ (instancetype)groupListViewController; - -/** - Display the groups described in the provided data source. - - Note: The provided data source will replace the current data source if any. The caller - should dispose properly this data source if it is not used anymore. - - @param listDataSource the data source providing the groups list. - */ -- (void)displayList:(MXKSessionGroupsDataSource*)listDataSource; - -/** - Refresh the groups table display. - */ -- (void)refreshGroupsTable; - -/** - Hide/show the search bar at the top of the groups table view. - */ -- (void)hideSearchBar:(BOOL)hidden; - -@end diff --git a/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.m deleted file mode 100644 index b23a55720..000000000 --- a/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.m +++ /dev/null @@ -1,614 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXKGroupListViewController.h" - -#import "MXKGroupTableViewCell.h" -#import "MXKTableViewHeaderFooterWithLabel.h" - -@interface MXKGroupListViewController () -{ - /** - The data source providing UITableViewCells - */ - MXKSessionGroupsDataSource *dataSource; - - /** - Search handling - */ - UIBarButtonItem *searchButton; - BOOL ignoreSearchRequest; - - /** - The reconnection animated view. - */ - UIView* reconnectingView; - - /** - The current table view header if any. - */ - UIView* tableViewHeaderView; - - /** - The latest server sync date - */ - NSDate* latestServerSync; - - /** - The restart the event connnection - */ - BOOL restartConnection; -} - -@end - -@implementation MXKGroupListViewController -@synthesize dataSource; - -#pragma mark - Class methods - -+ (UINib *)nib -{ - return [UINib nibWithNibName:NSStringFromClass([MXKGroupListViewController class]) - bundle:[NSBundle bundleForClass:[MXKGroupListViewController class]]]; -} - -+ (instancetype)groupListViewController -{ - return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKGroupListViewController class]) - bundle:[NSBundle bundleForClass:[MXKGroupListViewController class]]]; -} - -#pragma mark - - -- (void)finalizeInit -{ - [super finalizeInit]; - - _enableBarButtonSearch = YES; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - // Check whether the view controller has been pushed via storyboard - if (!_groupsTableView) - { - // Instantiate view controller objects - [[[self class] nib] instantiateWithOwner:self options:nil]; - } - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated" - // Adjust search bar Top constraint to take into account potential navBar. - if (_groupsSearchBarTopConstraint) - { - [NSLayoutConstraint deactivateConstraints:@[_groupsSearchBarTopConstraint]]; - - _groupsSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.groupsSearchBar - attribute:NSLayoutAttributeTop - multiplier:1.0f - constant:0.0f]; - - [NSLayoutConstraint activateConstraints:@[_groupsSearchBarTopConstraint]]; - } - - // Adjust table view Bottom constraint to take into account tabBar. - if (_groupsTableViewBottomConstraint) - { - [NSLayoutConstraint deactivateConstraints:@[_groupsTableViewBottomConstraint]]; - - _groupsTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.groupsTableView - attribute:NSLayoutAttributeBottom - multiplier:1.0f - constant:0.0f]; - - [NSLayoutConstraint activateConstraints:@[_groupsTableViewBottomConstraint]]; - } - #pragma clang diagnostic pop - - // Hide search bar by default - [self hideSearchBar:YES]; - - // Apply search option in navigation bar - self.enableBarButtonSearch = _enableBarButtonSearch; - - // Add an accessory view to the search bar in order to retrieve keyboard view. - self.groupsSearchBar.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero]; - - // Finalize table view configuration - // Note: self-sizing cells and self-sizing section headers are enabled from the nib file. - self.groupsTableView.delegate = self; - self.groupsTableView.dataSource = dataSource; // Note: dataSource may be nil here - self.groupsTableView.estimatedSectionHeaderHeight = 30; // The value set in the nib seems not available for iOS version < 10. - - // Set up classes to use for the cells and the section headers. - [self.groupsTableView registerNib:MXKGroupTableViewCell.nib forCellReuseIdentifier:MXKGroupTableViewCell.defaultReuseIdentifier]; - [self.groupsTableView registerNib:MXKTableViewHeaderFooterWithLabel.nib forHeaderFooterViewReuseIdentifier:MXKTableViewHeaderFooterWithLabel.defaultReuseIdentifier]; - - // Add a top view which will be displayed in case of vertical bounce. - CGFloat height = self.groupsTableView.frame.size.height; - UIView *topview = [[UIView alloc] initWithFrame:CGRectMake(0,-height,self.groupsTableView.frame.size.width,height)]; - topview.autoresizingMask = UIViewAutoresizingFlexibleWidth; - topview.backgroundColor = [UIColor groupTableViewBackgroundColor]; - [self.groupsTableView addSubview:topview]; - self->topview = topview; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Restore search mechanism (if enabled) - ignoreSearchRequest = NO; - - // Observe the server sync - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSyncNotification) name:kMXSessionDidSyncNotification object:nil]; - - // Do a full reload - [self refreshGroupsTable]; - - // Refresh all groups summary - [self.dataSource refreshGroupsSummary:nil]; -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - // The user may still press search button whereas the view disappears - ignoreSearchRequest = YES; - - // Leave potential search session - if (!self.groupsSearchBar.isHidden) - { - [self searchBarCancelButtonClicked:self.groupsSearchBar]; - } - - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidSyncNotification object:nil]; - - [self removeReconnectingView]; -} - -- (void)dealloc -{ - self.groupsSearchBar.inputAccessoryView = nil; - - searchButton = nil; -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - - // Dispose of any resources that can be recreated. -} - -#pragma mark - Override MXKViewController - -- (void)onKeyboardShowAnimationComplete -{ - // Report the keyboard view in order to track keyboard frame changes - self.keyboardView = _groupsSearchBar.inputAccessoryView.superview; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated" -- (void)setKeyboardHeight:(CGFloat)keyboardHeight -{ - // Deduce the bottom constraint for the table view (Don't forget the potential tabBar) - CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length; - // Check whether the keyboard is over the tabBar - if (tableViewBottomConst < 0) - { - tableViewBottomConst = 0; - } - - // Update constraints - _groupsTableViewBottomConstraint.constant = tableViewBottomConst; - - // Force layout immediately to take into account new constraint - [self.view layoutIfNeeded]; -} -#pragma clang diagnostic pop - -- (void)destroy -{ - self.groupsTableView.dataSource = nil; - self.groupsTableView.delegate = nil; - self.groupsTableView = nil; - - dataSource.delegate = nil; - dataSource = nil; - - _delegate = nil; - - [topview removeFromSuperview]; - topview = nil; - - [super destroy]; -} - -#pragma mark - - -- (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch -{ - _enableBarButtonSearch = enableBarButtonSearch; - - if (enableBarButtonSearch) - { - if (!searchButton) - { - searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)]; - } - - // Add it in right bar items - NSArray *rightBarButtonItems = self.navigationItem.rightBarButtonItems; - self.navigationItem.rightBarButtonItems = rightBarButtonItems ? [rightBarButtonItems arrayByAddingObject:searchButton] : @[searchButton]; - } - else - { - NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray: self.navigationItem.rightBarButtonItems]; - [rightBarButtonItems removeObject:searchButton]; - self.navigationItem.rightBarButtonItems = rightBarButtonItems; - } -} - -- (void)displayList:(MXKSessionGroupsDataSource *)listDataSource -{ - // Cancel registration on existing dataSource if any - if (dataSource) - { - dataSource.delegate = nil; - - // Remove associated matrix sessions - NSArray *mxSessions = self.mxSessions; - for (MXSession *mxSession in mxSessions) - { - [self removeMatrixSession:mxSession]; - } - } - - dataSource = listDataSource; - dataSource.delegate = self; - - // Report the matrix session at view controller level to update UI according to session state - [self addMatrixSession:listDataSource.mxSession]; - - if (self.groupsTableView) - { - // Set up table data source - self.groupsTableView.dataSource = dataSource; - } -} - -- (void)refreshGroupsTable -{ - // For now, do a simple full reload - [self.groupsTableView reloadData]; -} - -- (void)hideSearchBar:(BOOL)hidden -{ - self.groupsSearchBar.hidden = hidden; - self.groupsSearchBarHeightConstraint.constant = hidden ? 0 : 44; - [self.view setNeedsUpdateConstraints]; -} - -#pragma mark - Action - -- (IBAction)search:(id)sender -{ - // The user may have pressed search button whereas the view controller was disappearing - if (ignoreSearchRequest) - { - return; - } - - if (self.groupsSearchBar.isHidden) - { - // Check whether there are data in which search - if ([self.dataSource numberOfSectionsInTableView:self.groupsTableView]) - { - [self hideSearchBar:NO]; - - // Create search bar - [self.groupsSearchBar becomeFirstResponder]; - } - } - else - { - [self searchBarCancelButtonClicked: self.groupsSearchBar]; - } -} - -#pragma mark - MXKDataSourceDelegate - -- (Class)cellViewClassForCellData:(MXKCellData*)cellData -{ - // Return the default group table view cell - return MXKGroupTableViewCell.class; -} - -- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData -{ - // Return the default group table view cell - return MXKGroupTableViewCell.defaultReuseIdentifier; -} - -- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes -{ - // For now, do a simple full reload - [self refreshGroupsTable]; -} - -- (void)dataSource:(MXKDataSource *)dataSource didAddMatrixSession:(MXSession *)mxSession -{ - [self addMatrixSession:mxSession]; -} - -- (void)dataSource:(MXKDataSource *)dataSource didRemoveMatrixSession:(MXSession *)mxSession -{ - [self removeMatrixSession:mxSession]; -} - -#pragma mark - UITableView delegate - -- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - return tableView.estimatedRowHeight; -} - -- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section -{ - if (tableView.numberOfSections > 1) - { - return tableView.estimatedSectionHeaderHeight; - } - - return 0; -} - -- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath -{ - // Refresh here the estimated row height - tableView.estimatedRowHeight = cell.frame.size.height; -} - -- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(nonnull UIView *)view forSection:(NSInteger)section -{ - // Refresh here the estimated header height - tableView.estimatedSectionHeaderHeight = view.frame.size.height; -} - -- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section -{ - MXKTableViewHeaderFooterWithLabel *sectionHeader; - - if (tableView.numberOfSections > 1) - { - sectionHeader = [tableView dequeueReusableHeaderFooterViewWithIdentifier:MXKTableViewHeaderFooterWithLabel.defaultReuseIdentifier]; - - sectionHeader.mxkLabel.text = [self.dataSource tableView:tableView titleForHeaderInSection:section]; - } - - return sectionHeader; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (_delegate) - { - UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath]; - - if ([selectedCell conformsToProtocol:@protocol(MXKCellRendering)]) - { - id cell = (id)selectedCell; - - if ([cell respondsToSelector:@selector(renderedCellData)]) - { - MXKCellData *cellData = cell.renderedCellData; - if ([cellData conformsToProtocol:@protocol(MXKGroupCellDataStoring)]) - { - id groupCellData = (id)cellData; - [_delegate groupListViewController:self didSelectGroup:groupCellData.group inMatrixSession:self.mainSession]; - } - } - } - } - - // Hide the keyboard when user select a room - // do not hide the searchBar until the view controller disappear - // on tablets / iphone 6+, the user could expect to search again while looking at a room - [self.groupsSearchBar resignFirstResponder]; -} - -- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath -{ - // Release here resources, and restore reusable cells - if ([cell respondsToSelector:@selector(didEndDisplay)]) - { - [(id)cell didEndDisplay]; - } -} - -- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset -{ - // Detect vertical bounce at the top of the tableview to trigger reconnection. - if (scrollView == _groupsTableView) - { - [self detectPullToKick:scrollView]; - } -} - -- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView -{ - if (scrollView == _groupsTableView) - { - [self managePullToKick:scrollView]; - } -} - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView -{ - if (scrollView == _groupsTableView) - { - if (scrollView.contentOffset.y + scrollView.adjustedContentInset.top == 0) - { - [self managePullToKick:scrollView]; - } - } -} - -#pragma mark - UISearchBarDelegate - -- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText -{ - // Apply filter - if (searchText.length) - { - [self.dataSource searchWithPatterns:@[searchText]]; - } - else - { - [self.dataSource searchWithPatterns:nil]; - } -} - -- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar -{ - // "Done" key has been pressed - [searchBar resignFirstResponder]; -} - -- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar -{ - // Leave search - [searchBar resignFirstResponder]; - - [self hideSearchBar:YES]; - - self.groupsSearchBar.text = nil; - - // Refresh display - [self.dataSource searchWithPatterns:nil]; -} - -#pragma mark - resync management - -- (void)onSyncNotification -{ - latestServerSync = [NSDate date]; - - MXWeakify(self); - - // Refresh all groups summary - [self.dataSource refreshGroupsSummary:^{ - - MXStrongifyAndReturnIfNil(self); - - [self removeReconnectingView]; - }]; -} - -- (BOOL)canReconnect -{ - // avoid restarting connection if some data has been received within 1 second (1000 : latestServerSync is null) - NSTimeInterval interval = latestServerSync ? [[NSDate date] timeIntervalSinceDate:latestServerSync] : 1000; - return (interval > 1) && [self.mainSession reconnect]; -} - -- (void)addReconnectingView -{ - if (!reconnectingView) - { - UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; - spinner.transform = CGAffineTransformMakeScale(0.75f, 0.75f); - CGRect frame = spinner.frame; - frame.size.height = 80; // 80 * 0.75 = 60 - spinner.bounds = frame; - spinner.color = [UIColor darkGrayColor]; - spinner.hidesWhenStopped = NO; - spinner.backgroundColor = _groupsTableView.backgroundColor; - [spinner startAnimating]; - - // no need to manage constraints here, IOS defines them. - tableViewHeaderView = _groupsTableView.tableHeaderView; - _groupsTableView.tableHeaderView = reconnectingView = spinner; - } -} - -- (void)removeReconnectingView -{ - if (reconnectingView && !restartConnection) - { - _groupsTableView.tableHeaderView = tableViewHeaderView; - reconnectingView = nil; - } -} - -/** - Detect if the current connection must be restarted. - The spinner is displayed until the overscroll ends (and scrollViewDidEndDecelerating is called). - */ -- (void)detectPullToKick:(UIScrollView *)scrollView -{ - if (!reconnectingView) - { - // detect if the user scrolls over the tableview top - restartConnection = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top < -128); - - if (restartConnection) - { - // wait that list decelerate to display / hide it - [self addReconnectingView]; - } - } -} - -/** - Restarts the current connection if it is required. - The 0.3s delay is added to avoid flickering if the connection does not require to be restarted. - */ -- (void)managePullToKick:(UIScrollView *)scrollView -{ - // the current connection must be restarted - if (restartConnection) - { - // display at least 0.3s the spinner to show to the user that something is pending - // else the UI is flickering - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - self->restartConnection = NO; - - if (![self canReconnect]) - { - // if the event stream has not been restarted - // hide the spinner - [self removeReconnectingView]; - } - // else wait that onSyncNotification is called. - }); - } -} - -@end diff --git a/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.xib b/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.xib deleted file mode 100644 index fad906339..000000000 --- a/Riot/Modules/MatrixKit/Controllers/MXKGroupListViewController.xib +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/MatrixKit/MatrixKit.h b/Riot/Modules/MatrixKit/MatrixKit.h index d9cf57cf0..2bb02223b 100644 --- a/Riot/Modules/MatrixKit/MatrixKit.h +++ b/Riot/Modules/MatrixKit/MatrixKit.h @@ -146,9 +146,4 @@ #import "MXKCountryPickerViewController.h" #import "MXKLanguagePickerViewController.h" -#import "MXKGroupCellData.h" -#import "MXKSessionGroupsDataSource.h" -#import "MXKGroupListViewController.h" -#import "MXKGroupTableViewCell.h" - #import "MXKSlashCommands.h" diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 9e9dde0e5..5e7582808 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -1618,11 +1618,15 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; if (retry) { [self attemptDeviceDehydrationWithKeyData:keyData retry:NO success:success failure:failure]; - MXLogError(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error: %@. Retrying.", error); + MXLogErrorDetails(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error: Retrying.", @{ + @"error": error ?: @"unknown" + }); } else { - MXLogError(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error: %@", error); + MXLogErrorDetails(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error", @{ + @"error": error ?: @"unknown" + }); if (failure) { @@ -1772,7 +1776,9 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; dispatch_group_leave(dispatchGroup); } failure:^(NSError *error) { - MXLogError(@"[MXKAccount] onDateTimeFormatUpdate: event fetch failed: %@", error); + MXLogErrorDetails(@"[MXKAccount] onDateTimeFormatUpdate: event fetch failed", @{ + @"error": error ?: @"unknown" + }); dispatch_group_leave(dispatchGroup); }]; } diff --git a/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellData.h b/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellData.h deleted file mode 100644 index 9ecd35b9c..000000000 --- a/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellData.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXKGroupCellDataStoring.h" - -/** - `MXKGroupCellData` modelised the data for a `MXKGroupTableViewCell` cell. - */ -@interface MXKGroupCellData : MXKCellData - -@end diff --git a/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellData.m b/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellData.m deleted file mode 100644 index 9bb53db22..000000000 --- a/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellData.m +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXKGroupCellData.h" - -#import "MXKSessionGroupsDataSource.h" - -@implementation MXKGroupCellData -@synthesize group, groupsDataSource, groupDisplayname, sortingDisplayname; - -- (instancetype)initWithGroup:(MXGroup*)theGroup andGroupsDataSource:(MXKSessionGroupsDataSource*)theGroupsDataSource -{ - self = [self init]; - if (self) - { - groupsDataSource = theGroupsDataSource; - [self updateWithGroup:theGroup]; - } - return self; -} - -- (void)updateWithGroup:(MXGroup*)theGroup -{ - group = theGroup; - - groupDisplayname = sortingDisplayname = group.profile.name; - - if (!groupDisplayname.length) - { - groupDisplayname = group.groupId; - // Ignore the prefix '+' of the group id during sorting. - sortingDisplayname = [groupDisplayname substringFromIndex:1]; - } -} - -@end diff --git a/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellDataStoring.h b/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellDataStoring.h deleted file mode 100644 index 9ca2bd57b..000000000 --- a/Riot/Modules/MatrixKit/Models/Group/MXKGroupCellDataStoring.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import - -#import "MXKCellData.h" - -@class MXKSessionGroupsDataSource; - -/** - `MXKGroupCellDataStoring` defines a protocol a class must conform in order to store group cell data - managed by `MXKSessionGroupsDataSource`. - */ -@protocol MXKGroupCellDataStoring - -@property (nonatomic, weak, readonly) MXKSessionGroupsDataSource *groupsDataSource; - -@property (nonatomic, readonly) MXGroup *group; - -@property (nonatomic, readonly) NSString *groupDisplayname; -@property (nonatomic, readonly) NSString *sortingDisplayname; - -#pragma mark - Public methods -/** - Create a new `MXKCellData` object for a new group cell. - - @param group the `MXGroup` object that has data about the group. - @param groupsDataSource the `MXKSessionGroupsDataSource` object that will use this instance. - @return the newly created instance. - */ -- (instancetype)initWithGroup:(MXGroup*)group andGroupsDataSource:(MXKSessionGroupsDataSource*)groupsDataSource; - -/** - The `MXKSessionGroupsDataSource` object calls this method when the group data has been updated. - - @param group the updated group. - */ -- (void)updateWithGroup:(MXGroup*)group; - -@end diff --git a/Riot/Modules/MatrixKit/Models/Group/MXKSessionGroupsDataSource.h b/Riot/Modules/MatrixKit/Models/Group/MXKSessionGroupsDataSource.h deleted file mode 100644 index 7ee1ac3c0..000000000 --- a/Riot/Modules/MatrixKit/Models/Group/MXKSessionGroupsDataSource.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXKDataSource.h" -#import "MXKGroupCellData.h" - -/** - Identifier to use for cells that display a group. - */ -extern NSString *const kMXKGroupCellIdentifier; - -/** - 'MXKSessionGroupsDataSource' is a base class to handle the groups of a matrix session. - A 'MXKSessionGroupsDataSource' instance provides the data source for `MXKGroupListViewController`. - - A section is created to handle the invitations to a group, the first one if any. - */ -@interface MXKSessionGroupsDataSource : MXKDataSource -{ -@protected - - /** - The current list of the group invitations (sorted in the alphabetic order). - This list takes into account potential filter defined by`patternsList`. - */ - NSMutableArray *groupsInviteCellDataArray; - - /** - The current displayed list of the joined groups (sorted in the alphabetic order). - This list takes into account potential filter defined by`patternsList`. - */ - NSMutableArray *groupsCellDataArray; -} - -@property (nonatomic) NSInteger groupInvitesSection; -@property (nonatomic) NSInteger joinedGroupsSection; - -#pragma mark - Life cycle - -/** - Refresh all the groups summary. - The group data are not synced with the server, use this method to refresh them according to your needs. - - @param completion the block to execute when a request has been done for each group (whatever the result of the requests). - You may specify nil for this parameter. - */ -- (void)refreshGroupsSummary:(void (^)(void))completion; - -/** - Filter the current groups list according to the provided patterns. - When patterns are not empty, the search result is stored in `filteredGroupsCellDataArray`, - this array provides then data for the cells served by `MXKSessionGroupsDataSource`. - - @param patternsList the list of patterns (`NSString` instances) to match with. Set nil to cancel search. - */ -- (void)searchWithPatterns:(NSArray*)patternsList; - -/** - Get the data for the cell at the given index path. - - @param indexPath the index of the cell in the table - @return the cell data - */ -- (id)cellDataAtIndex:(NSIndexPath*)indexPath; - -/** - Get the index path of the cell related to the provided groupId. - - @param groupId the group identifier. - @return indexPath the index of the cell (nil if not found). - */ -- (NSIndexPath*)cellIndexPathWithGroupId:(NSString*)groupId; - -/** - Leave the group displayed at the provided path. - - @param indexPath the index of the group cell in the table - */ -- (void)leaveGroupAtIndexPath:(NSIndexPath *)indexPath; - -@end diff --git a/Riot/Modules/MatrixKit/Models/Group/MXKSessionGroupsDataSource.m b/Riot/Modules/MatrixKit/Models/Group/MXKSessionGroupsDataSource.m deleted file mode 100644 index d35ce9502..000000000 --- a/Riot/Modules/MatrixKit/Models/Group/MXKSessionGroupsDataSource.m +++ /dev/null @@ -1,611 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - Copyright 2018 New Vector Ltd - Copyright 2019 The Matrix.org Foundation C.I.C - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXKSessionGroupsDataSource.h" - -#import "NSBundle+MatrixKit.h" - -#import "MXKConstants.h" - -#import "MXKSwiftHeader.h" - -#pragma mark - Constant definitions -NSString *const kMXKGroupCellIdentifier = @"kMXKGroupCellIdentifier"; - - -@interface MXKSessionGroupsDataSource () -{ - /** - Internal array used to regulate change notifications. - Cell data changes are stored instantly in this array. - We wait at least for 500 ms between two notifications of the delegate. - */ - NSMutableArray *internalCellDataArray; - - /* - Timer to not notify the delegate on every changes. - */ - NSTimer *timer; - - /* - Tells whether some changes must be notified. - */ - BOOL isDataChangePending; - - /** - Store the current search patterns list. - */ - NSArray* searchPatternsList; -} - -@end - -@implementation MXKSessionGroupsDataSource - -- (instancetype)initWithMatrixSession:(MXSession *)matrixSession -{ - self = [super initWithMatrixSession:matrixSession]; - if (self) - { - internalCellDataArray = [NSMutableArray array]; - groupsCellDataArray = [NSMutableArray array]; - groupsInviteCellDataArray = [NSMutableArray array]; - - isDataChangePending = NO; - - // Set default data and view classes - [self registerCellDataClass:MXKGroupCellData.class forCellIdentifier:kMXKGroupCellIdentifier]; - } - return self; -} - -- (void)destroy -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - groupsCellDataArray = nil; - groupsInviteCellDataArray = nil; - internalCellDataArray = nil; - - searchPatternsList = nil; - - [timer invalidate]; - timer = nil; - - [super destroy]; -} - -- (void)didMXSessionStateChange -{ - if (MXSessionStateRunning <= self.mxSession.state) - { - // Check whether some data have been already load - if (0 == internalCellDataArray.count) - { - [self loadData]; - } - else if (self.mxSession.state == MXSessionStateRunning) - { - // Refresh the group data - [self refreshGroupsSummary:nil]; - } - } -} - -#pragma mark - - -- (void)refreshGroupsSummary:(void (^)(void))completion -{ - MXLogDebug(@"[MXKSessionGroupsDataSource] refreshGroupsSummary"); - - __block NSUInteger count = internalCellDataArray.count; - - if (count) - { - for (id groupData in internalCellDataArray) - { - // Force the matrix session to refresh the group summary. - [self.mxSession updateGroupSummary:groupData.group success:^{ - - if (completion && !(--count)) - { - // All the requests have been done. - completion (); - } - - } failure:^(NSError *error) { - - MXLogDebug(@"[MXKSessionGroupsDataSource] refreshGroupsSummary: group summary update failed %@", groupData.group.groupId); - - if (completion && !(--count)) - { - // All the requests have been done. - completion (); - } - - }]; - } - } - else if (completion) - { - completion(); - } -} - -- (void)searchWithPatterns:(NSArray*)patternsList -{ - if (patternsList.count) - { - searchPatternsList = patternsList; - } - else - { - searchPatternsList = nil; - } - - [self onCellDataChange]; -} - -- (id)cellDataAtIndex:(NSIndexPath*)indexPath -{ - id groupData; - - if (indexPath.section == _groupInvitesSection) - { - if (indexPath.row < groupsInviteCellDataArray.count) - { - groupData = groupsInviteCellDataArray[indexPath.row]; - } - } - else if (indexPath.section == _joinedGroupsSection) - { - if (indexPath.row < groupsCellDataArray.count) - { - groupData = groupsCellDataArray[indexPath.row]; - } - } - - return groupData; -} - -- (NSIndexPath*)cellIndexPathWithGroupId:(NSString*)groupId -{ - // Look for the cell - if (_groupInvitesSection != -1) - { - for (NSInteger index = 0; index < groupsInviteCellDataArray.count; index ++) - { - id groupData = groupsInviteCellDataArray[index]; - if ([groupId isEqualToString:groupData.group.groupId]) - { - // Got it - return [NSIndexPath indexPathForRow:index inSection:_groupInvitesSection]; - } - } - } - - if (_joinedGroupsSection != -1) - { - for (NSInteger index = 0; index < groupsCellDataArray.count; index ++) - { - id groupData = groupsCellDataArray[index]; - if ([groupId isEqualToString:groupData.group.groupId]) - { - // Got it - return [NSIndexPath indexPathForRow:index inSection:_joinedGroupsSection]; - } - } - } - - return nil; -} - -#pragma mark - Groups processing - -- (void)loadData -{ - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewGroupInviteNotification object:self.mxSession]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidJoinGroupNotification object:self.mxSession]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidLeaveGroupNotification object:self.mxSession]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdateGroupSummaryNotification object:self.mxSession]; - - // Reset the table - [internalCellDataArray removeAllObjects]; - - // Retrieve the MXKCellData class to manage the data - Class class = [self cellDataClassForCellIdentifier:kMXKGroupCellIdentifier]; - NSAssert([class conformsToProtocol:@protocol(MXKGroupCellDataStoring)], @"MXKSessionGroupsDataSource only manages MXKCellData that conforms to MXKGroupCellDataStoring protocol"); - - // Listen to MXSession groups changes - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onNewGroupInvite:) name:kMXSessionNewGroupInviteNotification object:self.mxSession]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didJoinGroup:) name:kMXSessionDidJoinGroupNotification object:self.mxSession]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didLeaveGroup:) name:kMXSessionDidLeaveGroupNotification object:self.mxSession]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGroup:) name:kMXSessionDidUpdateGroupSummaryNotification object:self.mxSession]; - - NSDate *startDate = [NSDate date]; - - NSArray *groups = self.mxSession.groups; - for (MXGroup *group in groups) - { - id cellData = [[class alloc] initWithGroup:group andGroupsDataSource:self]; - if (cellData) - { - [internalCellDataArray addObject:cellData]; - - // Force the matrix session to refresh the group summary. - [self.mxSession updateGroupSummary:group success:nil failure:^(NSError *error) { - MXLogDebug(@"[MXKSessionGroupsDataSource] loadData: group summary update failed %@", group.groupId); - }]; - } - } - - MXLogDebug(@"[MXKSessionGroupsDataSource] Loaded %tu groups in %.3fms", groups.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000); - - [self sortCellData]; - [self onCellDataChange]; -} - -- (void)didUpdateGroup:(NSNotification *)notif -{ - MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey]; - if (group) - { - id groupData = [self cellDataWithGroupId:group.groupId]; - if (groupData) - { - [groupData updateWithGroup:group]; - } - else - { - MXLogDebug(@"[MXKSessionGroupsDataSource] didUpdateGroup: Cannot find the changed group for %@ (%@). It is probably not managed by this group data source", group.groupId, group); - return; - } - } - - [self sortCellData]; - [self onCellDataChange]; -} - -- (void)onNewGroupInvite:(NSNotification *)notif -{ - MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey]; - if (group) - { - // Add the group if there is not yet a cell for it - id groupData = [self cellDataWithGroupId:group.groupId]; - if (nil == groupData) - { - MXLogDebug(@"MXKSessionGroupsDataSource] Add new group invite: %@", group.groupId); - - // Retrieve the MXKCellData class to manage the data - Class class = [self cellDataClassForCellIdentifier:kMXKGroupCellIdentifier]; - - id cellData = [[class alloc] initWithGroup:group andGroupsDataSource:self]; - if (cellData) - { - [internalCellDataArray addObject:cellData]; - - [self sortCellData]; - [self onCellDataChange]; - } - } - } -} - -- (void)didJoinGroup:(NSNotification *)notif -{ - MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey]; - if (group) - { - id groupData = [self cellDataWithGroupId:group.groupId]; - if (groupData) - { - MXLogDebug(@"MXKSessionGroupsDataSource] Update joined room: %@", group.groupId); - [groupData updateWithGroup:group]; - } - else - { - MXLogDebug(@"MXKSessionGroupsDataSource] Add new joined invite: %@", group.groupId); - - // Retrieve the MXKCellData class to manage the data - Class class = [self cellDataClassForCellIdentifier:kMXKGroupCellIdentifier]; - - id cellData = [[class alloc] initWithGroup:group andGroupsDataSource:self]; - if (cellData) - { - [internalCellDataArray addObject:cellData]; - } - } - - [self sortCellData]; - [self onCellDataChange]; - } -} - -- (void)didLeaveGroup:(NSNotification *)notif -{ - NSString *groupId = notif.userInfo[kMXSessionNotificationGroupIdKey]; - if (groupId) - { - [self removeGroup:groupId]; - } -} - -- (void)removeGroup:(NSString*)groupId -{ - id groupData = [self cellDataWithGroupId:groupId]; - if (groupData) - { - MXLogDebug(@"MXKSessionGroupsDataSource] Remove left group: %@", groupId); - - [internalCellDataArray removeObject:groupData]; - - [self sortCellData]; - [self onCellDataChange]; - } -} - -- (void)onCellDataChange -{ - isDataChangePending = NO; - - // Check no notification was done recently. - // Note: do not wait in case of search - if (timer == nil || searchPatternsList) - { - [timer invalidate]; - timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(checkPendingUpdate:) userInfo:nil repeats:NO]; - - // Prepare cell data array, and notify the delegate. - [self prepareCellDataAndNotifyChanges]; - } - else - { - isDataChangePending = YES; - } -} - -- (IBAction)checkPendingUpdate:(id)sender -{ - [timer invalidate]; - timer = nil; - - if (isDataChangePending) - { - [self onCellDataChange]; - } -} - -- (void)sortCellData -{ - // Order alphabetically the groups - [internalCellDataArray sortUsingComparator:^NSComparisonResult(id cellData1, id cellData2) - { - if (cellData1.sortingDisplayname.length && cellData2.sortingDisplayname.length) - { - return [cellData1.sortingDisplayname compare:cellData2.sortingDisplayname options:NSCaseInsensitiveSearch]; - } - else if (cellData1.sortingDisplayname.length) - { - return NSOrderedAscending; - } - else if (cellData2.sortingDisplayname.length) - { - return NSOrderedDescending; - } - return NSOrderedSame; - }]; -} - -- (void)prepareCellDataAndNotifyChanges -{ - // Prepare the cell data arrays by considering the potential filter. - [groupsInviteCellDataArray removeAllObjects]; - [groupsCellDataArray removeAllObjects]; - for (id groupData in internalCellDataArray) - { - BOOL isKept = !searchPatternsList; - - for (NSString* pattern in searchPatternsList) - { - if (groupData.groupDisplayname && [groupData.groupDisplayname rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound) - { - isKept = YES; - break; - } - } - - if (isKept) - { - if (groupData.group.membership == MXMembershipInvite) - { - [groupsInviteCellDataArray addObject:groupData]; - } - else - { - [groupsCellDataArray addObject:groupData]; - } - } - } - - // Update here data source state - if (state != MXKDataSourceStateReady) - { - state = MXKDataSourceStateReady; - if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)]) - { - [self.delegate dataSource:self didStateChange:state]; - } - } - - // And inform the delegate about the update - [self.delegate dataSource:self didCellChange:nil]; -} - -// Find the cell data that stores information about the given group id -- (id)cellDataWithGroupId:(NSString*)groupId -{ - id theGroupData; - for (id groupData in internalCellDataArray) - { - if ([groupData.group.groupId isEqualToString:groupId]) - { - theGroupData = groupData; - break; - } - } - return theGroupData; -} - -#pragma mark - UITableViewDataSource - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - NSInteger count = 0; - _groupInvitesSection = _joinedGroupsSection = -1; - - // Check whether all data sources are ready before rendering groups. - if (self.state == MXKDataSourceStateReady) - { - if (groupsInviteCellDataArray.count) - { - _groupInvitesSection = count++; - } - if (groupsCellDataArray.count) - { - _joinedGroupsSection = count++; - } - } - return count; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - if (section == _groupInvitesSection) - { - return groupsInviteCellDataArray.count; - } - else if (section == _joinedGroupsSection) - { - return groupsCellDataArray.count; - } - - return 0; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - NSString* sectionTitle = nil; - - if (section == _groupInvitesSection) - { - sectionTitle = [VectorL10n groupInviteSection]; - } - else if (section == _joinedGroupsSection) - { - sectionTitle = [VectorL10n groupSection]; - } - - return sectionTitle; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - id groupData; - - if (indexPath.section == _groupInvitesSection) - { - if (indexPath.row < groupsInviteCellDataArray.count) - { - groupData = groupsInviteCellDataArray[indexPath.row]; - } - } - else if (indexPath.section == _joinedGroupsSection) - { - if (indexPath.row < groupsCellDataArray.count) - { - groupData = groupsCellDataArray[indexPath.row]; - } - } - - if (groupData) - { - NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:groupData]; - if (cellIdentifier) - { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath]; - - // Make sure we listen to user actions on the cell - cell.delegate = self; - - // Make the bubble display the data - [cell render:groupData]; - - return cell; - } - } - - // Return a fake cell to prevent app from crashing. - return [[UITableViewCell alloc] init]; -} - -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - // Return NO if you do not want the specified item to be editable. - return YES; -} - -- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (editingStyle == UITableViewCellEditingStyleDelete) - { - [self leaveGroupAtIndexPath:indexPath]; - } -} - -- (void)leaveGroupAtIndexPath:(NSIndexPath *)indexPath -{ - id cellData = [self cellDataAtIndex:indexPath]; - - if (cellData.group) - { - __weak typeof(self) weakSelf = self; - - [self.mxSession leaveGroup:cellData.group.groupId success:^{ - - if (weakSelf) - { - // Refresh the table content - typeof(self) self = weakSelf; - [self removeGroup:cellData.group.groupId]; - } - - } failure:^(NSError *error) { - - MXLogDebug(@"[MXKSessionGroupsDataSource] Failed to leave group (%@)", cellData.group.groupId); - - // Notify MatrixKit user - NSString *myUserId = self.mxSession.myUser.userId; - [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil]; - - }]; - } -} - - -@end diff --git a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m index 8d8e44b12..40cf2ebd3 100644 --- a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m +++ b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m @@ -169,7 +169,6 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringRoomMessageFeedback, kMXEventTypeStringRoomRedaction, kMXEventTypeStringRoomThirdPartyInvite, - kMXEventTypeStringRoomRelatedGroups, kMXEventTypeStringReaction, kMXEventTypeStringCallInvite, kMXEventTypeStringCallAnswer, diff --git a/Riot/Modules/MatrixKit/Models/MXKDataSource.h b/Riot/Modules/MatrixKit/Models/MXKDataSource.h index a1332d6e2..d300a2d7f 100644 --- a/Riot/Modules/MatrixKit/Models/MXKDataSource.h +++ b/Riot/Modules/MatrixKit/Models/MXKDataSource.h @@ -221,5 +221,15 @@ typedef enum : NSUInteger { */ - (BOOL)dataSource:(MXKDataSource*)dataSource shouldDoAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo defaultValue:(BOOL)defaultValue; +/** + Notify the delegate that invites count did change + + @see `MXKCellRenderingDelegate` for more details. + + @param dataSource the involved data source. + @param invitesCount number of rooms in the invites section. + */ +- (void)dataSource:(MXKDataSource*)dataSource didUpdateInvitesCount:(NSUInteger)invitesCount; + @end diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m index 634ca547f..84b190a4b 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m @@ -476,7 +476,9 @@ NSString *const kMXKAttachmentFileNameBase = @"attatchment"; NSError *error; BOOL result = [NSFileManager.defaultManager removeItemAtPath:[temporaryDirectoryPath stringByAppendingPathComponent:filePath] error:&error]; if (!result && error) { - MXLogError(@"[MXKAttachment] Failed deleting temporary file with error: %@", error); + MXLogErrorDetails(@"[MXKAttachment] Failed deleting temporary file with error", @{ + @"error": error ?: @"unknown" + }); } } } diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index b1e904b60..e42b9e6d2 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -29,7 +29,7 @@ #import "GeneratedInterface-Swift.h" @implementation MXKRoomBubbleCellData -@synthesize senderId, targetId, roomId, senderDisplayName, senderAvatarUrl, senderAvatarPlaceholder, targetDisplayName, targetAvatarUrl, targetAvatarPlaceholder, isEncryptedRoom, isPaginationFirstBubble, shouldHideSenderInformation, date, isIncoming, isAttachmentWithThumbnail, isAttachmentWithIcon, attachment, senderFlair; +@synthesize senderId, targetId, roomId, senderDisplayName, senderAvatarUrl, senderAvatarPlaceholder, targetDisplayName, targetAvatarUrl, targetAvatarPlaceholder, isEncryptedRoom, isPaginationFirstBubble, shouldHideSenderInformation, date, isIncoming, isAttachmentWithThumbnail, isAttachmentWithIcon, attachment; @synthesize textMessage, attributedTextMessage, attributedTextMessageWithoutPositioningSpace; @synthesize shouldHideSenderName, isTyping, showBubbleDateTime, showBubbleReceipts, useCustomDateTimeLabel, useCustomReceipts, useCustomUnsentButton, hasNoDisplay; @synthesize tag; @@ -122,9 +122,6 @@ - (void)dealloc { - // Reset any observer on publicised groups by user. - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; - roomDataSource = nil; bubbleComponents = nil; } @@ -450,58 +447,6 @@ - (void)setShouldHideSenderInformation:(BOOL)inShouldHideSenderInformation { shouldHideSenderInformation = inShouldHideSenderInformation; - - if (!shouldHideSenderInformation) - { - // Refresh the flair - [self refreshSenderFlair]; - } -} - -- (void)refreshSenderFlair -{ - // Reset by default any observer on publicised groups by user. - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; - - // Check first whether the room enabled the flair for some groups - NSArray *roomRelatedGroups = roomDataSource.roomState.relatedGroups; - if (roomRelatedGroups.count && senderId) - { - NSArray *senderPublicisedGroups; - - senderPublicisedGroups = [self.mxSession publicisedGroupsForUser:senderId]; - - if (senderPublicisedGroups.count) - { - // Cross the 2 arrays to keep only the common group ids - NSMutableArray *flair = [NSMutableArray arrayWithCapacity:roomRelatedGroups.count]; - - for (NSString *groupId in roomRelatedGroups) - { - if ([senderPublicisedGroups indexOfObject:groupId] != NSNotFound) - { - MXGroup *group = [roomDataSource groupWithGroupId:groupId]; - [flair addObject:group]; - } - } - - if (flair.count) - { - self.senderFlair = flair; - } - else - { - self.senderFlair = nil; - } - } - else - { - self.senderFlair = nil; - } - - // Observe any change on publicised groups for the message sender - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionUpdatePublicisedGroupsForUsers:) name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; - } } - (BOOL)hasThreadRoot @@ -1042,18 +987,4 @@ } } -- (void)didMXSessionUpdatePublicisedGroupsForUsers:(NSNotification *)notif -{ - // Retrieved the list of the concerned users - NSArray *userIds = notif.userInfo[kMXSessionNotificationUserIdsArrayKey]; - if (userIds.count && self.senderId) - { - // Check whether the current sender is concerned. - if ([userIds indexOfObject:self.senderId] != NSNotFound) - { - [self refreshSenderFlair]; - } - } -} - @end diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h index c9a3fb98f..ad845ec4e 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h @@ -91,11 +91,6 @@ */ @property (nonatomic) UIImage *targetAvatarPlaceholder; -/** - The current sender flair (list of the publicised groups in the sender profile which matches the room flair settings) - */ -@property (nonatomic) NSArray *senderFlair; - /** Tell whether the room is encrypted. */ @@ -304,11 +299,6 @@ Update the event because its sent state changed or it is has been redacted. foregroundColor:(UIColor*)foregroundColor andFont:(UIFont*)patternFont; -/** - Refresh the sender flair information - */ -- (void)refreshSenderFlair; - /** Indicate that the current text message layout is no longer valid and should be recomputed before presentation in a bubble cell. This could be due to the content changing, or the diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 1caf1315c..a5c542880 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -100,11 +100,6 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; The queue of events that need to be processed in order to compute their display. */ NSMutableArray *eventsToProcess; - - /** - The dictionary of the related groups that the current user did not join. - */ - NSMutableDictionary *externalRelatedGroups; } /** @@ -777,17 +772,6 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; */ - (void)collapseRoomBubble:(id)bubbleData collapsed:(BOOL)collapsed; -#pragma mark - Groups - -/** - Get a MXGroup instance for a group. - This method is used by the bubble to retrieve a related groups of the room. - - @param groupId The identifier to the group. - @return the MXGroup instance. - */ -- (MXGroup *)groupWithGroupId:(NSString*)groupId; - #pragma mark - Reactions /** diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index dca87ded5..5872eee2d 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -87,11 +87,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { The listener to receipts events in the room. */ id receiptsListener; - - /** - The listener to the related groups state events in the room. - */ - id relatedGroupsListener; /** The listener to reactions changed in the room. @@ -318,8 +313,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { eventsToProcess = [NSMutableArray array]; eventIdToBubbleMap = [NSMutableDictionary dictionary]; - externalRelatedGroups = [NSMutableDictionary dictionary]; - _filterMessagesWithURL = NO; emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", kMXKSlashCmdEmote]; @@ -471,8 +464,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { - (void)resetNotifying:(BOOL)notify { - [externalRelatedGroups removeAllObjects]; - if (roomDidFlushDataNotificationObserver) { [[NSNotificationCenter defaultCenter] removeObserver:roomDidFlushDataNotificationObserver]; @@ -515,9 +506,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [_timeline removeListener:receiptsListener]; receiptsListener = nil; - - [_timeline removeListener:relatedGroupsListener]; - relatedGroupsListener = nil; } if (_secondaryRoom && secondaryLiveEventsListener) @@ -601,8 +589,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self unregisterScanManagerNotifications]; [self unregisterReactionsChangeListener]; [self unregisterEventEditsListener]; - - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeSentStateNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidDecryptNotification object:nil]; @@ -638,8 +624,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [_timeline destroy]; [_secondaryTimeline destroy]; - externalRelatedGroups = nil; - [super destroy]; } @@ -824,52 +808,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self setState:MXKDataSourceStateFailed]; } } - - if (_room && MXSessionStateRunning == self.mxSession.state) - { - // Flair handling: observe the update in the publicised groups by users when the flair is enabled in the room. - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; - [self.room state:^(MXRoomState *roomState) { - if (roomState.relatedGroups.count) - { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionUpdatePublicisedGroupsForUsers:) name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; - - // Get a fresh profile for all the related groups. Trigger a table refresh when all requests are done. - __block NSUInteger count = roomState.relatedGroups.count; - for (NSString *groupId in roomState.relatedGroups) - { - MXGroup *group = [self.mxSession groupWithGroupId:groupId]; - if (!group) - { - // Create a group instance for the groups that the current user did not join. - group = [[MXGroup alloc] initWithGroupId:groupId]; - [self->externalRelatedGroups setObject:group forKey:groupId]; - } - - // Refresh the group profile from server. - [self.mxSession updateGroupProfile:group success:^{ - - if (self.delegate && !(--count)) - { - // All the requests have been done. - [self.delegate dataSource:self didCellChange:nil]; - } - - } failure:^(NSError *error) { - - MXLogDebug(@"[MXKRoomDataSource][%p] group profile update failed %@", self, groupId); - - if (self.delegate && !(--count)) - { - // All the requests have been done. - [self.delegate dataSource:self didCellChange:nil]; - } - - }]; - } - } - }]; - } } - (void)initializeTimelineForThread @@ -1008,7 +946,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [_timeline removeListener:liveEventsListener]; [_timeline removeListener:redactionListener]; [_timeline removeListener:receiptsListener]; - [_timeline removeListener:relatedGroupsListener]; } // Listen to live events only for live timeline @@ -1071,16 +1008,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self didReceiveReceiptEvent:event roomState:roomState]; } }]; - - // Flair handling: register a listener for the related groups state event in this room. - relatedGroupsListener = [_timeline listenToEventsOfTypes:@[kMXEventTypeStringRoomRelatedGroups] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) { - - if (MXTimelineDirectionForwards == direction) - { - // The flair settings have been updated: flush the current bubble data and rebuild them. - [self reload]; - } - }]; } // Register a listener to handle redaction which can affect live and past timelines @@ -2693,32 +2620,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } } -- (void)didMXSessionUpdatePublicisedGroupsForUsers:(NSNotification *)notif -{ - // Retrieved the list of the concerned users - NSArray *userIds = notif.userInfo[kMXSessionNotificationUserIdsArrayKey]; - if (userIds.count) - { - // Check whether at least one listed user is a room member. - for (NSString* userId in userIds) - { - MXRoomMember * roomMember = [self.roomState.members memberWithUserId:userId]; - if (roomMember) - { - // Inform the delegate to refresh the bubble display - // We dispatch here this action in order to let each bubble data update their sender flair. - if (self.delegate) - { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate dataSource:self didCellChange:nil]; - }); - } - break; - } - } - } -} - - (void)eventDidChangeSentState:(NSNotification *)notif { MXEvent *event = notif.object; @@ -3993,34 +3894,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { return cell; } -#pragma mark - Groups - -- (MXGroup *)groupWithGroupId:(NSString*)groupId -{ - MXGroup *group = [self.mxSession groupWithGroupId:groupId]; - if (!group) - { - // Check whether an instance has been already created. - group = [externalRelatedGroups objectForKey:groupId]; - } - - if (!group) - { - // Create a new group instance. - group = [[MXGroup alloc] initWithGroupId:groupId]; - [externalRelatedGroups setObject:group forKey:groupId]; - - // Retrieve at least the group profile - [self.mxSession updateGroupProfile:group success:nil failure:^(NSError *error) { - - MXLogDebug(@"[MXKRoomDataSource][%p] groupWithGroupId: group profile update failed %@", self, groupId); - - }]; - } - - return group; -} - #pragma mark - MXScanManager notifications - (void)registerScanManagerNotifications diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h index 35c1508ef..776b79927 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h @@ -99,7 +99,6 @@ typedef enum : NSUInteger { @property (nonatomic) BOOL treatMatrixRoomIdAsLink; @property (nonatomic) BOOL treatMatrixRoomAliasAsLink; @property (nonatomic) BOOL treatMatrixEventIdAsLink; -@property (nonatomic) BOOL treatMatrixGroupIdAsLink; /** Initialise the event formatter. diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index a4b7a8f3b..fa6495fd5 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1022,21 +1022,6 @@ static NSString *const kHTMLATagRegexPattern = @"( } break; } - case MXEventTypeRoomRelatedGroups: - { - NSArray *groups; - MXJSONModelSetArray(groups, event.content[@"groups"]); - if (groups) - { - displayText = [VectorL10n noticeRoomRelatedGroups:[groups componentsJoinedByString:@", "]]; - // Append redacted info if any - if (redactedInfo) - { - displayText = [NSString stringWithFormat:@"%@\n %@", displayText, redactedInfo]; - } - } - break; - } case MXEventTypeRoomEncrypted: { // Is redacted? @@ -2081,12 +2066,6 @@ static NSString *const kHTMLATagRegexPattern = @"( { enabledMatrixIdsBitMask |= MXKTOOLS_EVENT_IDENTIFIER_BITWISE; } - - // If enabled, make group id clickable - if (_treatMatrixGroupIdAsLink) - { - enabledMatrixIdsBitMask |= MXKTOOLS_GROUP_IDENTIFIER_BITWISE; - } [MXKTools createLinksInMutableAttributedString:mutableAttributedString forEnabledMatrixIds:enabledMatrixIdsBitMask]; } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift b/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift index 392e900dd..0800f4307 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift @@ -44,7 +44,7 @@ extension MarkdownToHTMLRenderer: MarkdownToHTMLRendererProtocol { ast.repairLinks() return try DownHTMLRenderer.astToHTML(ast, options: options) } catch { - MXLog.error("[MarkdownToHTMLRenderer] renderToHTML failed with string: \(markdown)") + MXLog.error("[MarkdownToHTMLRenderer] renderToHTML failed") return nil } } diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.h b/Riot/Modules/MatrixKit/Utils/MXKTools.h index a94d390d7..f5326d405 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.h +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.h @@ -25,7 +25,6 @@ #define MXKTOOLS_ROOM_IDENTIFIER_BITWISE 0x02 #define MXKTOOLS_ROOM_ALIAS_BITWISE 0x04 #define MXKTOOLS_EVENT_IDENTIFIER_BITWISE 0x08 -#define MXKTOOLS_GROUP_IDENTIFIER_BITWISE 0x10 // Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string. extern NSString *const kMXKToolsBlockquoteMarkAttribute; diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index 9f611867f..10cf491a6 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -42,7 +42,6 @@ static NSRegularExpression *userIdRegex; static NSRegularExpression *roomIdRegex; static NSRegularExpression *roomAliasRegex; static NSRegularExpression *eventIdRegex; -static NSRegularExpression *groupIdRegex; // A regex to find http URLs. static NSRegularExpression *httpLinksRegex; // A regex to find all HTML tags @@ -59,7 +58,6 @@ static NSRegularExpression *htmlTagsRegex; roomIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixRoomIdentifier options:NSRegularExpressionCaseInsensitive error:nil]; roomAliasRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixRoomAlias options:NSRegularExpressionCaseInsensitive error:nil]; eventIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixEventIdentifier options:NSRegularExpressionCaseInsensitive error:nil]; - groupIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixGroupIdentifier options:NSRegularExpressionCaseInsensitive error:nil]; httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil]; htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil]; @@ -1039,12 +1037,6 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo { [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex]; } - - // If enabled, make group id clickable - if (enabledMatrixIdsBitMask & MXKTOOLS_GROUP_IDENTIFIER_BITWISE) - { - [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:groupIdRegex]; - } } + (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString matchingRegex:(NSRegularExpression*)regex diff --git a/Riot/Modules/MatrixKit/Utils/MXKVideoThumbnailGenerator.swift b/Riot/Modules/MatrixKit/Utils/MXKVideoThumbnailGenerator.swift index 9f999294b..5da97dcdc 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKVideoThumbnailGenerator.swift +++ b/Riot/Modules/MatrixKit/Utils/MXKVideoThumbnailGenerator.swift @@ -67,7 +67,7 @@ public class MXKVideoThumbnailGenerator: NSObject { let image = try assetImageGenerator.copyCGImage(at: .zero, actualTime: nil) thumbnailImage = UIImage(cgImage: image) } catch { - MXLog.error(error.localizedDescription) + MXLog.error("[MXKVideoThumbnailGenerator] generateThumbnail:", context: error) thumbnailImage = nil } diff --git a/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m b/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m index 5ae061284..cbaea4f84 100644 --- a/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m +++ b/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m @@ -192,6 +192,7 @@ static NSAttributedString *verticalWhitespace = nil; NSString *claimedKey = _mxEvent.keysClaimed[@"ed25519"]; NSString *algorithm = _mxEvent.wireContent[@"algorithm"]; NSString *sessionId = _mxEvent.wireContent[@"session_id"]; + NSString *untrusted = _mxEvent.isUntrusted ? [VectorL10n userVerificationSessionsListSessionUntrusted] : [VectorL10n userVerificationSessionsListSessionTrusted]; NSString *decryptionError; if (_mxEvent.decryptionError) @@ -277,6 +278,16 @@ static NSAttributedString *verticalWhitespace = nil; attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont systemFontOfSize:14]}]]; [eventInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]]; + + [eventInformationString appendAttributedString:[[NSMutableAttributedString alloc] + initWithString:[NSString stringWithFormat:@"%@\n", [VectorL10n sslTrust]] + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, + NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; + [eventInformationString appendAttributedString:[[NSMutableAttributedString alloc] + initWithString:untrusted + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, + NSFontAttributeName: [UIFont systemFontOfSize:14]}]]; + [eventInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]]; [textViewAttributedString appendAttributedString:eventInformationString]; } diff --git a/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.h b/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.h deleted file mode 100644 index 874023ebb..000000000 --- a/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXKTableViewCell.h" - -#import "MXKCellRendering.h" - -#import "MXKGroupCellDataStoring.h" - -/** - `MXKGroupTableViewCell` instances display a group. - */ -@interface MXKGroupTableViewCell : MXKTableViewCell -{ -@protected - /** - The current cell data displayed by the table view cell - */ - id groupCellData; -} - -@property (weak, nonatomic) IBOutlet UILabel *groupName; -@property (weak, nonatomic) IBOutlet UILabel *groupDescription; -@property (weak, nonatomic) IBOutlet UILabel *memberCount; - -@end diff --git a/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.m b/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.m deleted file mode 100644 index 641f2c4ac..000000000 --- a/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.m +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "MXKGroupTableViewCell.h" - -#import "NSBundle+MatrixKit.h" - -#import "MXKSwiftHeader.h" - -@implementation MXKGroupTableViewCell -@synthesize delegate; - -#pragma mark - Class methods - -- (void)render:(MXKCellData *)cellData -{ - groupCellData = (id)cellData; - if (groupCellData) - { - // Render the current group values. - _groupName.text = groupCellData.groupDisplayname; - _groupDescription.text = groupCellData.group.profile.shortDescription; - - if (_groupDescription.text.length) - { - _groupDescription.hidden = NO; - } - else - { - // Hide and fill the label with a fake description to harmonize the height of all the cells. - // This is a drawback of the self-sizing cell. - _groupDescription.hidden = YES; - _groupDescription.text = @"No description"; - } - - if (_memberCount) - { - if (groupCellData.group.summary.usersSection.totalUserCountEstimate > 1) - { - _memberCount.text = [VectorL10n numMembersOther:@(groupCellData.group.summary.usersSection.totalUserCountEstimate).stringValue]; - } - else if (groupCellData.group.summary.usersSection.totalUserCountEstimate == 1) - { - _memberCount.text = [VectorL10n numMembersOne:@(1).stringValue]; - } - else - { - _memberCount.text = nil; - } - } - } - else - { - _groupName.text = nil; - _groupDescription.text = nil; - _memberCount.text = nil; - } -} - -- (MXKCellData*)renderedCellData -{ - return groupCellData; -} - -+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth -{ - // The height is fixed - //@TODO: change this to handle dynamic font - return 70; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - - groupCellData = nil; -} - -@end diff --git a/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.xib b/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.xib deleted file mode 100644 index cf6efef01..000000000 --- a/Riot/Modules/MatrixKit/Views/Group/MXKGroupTableViewCell.xib +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index 4282c505d..503441a52 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -205,7 +205,7 @@ typedef enum : NSUInteger UIView *messageComposerContainer; @protected - UIView *inputAccessoryView; + UIView *inputAccessoryViewForKeyboard; } /** @@ -333,7 +333,7 @@ typedef enum : NSUInteger actually used to retrieve the keyboard view. Indeed the keyboard view is the superview of the accessory view when the message composer become the first responder. */ -@property (readonly) UIView *inputAccessoryView; +@property (readonly) UIView *inputAccessoryViewForKeyboard; /** Display the keyboard. diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m index 40d0d5356..2b9b06691 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m @@ -61,7 +61,7 @@ @end @implementation MXKRoomInputToolbarView -@synthesize messageComposerContainer, inputAccessoryView; +@synthesize messageComposerContainer, inputAccessoryViewForKeyboard; + (UINib *)nib { @@ -103,7 +103,7 @@ - (void)dealloc { - inputAccessoryView = nil; + inputAccessoryViewForKeyboard = nil; [self destroy]; } @@ -1197,7 +1197,8 @@ NSString* MXKFileSizes_description(MXKFileSizes sizes) pasteboardImage = [UIImage imageWithData:[dict objectForKey:key]]; } else { - MXLogError(@"[MXKRoomInputToolbarView] Unsupported image format %@ for mimetype %@ pasted.", MIMEType, NSStringFromClass([[dict objectForKey:key] class])); + NSString *message = [NSString stringWithFormat:@"[MXKRoomInputToolbarView] Unsupported image format %@ for mimetype %@ pasted.", MIMEType, NSStringFromClass([[dict objectForKey:key] class])]; + MXLogError(message); } if (pasteboardImage) diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarViewWithSimpleTextView.m b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarViewWithSimpleTextView.m index 3cdb38eda..c48bfda5a 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarViewWithSimpleTextView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarViewWithSimpleTextView.m @@ -30,8 +30,8 @@ [super awakeFromNib]; // Add an accessory view to the text view in order to retrieve keyboard view. - inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero]; - self.messageComposerTextView.inputAccessoryView = self.inputAccessoryView; + inputAccessoryViewForKeyboard = [[UIView alloc] initWithFrame:CGRectZero]; + self.messageComposerTextView.inputAccessoryView = inputAccessoryViewForKeyboard; } -(void)customizeViewRendering diff --git a/Riot/Modules/People/Views/InviteRecentTableViewCell.m b/Riot/Modules/People/Views/InviteRecentTableViewCell.m index ea71c2513..ea42b74a4 100644 --- a/Riot/Modules/People/Views/InviteRecentTableViewCell.m +++ b/Riot/Modules/People/Views/InviteRecentTableViewCell.m @@ -40,12 +40,12 @@ NSString *const kInviteRecentTableViewCellRoomKey = @"kInviteRecentTableViewCell { [super awakeFromNib]; - [self.leftButton.layer setCornerRadius:5]; + [self.leftButton.layer setCornerRadius:8]; self.leftButton.clipsToBounds = YES; [self.leftButton setTitle:[VectorL10n decline] forState:UIControlStateNormal]; [self.leftButton addTarget:self action:@selector(onDeclinePressed:) forControlEvents:UIControlEventTouchUpInside]; - [self.rightButton.layer setCornerRadius:5]; + [self.rightButton.layer setCornerRadius:8]; self.rightButton.clipsToBounds = YES; [self.rightButton setTitle:[VectorL10n accept] forState:UIControlStateNormal]; [self.rightButton addTarget:self action:@selector(onRightButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; @@ -57,8 +57,14 @@ NSString *const kInviteRecentTableViewCellRoomKey = @"kInviteRecentTableViewCell { [super customizeTableViewCellRendering]; - self.leftButton.backgroundColor = ThemeService.shared.theme.tintColor; + self.leftButton.backgroundColor = UIColor.clearColor; + self.leftButton.layer.borderWidth = 1; + self.leftButton.layer.borderColor = ThemeService.shared.theme.colors.alert.CGColor; + self.leftButton.titleLabel.font = ThemeService.shared.theme.fonts.body; + [self.leftButton setTitleColor:ThemeService.shared.theme.colors.alert forState:UIControlStateNormal]; + self.rightButton.backgroundColor = ThemeService.shared.theme.tintColor; + self.rightButton.titleLabel.font = ThemeService.shared.theme.fonts.body; } - (void)prepareForReuse diff --git a/Riot/Modules/People/Views/InviteRecentTableViewCell.xib b/Riot/Modules/People/Views/InviteRecentTableViewCell.xib index 5911b2ba0..4f0594d0e 100644 --- a/Riot/Modules/People/Views/InviteRecentTableViewCell.xib +++ b/Riot/Modules/People/Views/InviteRecentTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -45,50 +45,68 @@ - - + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + - + - + diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index d0c08b588..e25f706e3 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -41,7 +41,6 @@ enum SECTION_TAG_PROMOTION, SECTION_TAG_HISTORY, SECTION_TAG_ADDRESSES, - SECTION_TAG_FLAIR, SECTION_TAG_BANNED_USERS, SECTION_TAG_BANNED_ADVANCED }; @@ -77,12 +76,6 @@ enum ROOM_SETTINGS_HISTORY_VISIBILITY_SECTION_ROW_MEMBERS_ONLY_SINCE_JOINED }; -enum -{ - ROOM_SETTINGS_RELATED_GROUPS_NEW_GROUP, - ROOM_SETTINGS_RELATED_GROUPS_OFFSET = 1000 -}; - enum { ROOM_SETTINGS_ADVANCED_ROOM_ID, @@ -115,8 +108,6 @@ NSString *const kRoomSettingsHistoryVisibilityKey = @"kRoomSettingsHistoryVisibi NSString *const kRoomSettingsNewAliasesKey = @"kRoomSettingsNewAliasesKey"; NSString *const kRoomSettingsRemovedAliasesKey = @"kRoomSettingsRemovedAliasesKey"; NSString *const kRoomSettingsCanonicalAliasKey = @"kRoomSettingsCanonicalAliasKey"; -NSString *const kRoomSettingsNewRelatedGroupKey = @"kRoomSettingsNewRelatedGroupKey"; -NSString *const kRoomSettingsRemovedRelatedGroupKey = @"kRoomSettingsRemovedRelatedGroupKey"; NSString *const kRoomSettingsEncryptionKey = @"kRoomSettingsEncryptionKey"; NSString *const kRoomSettingsEncryptionBlacklistUnverifiedDevicesKey = @"kRoomSettingsEncryptionBlacklistUnverifiedDevicesKey"; @@ -124,7 +115,6 @@ NSString *const kRoomSettingsNameCellViewIdentifier = @"kRoomSettingsNameCellVie NSString *const kRoomSettingsTopicCellViewIdentifier = @"kRoomSettingsTopicCellViewIdentifier"; NSString *const kRoomSettingsWarningCellViewIdentifier = @"kRoomSettingsWarningCellViewIdentifier"; NSString *const kRoomSettingsNewAddressCellViewIdentifier = @"kRoomSettingsNewAddressCellViewIdentifier"; -NSString *const kRoomSettingsNewCommunityCellViewIdentifier = @"kRoomSettingsNewCommunityCellViewIdentifier"; NSString *const kRoomSettingsAddressCellViewIdentifier = @"kRoomSettingsAddressCellViewIdentifier"; NSString *const kRoomSettingsAdvancedCellViewIdentifier = @"kRoomSettingsAdvancedCellViewIdentifier"; NSString *const kRoomSettingsAdvancedEnableE2eCellViewIdentifier = @"kRoomSettingsAdvancedEnableE2eCellViewIdentifier"; @@ -156,10 +146,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti NSUInteger localAddressesCount; UITextField* addAddressTextField; - // Related groups/communities - NSMutableArray *relatedGroups; - UITextField* addGroupTextField; - // The potential image loader MXMediaLoader *uploader; @@ -248,7 +234,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti historyVisibilityTickCells = [[NSMutableDictionary alloc] initWithCapacity:4]; roomAddresses = [NSMutableArray array]; - relatedGroups = [NSMutableArray array]; [self.tableView registerClass:MXKTableViewCellWithLabelAndSwitch.class forCellReuseIdentifier:[MXKTableViewCellWithLabelAndSwitch defaultReuseIdentifier]]; [self.tableView registerClass:MXKTableViewCellWithLabelAndMXKImageView.class forCellReuseIdentifier:[MXKTableViewCellWithLabelAndMXKImageView defaultReuseIdentifier]]; @@ -258,7 +243,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti [self.tableView registerClass:MXKTableViewCellWithLabelAndTextField.class forCellReuseIdentifier:kRoomSettingsNameCellViewIdentifier]; [self.tableView registerClass:TableViewCellWithLabelAndLargeTextView.class forCellReuseIdentifier:kRoomSettingsTopicCellViewIdentifier]; [self.tableView registerClass:MXKTableViewCellWithLabelAndTextField.class forCellReuseIdentifier:kRoomSettingsNewAddressCellViewIdentifier]; - [self.tableView registerClass:MXKTableViewCellWithLabelAndTextField.class forCellReuseIdentifier:kRoomSettingsNewCommunityCellViewIdentifier]; [self.tableView registerClass:UITableViewCell.class forCellReuseIdentifier:kRoomSettingsAddressCellViewIdentifier]; [self.tableView registerClass:UITableViewCell.class forCellReuseIdentifier:kRoomSettingsWarningCellViewIdentifier]; @@ -421,7 +405,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti historyVisibilityTickCells = nil; roomAddresses = nil; - relatedGroups = nil; if (extraEventsListener) { @@ -516,8 +499,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti localAddressesCount++; } - [self refreshRelatedGroups]; - // create sections NSMutableArray *tmpSections = [NSMutableArray arrayWithCapacity:SECTION_TAG_BANNED_ADVANCED + 1]; @@ -606,34 +587,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti [tmpSections addObject:sectionAddresses]; } - if (RiotSettings.shared.roomSettingsScreenShowFlairSettings) - { - Section *sectionFlair = [Section sectionWithTag:SECTION_TAG_FLAIR]; - - for (NSInteger counter = 0; counter < relatedGroups.count; counter++) - { - [sectionFlair addRowWithTag:counter + ROOM_SETTINGS_RELATED_GROUPS_OFFSET]; - } - - if (self.mainSession) - { - // Check user's power level to know whether the user is allowed to add communities to this room - MXRoomPowerLevels *powerLevels = [mxRoomState powerLevels]; - NSInteger oneSelfPowerLevel = [powerLevels powerLevelOfUserWithUserID:self.mainSession.myUser.userId]; - - if (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomRelatedGroups]) - { - [sectionFlair addRowWithTag:ROOM_SETTINGS_RELATED_GROUPS_NEW_GROUP]; - } - } - - sectionFlair.headerTitle = [VectorL10n roomDetailsFlairSection]; - if ([sectionFlair hasAnyRows]) - { - [tmpSections addObject:sectionFlair]; - } - } - if (bannedMembers.count) { Section *sectionBannedUsers = [Section sectionWithTag:SECTION_TAG_BANNED_USERS]; @@ -1156,36 +1109,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti }]; } -- (void)refreshRelatedGroups -{ - // Refresh here the related communities list. - [relatedGroups removeAllObjects]; - [relatedGroups addObjectsFromArray:mxRoomState.relatedGroups]; - NSArray *removedCommunities = updatedItemsDict[kRoomSettingsRemovedRelatedGroupKey]; - if (removedCommunities.count) - { - for (NSUInteger index = 0; index < relatedGroups.count;) - { - NSString *groupId = relatedGroups[index]; - - // Check whether the user did not remove it - if ([removedCommunities indexOfObject:groupId] != NSNotFound) - { - [relatedGroups removeObjectAtIndex:index]; - } - else - { - index++; - } - } - } - NSArray *communities = updatedItemsDict[kRoomSettingsNewRelatedGroupKey]; - if (communities) - { - [relatedGroups addObjectsFromArray:communities]; - } -} - #pragma mark - UITextViewDelegate - (void)textViewDidBeginEditing:(UITextView *)textView; @@ -1347,18 +1270,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti addAddressTextField.text = nil; } } - else if (textField == addGroupTextField) - { - // Dismiss the keyboard - [addGroupTextField resignFirstResponder]; - - NSString *groupId = addGroupTextField.text; - if (!groupId.length || [self addCommunity:groupId]) - { - // Reset the input field - addGroupTextField.text = nil; - } - } return YES; } @@ -1920,52 +1831,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti return; } - - // Related groups - if (updatedItemsDict[kRoomSettingsNewRelatedGroupKey] || updatedItemsDict[kRoomSettingsRemovedRelatedGroupKey]) - { - [self refreshRelatedGroups]; - - pendingOperation = [mxRoom setRelatedGroups:relatedGroups success:^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->pendingOperation = nil; - - [self->updatedItemsDict removeObjectForKey:kRoomSettingsNewRelatedGroupKey]; - [self->updatedItemsDict removeObjectForKey:kRoomSettingsRemovedRelatedGroupKey]; - - [self onSave:nil]; - } - - } failure:^(NSError *error) { - - MXLogDebug(@"[RoomSettingsViewController] Update room communities failed"); - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->pendingOperation = nil; - - dispatch_async(dispatch_get_main_queue(), ^{ - - NSString* message = error.localizedDescription; - if (!message.length) - { - message = [VectorL10n roomDetailsFailToUpdateRoomCommunities]; - } - [self onSaveFailed:message withKeys:@[kRoomSettingsNewRelatedGroupKey,kRoomSettingsRemovedRelatedGroupKey]]; - - }); - } - - }]; - - return; - } } // Update here other room settings @@ -2793,64 +2658,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti cell = addressCell; } } - else if (section == SECTION_TAG_FLAIR) - { - if (row == ROOM_SETTINGS_RELATED_GROUPS_NEW_GROUP) - { - MXKTableViewCellWithLabelAndTextField *addCommunityCell = [tableView dequeueReusableCellWithIdentifier:kRoomSettingsNewCommunityCellViewIdentifier forIndexPath:indexPath]; - - // Retrieve the current edited value if any - NSString *currentValue = (addGroupTextField ? addGroupTextField.text : nil); - - addCommunityCell.mxkLabelLeadingConstraint.constant = 0; - addCommunityCell.mxkTextFieldLeadingConstraint.constant = tableView.vc_separatorInset.left; - addCommunityCell.mxkTextFieldTrailingConstraint.constant = 15; - - addCommunityCell.mxkLabel.text = nil; - - addCommunityCell.accessoryType = UITableViewCellAccessoryNone; - addCommunityCell.accessoryView = [[UIImageView alloc] initWithImage:[AssetImages.plusIcon.image vc_tintedImageUsingColor:ThemeService.shared.theme.textPrimaryColor]]; - - addGroupTextField = addCommunityCell.mxkTextField; - addGroupTextField.placeholder = [VectorL10n roomDetailsNewFlairPlaceholder:self.mainSession.matrixRestClient.homeserverSuffix]; - addGroupTextField.attributedPlaceholder = [[NSAttributedString alloc] - initWithString:addGroupTextField.placeholder - attributes:@{NSForegroundColorAttributeName: ThemeService.shared.theme.placeholderTextColor}]; - addGroupTextField.userInteractionEnabled = YES; - addGroupTextField.text = currentValue; - addGroupTextField.textColor = ThemeService.shared.theme.textSecondaryColor; - - addGroupTextField.tintColor = ThemeService.shared.theme.tintColor; - addGroupTextField.font = [UIFont systemFontOfSize:17]; - addGroupTextField.borderStyle = UITextBorderStyleNone; - addGroupTextField.textAlignment = NSTextAlignmentLeft; - - addGroupTextField.autocorrectionType = UITextAutocorrectionTypeNo; - addGroupTextField.spellCheckingType = UITextSpellCheckingTypeNo; - addGroupTextField.delegate = self; - - cell = addCommunityCell; - } - else if (row >= ROOM_SETTINGS_RELATED_GROUPS_OFFSET) - { - UITableViewCell *communityCell = [tableView dequeueReusableCellWithIdentifier:kRoomSettingsAddressCellViewIdentifier forIndexPath:indexPath]; - - communityCell.textLabel.font = [UIFont systemFontOfSize:16]; - communityCell.textLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - communityCell.textLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; - communityCell.accessoryView = nil; - communityCell.accessoryType = UITableViewCellAccessoryNone; - communityCell.selectionStyle = UITableViewCellSelectionStyleNone; - - NSInteger index = row - ROOM_SETTINGS_RELATED_GROUPS_OFFSET; - - if (index < relatedGroups.count) - { - communityCell.textLabel.text = relatedGroups[index]; - } - cell = communityCell; - } - } else if (section == SECTION_TAG_BANNED_USERS) { UITableViewCell *addressCell = [tableView dequeueReusableCellWithIdentifier:kRoomSettingsAddressCellViewIdentifier forIndexPath:indexPath]; @@ -3171,18 +2978,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti }); } } - else if (section == SECTION_TAG_FLAIR) - { - if (row == ROOM_SETTINGS_RELATED_GROUPS_NEW_GROUP) - { - NSString *groupId = addGroupTextField.text; - if (!groupId.length || [self addCommunity:groupId]) - { - // Reset the input field - addGroupTextField.text = nil; - } - } - } else if (section == SECTION_TAG_BANNED_USERS) { // Show the RoomMemberDetailsViewController on this member so that @@ -3240,27 +3035,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti swipeActionConfiguration.performsFirstActionWithFullSwipe = NO; return swipeActionConfiguration; } - else if (section == SECTION_TAG_FLAIR && row >= ROOM_SETTINGS_RELATED_GROUPS_OFFSET) - { - UIContextualAction *removeAddressAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive - title:@" " - handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) { - [self removeCommunityAtIndexPath:indexPath]; - completionHandler(YES); - }]; - removeAddressAction.backgroundColor = ThemeService.shared.theme.headerBackgroundColor; - removeAddressAction.image = [AssetImages.removeIcon.image vc_notRenderedImage]; - - // Create swipe action configuration - - NSArray *actions = @[ - removeAddressAction - ]; - - UISwipeActionsConfiguration *swipeActionConfiguration = [UISwipeActionsConfiguration configurationWithActions:actions]; - swipeActionConfiguration.performsFirstActionWithFullSwipe = NO; - return swipeActionConfiguration; - } return nil; } @@ -3693,20 +3467,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti } } -- (void)removeCommunityAtIndexPath:(NSIndexPath *)indexPath -{ - indexPath = [_tableViewSections tagsIndexPathFromTableViewIndexPath:indexPath]; - NSInteger row = indexPath.row; - - NSInteger index = row - ROOM_SETTINGS_RELATED_GROUPS_OFFSET; - - if (index < relatedGroups.count) - { - NSString *groupId = relatedGroups[index]; - [self removeCommunity:groupId]; - } -} - - (void)removeRoomAlias:(NSString*)roomAlias { NSString *canonicalAlias; @@ -3762,36 +3522,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti } } -- (void)removeCommunity:(NSString*)groupId -{ - // Check whether the alias has just been added - NSMutableArray *addedGroup = updatedItemsDict[kRoomSettingsNewRelatedGroupKey]; - if (addedGroup && [addedGroup indexOfObject:groupId] != NSNotFound) - { - [addedGroup removeObject:groupId]; - - if (!addedGroup.count) - { - [updatedItemsDict removeObjectForKey:kRoomSettingsNewRelatedGroupKey]; - } - } - else - { - NSMutableArray *removedGroup = updatedItemsDict[kRoomSettingsRemovedRelatedGroupKey]; - if (!removedGroup) - { - removedGroup = [NSMutableArray array]; - updatedItemsDict[kRoomSettingsRemovedRelatedGroupKey] = removedGroup; - } - - [removedGroup addObject:groupId]; - } - - [self updateSections]; - - [self getNavigationItem].rightBarButtonItem.enabled = (updatedItemsDict.count != 0); -} - - (BOOL)addRoomAlias:(NSString*)roomAlias { // Check whether the provided alias is valid @@ -3873,71 +3603,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti return NO; } -- (BOOL)addCommunity:(NSString*)groupId -{ - // Check whether the provided id is valid - if ([MXTools isMatrixGroupIdentifier:groupId]) - { - // Check whether this group has just been deleted - NSMutableArray *removedGroups = updatedItemsDict[kRoomSettingsRemovedRelatedGroupKey]; - if (removedGroups && [removedGroups indexOfObject:groupId] != NSNotFound) - { - [removedGroups removeObject:groupId]; - - if (!removedGroups.count) - { - [updatedItemsDict removeObjectForKey:kRoomSettingsRemovedRelatedGroupKey]; - } - } - // Check whether this alias is not already defined for this room - else if ([relatedGroups indexOfObject:groupId] == NSNotFound) - { - NSMutableArray *addedGroup = updatedItemsDict[kRoomSettingsNewRelatedGroupKey]; - if (!addedGroup) - { - addedGroup = [NSMutableArray array]; - updatedItemsDict[kRoomSettingsNewRelatedGroupKey] = addedGroup; - } - - [addedGroup addObject:groupId]; - } - - [self updateSections]; - - [self getNavigationItem].rightBarButtonItem.enabled = (updatedItemsDict.count != 0); - - return YES; - } - - // Prompt here user for invalid id - __weak typeof(self) weakSelf = self; - - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - NSString *alertMsg = [VectorL10n roomDetailsFlairInvalidIdPromptMsg:groupId]; - - currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomDetailsFlairInvalidIdPromptTitle] - message:alertMsg - preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"RoomSettingsVCAddCommunityAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; - - return NO; -} - #pragma mark - TableViewCellWithCheckBoxesDelegate - (void)tableViewCellWithCheckBoxes:(TableViewCellWithCheckBoxes *)tableViewCellWithCheckBoxes didTapOnCheckBoxAtIndex:(NSUInteger)index diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h index 15e18f89b..fca4f31aa 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h @@ -304,11 +304,6 @@ extern NSString *const kMXKRoomBubbleCellUrlItemInteraction; */ - (void)prepareRender:(MXKCellData*)cellData; -/** - Refresh the flair information added to the sender display name. - */ -- (void)renderSenderFlair; - /** Highlight text message related to a specific event in the displayed message. diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m index fe4a3558d..13eac3bcc 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m @@ -497,16 +497,7 @@ static BOOL _disableLongPressGestureOnEvent; // Display sender's name except if the name appears in the displayed text (see emote and membership events) if (bubbleData.shouldHideSenderName == NO) { - if (bubbleData.senderFlair) - { - [self renderSenderFlair]; - } - else - { - self.userNameLabel.text = bubbleData.senderDisplayName; - } - - + self.userNameLabel.text = bubbleData.senderDisplayName; self.userNameLabel.hidden = NO; self.userNameTapGestureMaskView.userInteractionEnabled = YES; } @@ -800,75 +791,6 @@ static BOOL _disableLongPressGestureOnEvent; mxkCellData = cellData; } -- (void)renderSenderFlair -{ - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ ", bubbleData.senderDisplayName]]; - - NSUInteger index = 0; - - for (MXGroup *group in bubbleData.senderFlair) - { - NSString *mxcAvatarURI = group.profile.avatarUrl; - NSString *cacheFilePath = [MXMediaManager thumbnailCachePathForMatrixContentURI:mxcAvatarURI andType:@"image/jpeg" inFolder:kMXMediaManagerDefaultCacheFolder toFitViewSize:CGSizeMake(12, 12) withMethod:MXThumbnailingMethodCrop]; - - // Check whether the avatar url is valid - if (cacheFilePath) - { - UIImage *image = [MXMediaManager loadThroughCacheWithFilePath:cacheFilePath]; - if (image) - { - NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init]; - textAttachment.image = [MXKTools resizeImageWithRoundedCorners:image toSize:CGSizeMake(12, 12)]; - NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment]; - [attributedString appendAttributedString:attrStringWithImage]; - [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]]; - } - else - { - NSString *downloadId = [MXMediaManager thumbnailDownloadIdForMatrixContentURI:mxcAvatarURI - inFolder:kMXMediaManagerDefaultCacheFolder - toFitViewSize:CGSizeMake(12, 12) - withMethod:MXThumbnailingMethodCrop]; - // Check whether the download is in progress. - MXMediaLoader* loader = [MXMediaManager existingDownloaderWithIdentifier:downloadId]; - if (loader) - { - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXMediaLoaderStateDidChangeNotification object:loader]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onFlairDownloadStateChange:) name:kMXMediaLoaderStateDidChangeNotification object:loader]; - } - else - { - MXWeakify(self); - [bubbleData.mxSession.mediaManager downloadThumbnailFromMatrixContentURI:mxcAvatarURI - withType:@"image/jpeg" - inFolder:kMXMediaManagerDefaultCacheFolder - toFitViewSize:CGSizeMake(12, 12) - withMethod:MXThumbnailingMethodCrop - success:^(NSString *outputFilePath) { - // Refresh sender flair - MXStrongifyAndReturnIfNil(self); - [self renderSenderFlair]; - } - failure:nil]; - } - } - - index++; - if (index == 3) - { - if (bubbleData.senderFlair.count > 3) - { - NSAttributedString *more = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"+%tu", (bubbleData.senderFlair.count - 3)] attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:11.0], NSBaselineOffsetAttributeName:@(+2)}]; - [attributedString appendAttributedString:more]; - } - break; - } - } - } - - self.userNameLabel.attributedText = attributedString; -} - - (void)renderGif { if (self.attachmentView && bubbleData.attachment) @@ -1292,23 +1214,6 @@ static BOOL _disableLongPressGestureOnEvent; } } -- (void)onFlairDownloadStateChange:(NSNotification *)notif -{ - MXMediaLoader *loader = (MXMediaLoader*)notif.object; - switch (loader.state) { - case MXMediaLoaderStateDownloadCompleted: - [self renderSenderFlair]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXMediaLoaderStateDidChangeNotification object:loader]; - break; - case MXMediaLoaderStateDownloadFailed: - case MXMediaLoaderStateCancelled: - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXMediaLoaderStateDidChangeNotification object:loader]; - break; - default: - break; - } -} - - (void)startProgressUI { self.progressView.hidden = YES; diff --git a/Riot/Modules/Room/TimelineCells/LocationView/RoomTimelineLocationView.swift b/Riot/Modules/Room/TimelineCells/LocationView/RoomTimelineLocationView.swift index 12c95b794..e33134bf4 100644 --- a/Riot/Modules/Room/TimelineCells/LocationView/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/TimelineCells/LocationView/RoomTimelineLocationView.swift @@ -396,7 +396,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { - MXLog.error("[RoomTimelineLocationView] Failed to load map with error: \(error)") + MXLog.error("[RoomTimelineLocationView] Failed to load map", context: error) self.isMapViewLoadingFailed = true } diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Sticker/RoomSelectedStickerBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Sticker/RoomSelectedStickerBubbleCell.m index cc0506fd2..110515638 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Sticker/RoomSelectedStickerBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Sticker/RoomSelectedStickerBubbleCell.m @@ -102,15 +102,7 @@ // Display sender's name except if the name appears in the displayed text (see emote and membership events) if (bubbleData.shouldHideSenderName == NO) { - if (bubbleData.senderFlair) - { - [self renderSenderFlair]; - } - else - { - self.userNameLabel.text = bubbleData.senderDisplayName; - } - + self.userNameLabel.text = bubbleData.senderDisplayName; self.userNameLabel.hidden = NO; self.userNameTapGestureMaskView.userInteractionEnabled = YES; } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 106fa3bb3..cd57bd0ed 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -80,10 +80,9 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [self updateUIWithAttributedTextMessage:nil animated:NO]; self.textView.toolbarDelegate = self; - - // Add an accessory view to the text view in order to retrieve keyboard view. - inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero]; - self.textView.inputAccessoryView = inputAccessoryView; + + inputAccessoryViewForKeyboard = [[UIView alloc] initWithFrame:CGRectZero]; + self.textView.inputAccessoryView = inputAccessoryViewForKeyboard; } - (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 6c6200298..c7e52e89a 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -107,7 +107,7 @@ class VoiceMessageAttachmentCacheManager { try setupTemporaryFilesFolder() } catch { completion(Result.failure(VoiceMessageAttachmentCacheManagerError.preparationError(error))) - MXLog.error("[VoiceMessageAttachmentCacheManager] Failed creating temporary files folder with error: \(error)") + MXLog.error("[VoiceMessageAttachmentCacheManager] Failed creating temporary files folder", context: error) return } @@ -140,7 +140,7 @@ class VoiceMessageAttachmentCacheManager { do { try FileManager.default.removeItem(at: temporaryFilesFolderURL) } catch { - MXLog.error("[VoiceMessageAttachmentCacheManager] Failed clearing cached disk files with error: \(error)") + MXLog.error("[VoiceMessageAttachmentCacheManager] Failed clearing cached disk files", context: error) } } @@ -176,7 +176,7 @@ class VoiceMessageAttachmentCacheManager { }, failure: { error in // A nil error in this case is a cancellation on the MXMediaLoader if let error = error { - MXLog.error("[VoiceMessageAttachmentCacheManager] Failed decrypting attachment with error: \(String(describing: error))") + MXLog.error("[VoiceMessageAttachmentCacheManager] Failed decrypting attachment", context: error) self.invokeFailureCallbacksForIdentifier(identifier, requiredNumberOfSamples: numberOfSamples, error: VoiceMessageAttachmentCacheManagerError.decryptionError(error)) } semaphore.signal() @@ -189,7 +189,7 @@ class VoiceMessageAttachmentCacheManager { }, failure: { error in // A nil error in this case is a cancellation on the MXMediaLoader if let error = error { - MXLog.error("[VoiceMessageAttachmentCacheManager] Failed preparing attachment with error: \(String(describing: error))") + MXLog.error("[VoiceMessageAttachmentCacheManager] Failed preparing attachment", context: error) self.invokeFailureCallbacksForIdentifier(identifier, requiredNumberOfSamples: numberOfSamples, error: VoiceMessageAttachmentCacheManagerError.preparationError(error)) } semaphore.signal() @@ -232,14 +232,14 @@ class VoiceMessageAttachmentCacheManager { semaphore.signal() } case .failure(let error): - MXLog.error("[VoiceMessageAttachmentCacheManager] Failed retrieving audio duration with error: \(error)") + MXLog.error("[VoiceMessageAttachmentCacheManager] Failed retrieving audio duration", context: error) self.invokeFailureCallbacksForIdentifier(identifier, requiredNumberOfSamples: numberOfSamples, error: VoiceMessageAttachmentCacheManagerError.durationError(error)) semaphore.signal() } } } case .failure(let error): - MXLog.error("[VoiceMessageAttachmentCacheManager] Failed converting voice message with error: \(error)") + MXLog.error("[VoiceMessageAttachmentCacheManager] Failed converting voice message", context: error) self.invokeFailureCallbacksForIdentifier(identifier, requiredNumberOfSamples: numberOfSamples, error: VoiceMessageAttachmentCacheManagerError.conversionError(error)) semaphore.signal() } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 1ee5288d1..85cae9941 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -308,7 +308,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, MXLog.error("[VoiceMessageController] Failed retrieving media duration") } case .failure(let error): - MXLog.error("[VoiceMessageController] Failed getting audio duration with: \(error)") + MXLog.error("[VoiceMessageController] Failed getting audio duration", context: error) } dispatchGroup.leave() @@ -336,7 +336,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, case .success: finalURL = destinationURL case .failure(let error): - MXLog.error("Failed failed encoding audio message with: \(error)") + MXLog.error("Failed failed encoding audio message", context: error) } dispatchGroup.leave() @@ -371,7 +371,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, do { try FileManager.default.removeItem(at: url) } catch { - MXLog.error(error) + MXLog.error("[VoiceMessageController] deleteRecordingAtURL:", context: error) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 36a894adf..9bc0b705f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -137,7 +137,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) { state = .error - MXLog.error("Failed playing voice message with error: \(error)") + MXLog.error("Failed playing voice message", context: error) } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift index b4c04c610..8e2c1a529 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift @@ -96,8 +96,7 @@ final class ServiceTermsModalCoordinator: NSObject, ServiceTermsModalCoordinator // Disable IS feature on user's account session.setIdentityServer(nil, andAccessToken: nil) session.setAccountDataIdentityServer(nil, success: nil) { error in - guard let errorDescription = error?.localizedDescription else { return } - MXLog.error("[ServiceTermsModalCoordinator] IS Terms: Error: \(errorDescription)") + MXLog.error("[ServiceTermsModalCoordinator] IS Terms", context: error) } } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 93d20bad4..6b9a19f97 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -36,9 +36,6 @@ #import "ThemeService.h" #import "TableViewCellWithPhoneNumberTextField.h" -#import "GroupsDataSource.h" -#import "GroupTableViewCellWithSwitch.h" - #import "GBDeviceInfo_iOS.h" #import "MediaPickerViewController.h" @@ -69,7 +66,6 @@ typedef NS_ENUM(NSUInteger, SECTION_TAG) SECTION_TAG_ADVANCED, SECTION_TAG_ABOUT, SECTION_TAG_LABS, - SECTION_TAG_FLAIR, SECTION_TAG_DEACTIVATE_ACCOUNT }; @@ -182,13 +178,14 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) typedef NS_ENUM(NSUInteger, SECURITY) { SECURITY_BUTTON_INDEX = 0, + DEVICE_MANAGER_INDEX }; typedef void (^blockSettingsViewController_onReadyToDestroy)(void); #pragma mark - SettingsViewController -@interface SettingsViewController () CountryPickerViewController *newPhoneNumberCountryPicker; NBPhoneNumber *newPhoneNumber; - // Flair: the groups data source - GroupsDataSource *groupsDataSource; - // Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar. __weak id kAppDelegateDidTapStatusBarNotificationObserver; @@ -291,6 +285,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> @property (nonatomic, strong) ThreadsBetaCoordinatorBridgePresenter *threadsBetaBridgePresenter; @property (nonatomic, strong) ChangePasswordCoordinatorBridgePresenter *changePasswordBridgePresenter; +@property (nonatomic, strong) UserSessionsFlowCoordinatorBridgePresenter *userSessionsFlowCoordinatorBridgePresenter; /** Whether or not to check for contacts access after the user accepts the service terms. The value of this property is @@ -407,6 +402,13 @@ ChangePasswordCoordinatorBridgePresenterDelegate> Section *sectionSecurity = [Section sectionWithTag:SECTION_TAG_SECURITY]; [sectionSecurity addRowWithTag:SECURITY_BUTTON_INDEX]; + + if (BuildSettings.deviceManagerEnabled) + { + // NOTE: Add device manager entry point in the security section atm for debug purpose + [sectionSecurity addRowWithTag:DEVICE_MANAGER_INDEX]; + } + sectionSecurity.headerTitle = [VectorL10n settingsSecurity]; [tmpSections addObject:sectionSecurity]; @@ -600,19 +602,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } } - if ([groupsDataSource numberOfSectionsInTableView:self.tableView] && groupsDataSource.joinedGroupsSection != -1) - { - NSInteger count = [groupsDataSource tableView:self.tableView - numberOfRowsInSection:groupsDataSource.joinedGroupsSection]; - Section *sectionFlair = [Section sectionWithTag:SECTION_TAG_FLAIR]; - for (NSInteger index = 0; index < count; index++) - { - [sectionFlair addRowWithTag:index]; - } - sectionFlair.headerTitle = [VectorL10n settingsFlair]; - [tmpSections addObject:sectionFlair]; - } - if (BuildSettings.settingsScreenAllowDeactivatingAccount) { Section *sectionDeactivate = [Section sectionWithTag:SECTION_TAG_DEACTIVATE_ACCOUNT]; @@ -637,7 +626,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [self.tableView registerClass:MXKTableViewCellWithLabelAndSwitch.class forCellReuseIdentifier:[MXKTableViewCellWithLabelAndSwitch defaultReuseIdentifier]]; [self.tableView registerClass:MXKTableViewCellWithLabelAndMXKImageView.class forCellReuseIdentifier:[MXKTableViewCellWithLabelAndMXKImageView defaultReuseIdentifier]]; [self.tableView registerClass:TableViewCellWithPhoneNumberTextField.class forCellReuseIdentifier:[TableViewCellWithPhoneNumberTextField defaultReuseIdentifier]]; - [self.tableView registerClass:GroupTableViewCellWithSwitch.class forCellReuseIdentifier:[GroupTableViewCellWithSwitch defaultReuseIdentifier]]; [self.tableView registerNib:MXKTableViewCellWithTextView.nib forCellReuseIdentifier:[MXKTableViewCellWithTextView defaultReuseIdentifier]]; [self.tableView registerNib:SectionFooterView.nib forHeaderFooterViewReuseIdentifier:[SectionFooterView defaultReuseIdentifier]]; @@ -694,10 +682,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } [self setupDiscoverySection]; - - groupsDataSource = [[GroupsDataSource alloc] initWithMatrixSession:self.mainSession]; - [groupsDataSource finalizeInitialization]; - groupsDataSource.delegate = self; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(onSave:)]; self.navigationItem.rightBarButtonItem.accessibilityIdentifier=@"SettingsVCNavBarSaveButton"; @@ -753,13 +737,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> - (void)destroy { - if (groupsDataSource) - { - groupsDataSource.delegate = nil; - [groupsDataSource destroy]; - groupsDataSource = nil; - } - // Release the potential pushed view controller [self releasePushedViewController]; @@ -2556,32 +2533,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell = [self buildLiveLocationSharingCellForTableView:tableView atIndexPath:indexPath]; } } - else if (section == SECTION_TAG_FLAIR) - { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:groupsDataSource.joinedGroupsSection]; - cell = [groupsDataSource tableView:tableView cellForRowAtIndexPath:indexPath]; - - if ([cell isKindOfClass:GroupTableViewCellWithSwitch.class]) - { - GroupTableViewCellWithSwitch* groupWithSwitchCell = (GroupTableViewCellWithSwitch*)cell; - id groupCellData = [groupsDataSource cellDataAtIndex:indexPath]; - - // Display the groupId in the description label, except if the group has no name - if (![groupWithSwitchCell.groupName.text isEqualToString:groupCellData.group.groupId]) - { - groupWithSwitchCell.groupDescription.hidden = NO; - groupWithSwitchCell.groupDescription.text = groupCellData.group.groupId; - } - - // Update the toogle button - groupWithSwitchCell.toggleButton.on = groupCellData.group.summary.user.isPublicised; - groupWithSwitchCell.toggleButton.enabled = YES; - groupWithSwitchCell.toggleButton.tag = row; - - [groupWithSwitchCell.toggleButton removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; - [groupWithSwitchCell.toggleButton addTarget:self action:@selector(toggleCommunityFlair:) forControlEvents:UIControlEventTouchUpInside]; - } - } else if (section == SECTION_TAG_SECURITY) { switch (row) @@ -2591,6 +2542,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell.textLabel.text = [VectorL10n securitySettingsTitle]; [cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; break; + case DEVICE_MANAGER_INDEX: + cell = [self getDefaultTableViewCell:tableView]; + cell.textLabel.text = [VectorL10n userSessionsSettings]; + [cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; + break; } } else if (section == SECTION_TAG_DEACTIVATE_ACCOUNT) @@ -2943,6 +2899,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [self pushViewController:securityViewController]; break; } + case DEVICE_MANAGER_INDEX: + { + [self showUserSessionsFlow]; + break; + } } } else if (section == SECTION_TAG_NOTIFICATIONS) @@ -3326,43 +3287,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.pinRoomsWithUnreadMessagesOnHome = sender.on; } -- (void)toggleCommunityFlair:(UISwitch *)sender -{ - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:sender.tag inSection:groupsDataSource.joinedGroupsSection]; - id groupCellData = [groupsDataSource cellDataAtIndex:indexPath]; - MXGroup *group = groupCellData.group; - - if (group) - { - [self startActivityIndicator]; - - __weak typeof(self) weakSelf = self; - - [self.mainSession updateGroupPublicity:group isPublicised:sender.isOn success:^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - } - - } failure:^(NSError *error) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - - // Come back to previous state button - [sender setOn:!sender.isOn animated:YES]; - - // Notify user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - } - }]; - } -} - - (void)toggleUseOnlyLatestUserAvatarAndName:(UISwitch *)sender { RiotSettings.shared.roomScreenUseOnlyLatestUserAvatarAndName = sender.isOn; @@ -4170,25 +4094,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } } -#pragma mark - MXKDataSourceDelegate - -- (Class)cellViewClassForCellData:(MXKCellData*)cellData -{ - // Return the class used to display a group with a toogle button - return GroupTableViewCellWithSwitch.class; -} - -- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData -{ - return GroupTableViewCellWithSwitch.defaultReuseIdentifier; -} - -- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes -{ - // Group data has been updated. Do a simple full reload - [self refreshSettings]; -} - #pragma mark - DeactivateAccountViewControllerDelegate - (void)deactivateAccountViewControllerDidDeactivateWithSuccess:(DeactivateAccountViewController *)deactivateAccountViewController @@ -4637,4 +4542,35 @@ ChangePasswordCoordinatorBridgePresenterDelegate> self.changePasswordBridgePresenter = nil; } +#pragma mark - User sessions management + +- (void)showUserSessionsFlow +{ + if (!self.mainSession) + { + MXLogError(@"[SettingsViewController] Cannot show user sessions flow, no user session available"); + return; + } + + if (!self.navigationController) + { + MXLogError(@"[SettingsViewController] Cannot show user sessions flow, no navigation controller available"); + return; + } + + UserSessionsFlowCoordinatorBridgePresenter *userSessionsFlowCoordinatorBridgePresenter = [[UserSessionsFlowCoordinatorBridgePresenter alloc] initWithMxSession:self.mainSession]; + + MXWeakify(self); + + userSessionsFlowCoordinatorBridgePresenter.completion = ^{ + MXStrongifyAndReturnIfNil(self); + + self.userSessionsFlowCoordinatorBridgePresenter = nil; + }; + + self.userSessionsFlowCoordinatorBridgePresenter = userSessionsFlowCoordinatorBridgePresenter; + + [self.userSessionsFlowCoordinatorBridgePresenter pushFrom:self.navigationController animated:YES]; +} + @end diff --git a/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.h b/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.h deleted file mode 100644 index d67440bf0..000000000 --- a/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.h +++ /dev/null @@ -1,26 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupTableViewCell.h" - -/** - `GroupTableViewCellWithSwitch` instances display a group with a toggle button. - */ -@interface GroupTableViewCellWithSwitch : GroupTableViewCell - -@property (strong, nonatomic) IBOutlet UISwitch *toggleButton; - -@end diff --git a/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.m b/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.m deleted file mode 100644 index e29804a37..000000000 --- a/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.m +++ /dev/null @@ -1,21 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "GroupTableViewCellWithSwitch.h" - -@implementation GroupTableViewCellWithSwitch - -@end diff --git a/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.xib b/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.xib deleted file mode 100644 index cb141c781..000000000 --- a/Riot/Modules/Settings/Views/GroupTableViewCellWithSwitch.xib +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/SideMenu/SideMenuCoordinator.swift b/Riot/Modules/SideMenu/SideMenuCoordinator.swift index a0ffbd62c..86e902f3d 100644 --- a/Riot/Modules/SideMenu/SideMenuCoordinator.swift +++ b/Riot/Modules/SideMenu/SideMenuCoordinator.swift @@ -320,7 +320,9 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { func showSpaceInvite(spaceId: String, session: MXSession) { guard let space = session.spaceService.getSpace(withId: spaceId), let spaceRoom = space.room else { - MXLog.error("[SideMenuCoordinator] showSpaceInvite: failed to find space with id \(spaceId)") + MXLog.error("[SideMenuCoordinator] showSpaceInvite: failed to find space", context: [ + "space_id": spaceId + ]) return } diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailCoordinator.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailCoordinator.swift new file mode 100644 index 000000000..54a2f499a --- /dev/null +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailCoordinator.swift @@ -0,0 +1,90 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +struct SpaceDetailCoordinatorParameters { + let spaceId: String + let session: MXSession + let showCancel: Bool +} + +enum SpaceDetailCoordinatorResult { + case cancel + case dismiss + case open + case join +} + +/// Space detail screen +final class SpaceDetailCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: SpaceDetailCoordinatorParameters + private var viewModel: SpaceDetailViewModel! + private let viewController: SpaceDetailViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((SpaceDetailCoordinatorResult) -> Void)? + + // MARK: - Setup + + init(parameters: SpaceDetailCoordinatorParameters) { + self.parameters = parameters + + let viewModel = SpaceDetailViewModel(session: parameters.session, spaceId: parameters.spaceId) + let viewController = SpaceDetailViewController.instantiate(mediaManager: parameters.session.mediaManager, viewModel: viewModel, showCancel: parameters.showCancel) + self.viewModel = viewModel + self.viewController = viewController + } + + // MARK: - Public methods + + func start() { + self.viewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.viewController + } +} + +// MARK: - SpaceDetailModelViewModelCoordinatorDelegate + +extension SpaceDetailCoordinator: SpaceDetailModelViewModelCoordinatorDelegate { + func spaceDetailViewModelDidJoin(_ viewModel: SpaceDetailViewModelType) { + completion?(.join) + } + + func spaceDetailViewModelDidOpen(_ viewModel: SpaceDetailViewModelType) { + completion?(.open) + } + + func spaceDetailViewModelDidCancel(_ viewModel: SpaceDetailViewModelType) { + completion?(.cancel) + } + + func spaceDetailViewModelDidDismiss(_ viewModel: SpaceDetailViewModelType) { + completion?(.dismiss) + } +} diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailPresenter.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailPresenter.swift index ab340fbc0..30302d982 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailPresenter.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailPresenter.swift @@ -84,7 +84,7 @@ class SpaceDetailPresenter: NSObject { // MARK: - Private private func show(with session: MXSession) { - let viewController = SpaceDetailViewController.instantiate(mediaManager: session.mediaManager, viewModel: self.viewModel) + let viewController = SpaceDetailViewController.instantiate(mediaManager: session.mediaManager, viewModel: self.viewModel, showCancel: true) self.present(viewController, animated: true) } diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift index bbd8b5a90..4557f829c 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift @@ -34,6 +34,7 @@ class SpaceDetailViewController: UIViewController { private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! private var isJoined: Bool = false + private var showCancel: Bool = true // MARK: Outlets @@ -60,11 +61,12 @@ class SpaceDetailViewController: UIViewController { // MARK: - Setup - class func instantiate(mediaManager: MXMediaManager, viewModel: SpaceDetailViewModelType!) -> SpaceDetailViewController { + class func instantiate(mediaManager: MXMediaManager, viewModel: SpaceDetailViewModelType!, showCancel: Bool) -> SpaceDetailViewController { let viewController = StoryboardScene.SpaceDetailViewController.initialScene.instantiate() viewController.mediaManager = mediaManager viewController.viewModel = viewModel viewController.theme = ThemeService.shared().theme + viewController.showCancel = showCancel return viewController } @@ -176,6 +178,7 @@ class SpaceDetailViewController: UIViewController { private func setupViews() { self.closeButton.layer.masksToBounds = true self.closeButton.layer.cornerRadius = self.closeButton.bounds.height / 2 + self.closeButton.isHidden = !self.showCancel self.setup(button: self.joinButton, withTitle: VectorL10n.join) self.setup(button: self.acceptButton, withTitle: VectorL10n.accept) @@ -209,13 +212,16 @@ class SpaceDetailViewController: UIViewController { switch parameters.membership { case .invite: + self.title = VectorL10n.spaceInviteNavTitle self.joinButton.isHidden = true self.inviteActionPanel.isHidden = false case .join: + self.title = VectorL10n.spaceDetailNavTitle self.inviterPanelHeight.constant = 0 self.joinButton.setTitle(VectorL10n.open, for: .normal) self.isJoined = true default: + self.title = VectorL10n.spaceDetailNavTitle self.inviterPanelHeight.constant = 0 } diff --git a/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift b/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift index f9dde8858..e5d8252f0 100644 --- a/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift @@ -156,7 +156,9 @@ extension SpaceMembersCoordinator: SpaceMemberListCoordinatorDelegate { func spaceMemberListCoordinatorShowInvite(_ coordinator: SpaceMemberListCoordinatorType) { guard let space = parameters.session.spaceService.getSpace(withId: parameters.spaceId), let spaceRoom = space.room else { - MXLog.error("[SpaceMembersCoordinator] spaceMemberListCoordinatorShowInvite: failed to find space with id \(parameters.spaceId)") + MXLog.error("[SpaceMembersCoordinator] spaceMemberListCoordinatorShowInvite: failed to find space", context: [ + "space_id": parameters.spaceId + ]) return } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift index f32d194ae..bf4e03aa6 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift @@ -148,7 +148,9 @@ extension SpaceMenuPresenter: SpaceMenuModelViewModelCoordinatorDelegate { case .leaveSpaceAndChooseRooms: self.showLeaveSpace() default: - MXLog.error("[SpaceMenuPresenter] spaceListViewModel didSelectItem: invalid action \(action)") + MXLog.error("[SpaceMenuPresenter] spaceListViewModel didSelectItem: invalid action", context: [ + "action": action + ]) } } } diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index c6b732f2e..e948bb301 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -22,21 +22,18 @@ #import "FavouritesViewController.h" #import "PeopleViewController.h" #import "RoomsViewController.h" -#import "GroupsViewController.h" #define TABBAR_HOME_INDEX 0 #define TABBAR_FAVOURITES_INDEX 1 #define TABBAR_PEOPLE_INDEX 2 #define TABBAR_ROOMS_INDEX 3 -#define TABBAR_GROUPS_INDEX 4 -#define TABBAR_COUNT 5 +#define TABBAR_COUNT 4 typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { MasterTabBarIndexHome = TABBAR_HOME_INDEX, MasterTabBarIndexFavourites = TABBAR_FAVOURITES_INDEX, MasterTabBarIndexPeople = TABBAR_PEOPLE_INDEX, - MasterTabBarIndexRooms = TABBAR_ROOMS_INDEX, - MasterTabBarIndexGroups = TABBAR_GROUPS_INDEX + MasterTabBarIndexRooms = TABBAR_ROOMS_INDEX }; @protocol MasterTabBarControllerDelegate; @@ -88,16 +85,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { - (void)selectContact:(MXKContact*)contact withPresentationParameters:(ScreenPresentationParameters*)presentationParameters; -/** - Open a GroupDetailsViewController to display the information of the provided group. - - @param group Selected community. - @param matrixSession the matrix session in which the group should be available. - */ -- (void)selectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession; - -- (void)selectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession presentationParameters:(ScreenPresentationParameters*)presentationParameters; - /** Release the current selected item (if any). */ @@ -147,7 +134,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { @property (nonatomic, readonly) FavouritesViewController *favouritesViewController; @property (nonatomic, readonly) PeopleViewController *peopleViewController; @property (nonatomic, readonly) RoomsViewController *roomsViewController; -@property (nonatomic, readonly) GroupsViewController *groupsViewController; // References on the currently selected room @@ -159,10 +145,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { // References on the currently selected contact @property (nonatomic, readonly) MXKContact *selectedContact; -// References on the currently selected group -@property (nonatomic, readonly) MXGroup *selectedGroup; -@property (nonatomic, readonly) MXSession *selectedGroupSession; - // YES while the onboarding flow is displayed @property (nonatomic, readonly) BOOL isOnboardingInProgress; @@ -183,6 +165,5 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomWithParameters:(RoomNavigationParameters*)roomNavigationParameters completion:(void (^)(void))completion; - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)roomPreviewNavigationParameters completion:(void (^)(void))completion; - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectContact:(MXKContact*)contact withPresentationParameters:(ScreenPresentationParameters*)presentationParameters; -- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession presentationParameters:(ScreenPresentationParameters*)presentationParameters; @end diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index cb1546a02..b9f926407 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -18,7 +18,6 @@ #import "MasterTabBarController.h" #import "RecentsDataSource.h" -#import "GroupsDataSource.h" #import "MXRoom+Riot.h" @@ -46,9 +45,6 @@ // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; - // The groups data source - GroupsDataSource *groupsDataSource; - // Custom title view of the navigation bar MainTitleView *titleView; @@ -58,6 +54,7 @@ @property(nonatomic,getter=isHidden) BOOL hidden; @property (nonatomic, readwrite) OnboardingCoordinatorBridgePresenter *onboardingCoordinatorBridgePresenter; +@property (nonatomic) AllChatsOnboardingCoordinatorBridgePresenter *allChatsOnboardingCoordinatorBridgePresenter; // Tell whether the onboarding screen is preparing. @property (nonatomic, readwrite) BOOL isOnboardingCoordinatorPreparing; @@ -92,11 +89,6 @@ return (RoomsViewController*)[self viewControllerForClass:RoomsViewController.class]; } -- (GroupsViewController *)groupsViewController -{ - return (GroupsViewController*)[self viewControllerForClass:GroupsViewController.class]; -} - #pragma mark - Life cycle - (void)viewDidLoad @@ -221,6 +213,11 @@ } [[AppDelegate theDelegate] checkAppVersion]; + + if (BuildSettings.newAppLayoutEnabled && !RiotSettings.shared.allChatsOnboardingHasBeenDisplayed) + { + [self showAllChatsOnboardingScreen]; + } } } @@ -357,11 +354,6 @@ } [recentsDataSource setDelegate:recentsDataSourceDelegate andRecentsDataSourceMode:recentsDataSourceMode]; - // Init the recents data source - groupsDataSource = [[GroupsDataSource alloc] initWithMatrixSession:mainSession]; - [groupsDataSource finalizeInitialization]; - [self.groupsViewController displayList:groupsDataSource]; - // Check whether there are others sessions NSArray* mxSessions = self.mxSessions; if (mxSessions.count > 1) @@ -418,8 +410,6 @@ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil]; } [mxSessionArray addObject:mxSession]; - - // @TODO: handle multi sessions for groups } - (void)removeMatrixSession:(MXSession *)mxSession @@ -448,8 +438,6 @@ } [mxSessionArray removeObject:mxSession]; - - // @TODO: handle multi sessions for groups } - (void)onMatrixSessionStateDidChange:(NSNotification *)notif @@ -457,6 +445,24 @@ [self refreshTabBarBadges]; } +- (void)showAllChatsOnboardingScreen +{ + self.allChatsOnboardingCoordinatorBridgePresenter = [AllChatsOnboardingCoordinatorBridgePresenter new]; + MXWeakify(self); + self.allChatsOnboardingCoordinatorBridgePresenter.completion = ^{ + RiotSettings.shared.allChatsOnboardingHasBeenDisplayed = YES; + + MXStrongifyAndReturnIfNil(self); + + MXWeakify(self); + [self.allChatsOnboardingCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + MXStrongifyAndReturnIfNil(self); + self.allChatsOnboardingCoordinatorBridgePresenter = nil; + }]; + }; + [self.allChatsOnboardingCoordinatorBridgePresenter presentFrom:self animated:YES]; +} + // TODO: Manage the onboarding coordinator at the AppCoordinator level - (void)presentOnboardingFlow { @@ -566,25 +572,6 @@ [self refreshSelectedControllerSelectedCellIfNeeded]; } -- (void)selectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession -{ - ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES stackAboveVisibleViews:NO]; - - [self selectGroup:group inMatrixSession:matrixSession presentationParameters:presentationParameters]; -} - -- (void)selectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession presentationParameters:(ScreenPresentationParameters*)presentationParameters -{ - [self releaseSelectedItem]; - - _selectedGroup = group; - _selectedGroupSession = matrixSession; - - [self.masterTabBarDelegate masterTabBarController:self didSelectGroup:group inMatrixSession:matrixSession presentationParameters:presentationParameters]; - - [self refreshSelectedControllerSelectedCellIfNeeded]; -} - - (void)releaseSelectedItem { _selectedRoomId = nil; @@ -593,9 +580,6 @@ _selectedRoomPreviewData = nil; _selectedContact = nil; - - _selectedGroup = nil; - _selectedGroupSession = nil; } - (NSUInteger)missedDiscussionsCount diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 615540040..d059aa5d3 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -332,13 +332,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { return roomsViewController } - private func createGroupsViewController() -> GroupsViewController { - let groupsViewController: GroupsViewController = GroupsViewController.instantiate() - groupsViewController.tabBarItem.tag = Int(TABBAR_GROUPS_INDEX) - groupsViewController.accessibilityLabel = VectorL10n.titleGroups - return groupsViewController - } - private func createUnifiedSearchController() -> UnifiedSearchViewController { let viewController: UnifiedSearchViewController = UnifiedSearchViewController.instantiate() @@ -392,11 +385,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { let roomsViewController = self.createRoomsViewController() viewControllers.append(roomsViewController) } - - if RiotSettings.shared.homeScreenShowCommunitiesTab && !(self.currentMatrixSession?.groups().isEmpty ?? false) && showCommunities { - let groupsViewController = self.createGroupsViewController() - viewControllers.append(groupsViewController) - } } tabBarController.updateViewControllers(viewControllers) @@ -449,18 +437,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } } - // FIXME: Should be displayed from a tab. - private func showGroupDetails(with group: MXGroup, for matrixSession: MXSession, presentationParameters: ScreenPresentationParameters) { - let coordinatorParameters = GroupDetailsCoordinatorParameters(session: matrixSession, group: group) - let coordinator = GroupDetailsCoordinator(parameters: coordinatorParameters) - coordinator.start() - self.add(childCoordinator: coordinator) - - self.showSplitViewDetails(with: coordinator, stackedOnSplitViewDetail: presentationParameters.stackAboveVisibleViews) { [weak self] in - self?.remove(childCoordinator: coordinator) - } - } - private func showRoom(withId roomId: String, eventId: String? = nil) { guard let matrixSession = self.parameters.userSessionsService.mainUserSession?.matrixSession else { @@ -690,10 +666,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } self.addMatrixSessionToMasterTabBarController(userSession.matrixSession) - - if let matrixSession = self.currentMatrixSession, matrixSession.groups().isEmpty { - self.masterTabBarController.removeTab(at: .groups) - } } @objc private func userSessionsServiceWillRemoveUserSession(_ notification: Notification) { @@ -721,10 +693,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } @objc private func sessionDidSync(_ notification: Notification) { - if self.currentMatrixSession?.groups().isEmpty ?? true { - self.masterTabBarController.removeTab(at: .groups) - } - if let session = notification.object as? MXSession { showCoachMessageIfNeeded(with: session) } @@ -775,13 +743,13 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func createAvatarButtonItem(for viewController: UIViewController) { var actions: [UIMenuElement] = [] - actions.append(UIAction(title: VectorL10n.allChatsUserMenuSettings, image: UIImage(systemName: "gearshape")) { [weak self] action in + actions.append(UIAction(title: VectorL10n.settings, image: UIImage(systemName: "gearshape")) { [weak self] action in self?.showSettings() }) var subMenuActions: [UIAction] = [] if BuildSettings.sideMenuShowInviteFriends { - subMenuActions.append(UIAction(title: VectorL10n.sideMenuActionInviteFriends, image: UIImage(systemName: "square.and.arrow.up.fill")) { [weak self] action in + subMenuActions.append(UIAction(title: VectorL10n.inviteTo(AppInfo.current.displayName), image: UIImage(systemName: "envelope")) { [weak self] action in self?.showInviteFriends(from: nil) }) } @@ -968,10 +936,6 @@ extension TabBarCoordinator: MasterTabBarControllerDelegate { self.showRoom(with: roomId, eventId: eventId, matrixSession: matrixSession, completion: completion) } - func masterTabBarController(_ masterTabBarController: MasterTabBarController!, didSelect group: MXGroup!, inMatrixSession matrixSession: MXSession!, presentationParameters: ScreenPresentationParameters!) { - self.showGroupDetails(with: group, for: matrixSession, presentationParameters: presentationParameters) - } - func masterTabBarController(_ masterTabBarController: MasterTabBarController!, needsSideMenuIconWithNotification displayNotification: Bool) { guard BuildSettings.enableSideMenu else { return diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 6b7bac20f..de9cb89de 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -253,7 +253,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { self.threads = threads self.threadsLoaded() case .failure(let error): - MXLog.error("[ThreadListViewModel] loadData: error: \(error)") + MXLog.error("[ThreadListViewModel] loadData", context: error) self.viewState = .error(error) } } diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index f3c41594e..7eacc6f91 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -41,7 +41,6 @@ #import "Tools.h" #import "RoomViewController.h" #import "ContactDetailsViewController.h" -#import "GroupDetailsViewController.h" #import "RoomInputToolbarView.h" #import "NSArray+Element.h" #import "ShareItemSender.h" diff --git a/Riot/Utils/DictionaryEncodable.swift b/Riot/Utils/DictionaryEncodable.swift index b6a920f94..5c4c2f224 100644 --- a/Riot/Utils/DictionaryEncodable.swift +++ b/Riot/Utils/DictionaryEncodable.swift @@ -31,7 +31,9 @@ extension DictionaryEncodable { let object = try JSONSerialization.jsonObject(with: jsonData) guard let dictionary = object as? [String: Any] else { - MXLog.error("[DictionaryEncodable] Unexpected type decoded \(type(of: object)). Expected a Dictionary.") + MXLog.error("[DictionaryEncodable] Unexpected type decoded, expected a Dictionary.", context: [ + "type": type(of: object) + ]) throw DictionaryEncodableError.typeError } diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 14f7256ac..c56381d51 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -105,7 +105,9 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; // If we cannot create attributed string, but the message is nevertheless meant for display, show generic error // instead of a missing message on a timeline. if ([self shouldDisplayEvent:event]) { - MXLogError(@"[EventFormatter]: Missing attributed string for message event: %@", event.eventId); + MXLogErrorDetails(@"[EventFormatter]: Missing attributed string for message event", @{ + @"event_id": event.eventId ?: @"unknown" + }); string = [[NSAttributedString alloc] initWithString:[VectorL10n noticeErrorUnformattableEvent] attributes:@{ NSFontAttributeName: [self encryptedMessagesTextFont] }]; diff --git a/Riot/target.yml b/Riot/target.yml index d8ee465a3..f2a1bb16b 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -34,8 +34,8 @@ targets: - target: RiotShareExtension - target: SiriIntents - target: RiotNSE + - target: DesignKit - target: CommonKit - - package: DesignKit - package: Mapbox - package: OrderedCollections - package: SwiftOGG diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index f64d3bd99..3b4890364 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -241,7 +241,7 @@ class NotificationService: UNNotificationServiceExtension { MXLog.debug("[NotificationService] fetchAndProcessEvent: Event fetched successfully") self?.checkPlaybackAndContinueProcessing(event, roomId: roomId) case .failure(let error): - MXLog.error("[NotificationService] fetchAndProcessEvent: Failed fetching notification event with error: \(error)") + MXLog.error("[NotificationService] fetchAndProcessEvent: Failed fetching notification event", context: error) self?.fallbackToBestAttemptContent(forEventId: eventId) } } @@ -265,7 +265,7 @@ class NotificationService: UNNotificationServiceExtension { } case .failure(let error): - MXLog.error("[NotificationService] checkPlaybackAndContinueProcessing: Failed fetching read marker event with error: \(error)") + MXLog.error("[NotificationService] checkPlaybackAndContinueProcessing: Failed fetching read marker event", context: error) self?.processEvent(notificationEvent) } } @@ -849,7 +849,7 @@ class NotificationService: UNNotificationServiceExtension { if response.isSuccess { MXLog.debug("[NotificationService] sendReadReceipt: Read receipt send successfully.") } else if let error = response.error { - MXLog.error("[NotificationService] sendReadReceipt: Read receipt send failed with error \(error).") + MXLog.error("[NotificationService] sendReadReceipt: Read receipt send failed", context: error) } } } diff --git a/RiotShareExtension/Sources/ShareItemSender.m b/RiotShareExtension/Sources/ShareItemSender.m index 2c11e2a46..fd050d640 100644 --- a/RiotShareExtension/Sources/ShareItemSender.m +++ b/RiotShareExtension/Sources/ShareItemSender.m @@ -528,7 +528,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [room sendTextMessage:text threadId:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { - MXLogError(@"[ShareItemSender] sendTextMessage failed with error %@", error); + MXLogErrorDetails(@"[ShareItemSender] sendTextMessage failed with error", @{ + @"error": error ?: @"unknown" + }); error = innerError; dispatch_group_leave(dispatchGroup); }]; @@ -568,7 +570,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [room sendFile:fileUrl mimeType:mimeType threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { - MXLogError(@"[ShareItemSender] sendFile failed with error %@", innerError); + MXLogErrorDetails(@"[ShareItemSender] sendFile failed with error", @{ + @"error": innerError ?: @"unknown" + }); error = innerError; dispatch_group_leave(dispatchGroup); } keepActualFilename:YES]; @@ -619,7 +623,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { - MXLogError(@"[ShareManager] Failed sending video with error %@", innerError); + MXLogErrorDetails(@"[ShareManager] Failed sending video with error", @{ + @"error": innerError ?: @"unknown" + }); error = innerError; dispatch_group_leave(dispatchGroup); }]; @@ -705,7 +711,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { - MXLogError(@"[ShareItemSender] sendVoiceMessage failed with error %@", error); + MXLogErrorDetails(@"[ShareItemSender] sendVoiceMessage failed with error", @{ + @"error": error ?: @"unknown" + }); error = innerError; dispatch_group_leave(dispatchGroup); } keepActualFilename:YES]; @@ -870,7 +878,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { - MXLogError(@"[ShareManager] sendImage failed with error %@", error); + MXLogErrorDetails(@"[ShareManager] sendImage failed with error", @{ + @"error": error ?: @"unknown" + }); error = innerError; dispatch_group_leave(dispatchGroup); }]; diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index c50705aa0..bf2a032f5 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -30,8 +30,6 @@ targets: RiotShareExtension: platform: iOS type: app-extension - dependencies: - - package: DesignKit configFiles: Debug: Debug.xcconfig diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift index 6af694b5e..c3332146c 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift @@ -52,7 +52,9 @@ enum LoginMode { var ssoIdentityProviders: [SSOIdentityProvider]? { switch self { case .sso(let ssoIdentityProviders), .ssoAndPassword(let ssoIdentityProviders): - return ssoIdentityProviders + // Provide a backup for homeservers that support SSO but don't offer any identity providers + // https://spec.matrix.org/latest/client-server-api/#client-login-via-sso + return ssoIdentityProviders.count > 0 ? ssoIdentityProviders : [SSOIdentityProvider(id: "", name: "SSO", brand: nil, iconURL: nil)] default: return nil } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift index 55904b5d1..2b7fa9e60 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift @@ -49,7 +49,7 @@ struct AvatarImage: View { mxContentUri: mxContentUri, matrixItemId: matrixItemId, displayName: displayName, - colorCount: theme.colors.contentAndAvatars.count, + colorCount: theme.colors.namesAndAvatars.count, avatarSize: size ) } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift index 7dbc2ba4f..f119a7e14 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift @@ -36,7 +36,7 @@ struct PlaceholderAvatarImage: View { var body: some View { ZStack { - theme.colors.contentAndAvatars[colorIndex] + theme.colors.namesAndAvatars[colorIndex] Text(String(firstCharacter)) .padding(4) diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift index 31e734c58..d82e2107f 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift @@ -38,7 +38,7 @@ struct SpaceAvatarImage: View { .padding(10) .frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue)) .foregroundColor(.white) - .background(theme.colors.contentAndAvatars[colorIndex]) + .background(theme.colors.namesAndAvatars[colorIndex]) .clipShape(RoundedRectangle(cornerRadius: 8)) // Make the text resizable (i.e. Make it large and then allow it to scale down) .font(.system(size: 200)) @@ -55,7 +55,7 @@ struct SpaceAvatarImage: View { mxContentUri: mxContentUri, matrixItemId: matrixItemId, displayName: value, - colorCount: theme.colors.contentAndAvatars.count, + colorCount: theme.colors.namesAndAvatars.count, avatarSize: size ) }) @@ -65,7 +65,7 @@ struct SpaceAvatarImage: View { mxContentUri: mxContentUri, matrixItemId: matrixItemId, displayName: displayName, - colorCount: theme.colors.contentAndAvatars.count, + colorCount: theme.colors.namesAndAvatars.count, avatarSize: size ) } diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index 6a1ee265a..90f736da6 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -54,7 +54,7 @@ class AvatarViewModel: InjectableObject, ObservableObject { avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize) .sink { completion in guard case let .failure(error) = completion else { return } - UILog.error("[AvatarService] Failed to retrieve avatar: \(error)") + UILog.error("[AvatarService] Failed to retrieve avatar", context: error) } receiveValue: { image in self.viewState = .avatar(image) } diff --git a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift index daf298461..38eb5db11 100644 --- a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift +++ b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift @@ -31,7 +31,7 @@ class EffectsScene: SCNScene { static func confetti(with theme: ThemeSwiftUI) -> EffectsScene? { guard let scene = EffectsScene(named: Constants.confettiSceneName) else { return nil } - let colors: [[Float]] = theme.colors.contentAndAvatars.compactMap { $0.floatComponents } + let colors: [[Float]] = theme.colors.namesAndAvatars.compactMap { $0.floatComponents } if let particles = scene.rootNode.childNode(withName: Constants.particlesNodeName, recursively: false)?.particleSystems?.first { // The particles need a non-zero color variation for the handler to affect the color diff --git a/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift index 806f4b1c7..bedcb6a67 100644 --- a/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift +++ b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift @@ -22,5 +22,5 @@ protocol LoggerProtocol { static func debug(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) static func info(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) static func warning(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) - static func error(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) + static func error(_ message: @autoclosure () -> StaticString, _ file: String, _ function: String, line: Int, context: Any?) } diff --git a/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift index 29bfc8421..85587467f 100644 --- a/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift +++ b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift @@ -32,7 +32,7 @@ class PrintLogger: LoggerProtocol { static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { print(message()) } - static func error(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + static func error(_ message: @autoclosure () -> StaticString, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { print(message()) } } diff --git a/RiotSwiftUI/Modules/Common/Logging/UILog.swift b/RiotSwiftUI/Modules/Common/Logging/UILog.swift index 75c3325af..ee0c4f22b 100644 --- a/RiotSwiftUI/Modules/Common/Logging/UILog.swift +++ b/RiotSwiftUI/Modules/Common/Logging/UILog.swift @@ -63,7 +63,7 @@ class UILog: LoggerProtocol { } static func error( - _ message: @autoclosure () -> Any, + _ message: @autoclosure () -> StaticString, _ file: String = #file, _ function: String = #function, line: Int = #line, diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift index 7e8fe5308..f5a15424f 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift @@ -14,33 +14,10 @@ // limitations under the License. // -import SwiftUI +import Foundation import DesignKit -import DesignTokens protocol ThemeSwiftUI: ThemeSwiftUIType { var identifier: ThemeIdentifier { get } var isDark: Bool { get } } - -/// Theme v2 for SwiftUI. -@available(iOS 14.0, *) -public protocol ThemeSwiftUIType { - - /// Colors object - var colors: ElementColors { get } - - /// Fonts object - var fonts: ElementFonts { get } - - /// may contain more design components in future, like icons, audio files etc. -} - -// MARK: - Legacy Colors - -public extension ElementColors { - var legacyTile: Color { - let dynamicColor = UIColor { $0.userInterfaceStyle == .light ? .elementLight.tile : .elementDark.tile } - return Color(dynamicColor) - } -} diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift index 6c3e3c2e2..ad1eeb222 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift @@ -23,7 +23,7 @@ extension ThemeSwiftUI { /// - Parameter userId: The user id used to hash. /// - Returns: The SwiftUI color for the associated userId. func userColor(for userId: String) -> Color { - let senderNameColorIndex = Int(userId.vc_hashCode % Int32(colors.contentAndAvatars.count)) - return colors.contentAndAvatars[senderNameColorIndex] + let senderNameColorIndex = Int(userId.vc_hashCode % Int32(colors.namesAndAvatars.count)) + return colors.namesAndAvatars[senderNameColorIndex] } } diff --git a/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift index a572a4694..0e9250070 100644 --- a/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift +++ b/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift @@ -14,12 +14,12 @@ // limitations under the License. // -import SwiftUI +import Foundation import DesignKit struct DarkThemeSwiftUI: ThemeSwiftUI { var identifier: ThemeIdentifier = .dark let isDark: Bool = true - var colors = Color.element - var fonts = Font.element + var colors: ColorSwiftUI = DarkColors.swiftUI + var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts()) } diff --git a/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift index bfc2e87c0..85ba4d810 100644 --- a/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift +++ b/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift @@ -14,12 +14,12 @@ // limitations under the License. // -import SwiftUI +import Foundation import DesignKit struct DefaultThemeSwiftUI: ThemeSwiftUI { var identifier: ThemeIdentifier = .light let isDark: Bool = false - var colors = Color.element - var fonts = Font.element + var colors: ColorSwiftUI = LightColors.swiftUI + var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts()) } diff --git a/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift index 2615efa47..fe75aa300 100644 --- a/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift @@ -50,7 +50,7 @@ struct BorderedInputFieldStyle: TextFieldStyle { if (theme.identifier == ThemeIdentifier.dark) { return (isEnabled ? theme.colors.primaryContent : theme.colors.tertiaryContent) } else { - return (isEnabled ? theme.colors.primaryContent : theme.colors.quaternaryContent) + return (isEnabled ? theme.colors.primaryContent : theme.colors.quarterlyContent) } } diff --git a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift index 6ab0832ac..7eb67d39c 100644 --- a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift @@ -47,7 +47,7 @@ struct ClearViewModifier: ViewModifier { }) { Image(systemName: "xmark.circle.fill") .renderingMode(.template) - .foregroundColor(theme.colors.quaternaryContent) + .foregroundColor(theme.colors.quarterlyContent) } .padding(.top, alignment == .top ? 8 : 0) .padding(.bottom, alignment == .bottom ? 8 : 0) diff --git a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift index a03c4b21b..5e20f11b0 100644 --- a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift @@ -56,7 +56,7 @@ struct MultilineTextField: View { return theme.colors.accent } - return theme.colors.quaternaryContent + return theme.colors.quarterlyContent } private var borderWidth: CGFloat { @@ -75,7 +75,7 @@ struct MultilineTextField: View { .overlay(rect.stroke(borderColor, lineWidth: borderWidth)) .introspectTextView { textView in textView.textColor = UIColor(textColor) - textView.font = .element.callout + textView.font = theme.fonts.uiFonts.callout } } diff --git a/RiotSwiftUI/Modules/Common/Util/OptionButton.swift b/RiotSwiftUI/Modules/Common/Util/OptionButton.swift index 428ecf09c..17e54bbda 100644 --- a/RiotSwiftUI/Modules/Common/Util/OptionButton.swift +++ b/RiotSwiftUI/Modules/Common/Util/OptionButton.swift @@ -55,7 +55,7 @@ struct OptionButton: View { } } Spacer() - Image(systemName: "chevron.right").font(.system(size: 16, weight: .regular)).foregroundColor(theme.colors.quaternaryContent) + Image(systemName: "chevron.right").font(.system(size: 16, weight: .regular)).foregroundColor(theme.colors.quarterlyContent) } .padding(EdgeInsets(top: 15, leading: 16, bottom: 15, trailing: 16)) .background(theme.colors.quinaryContent) diff --git a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift index 3901ea65a..4edaa2e5c 100644 --- a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift +++ b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift @@ -38,7 +38,7 @@ struct SearchBar: View { } .padding(8) .padding(.horizontal, 25) - .background(theme.colors.system) + .background(theme.colors.navigation) .cornerRadius(8) .padding(.leading) .padding(.trailing, isEditing ? 8 : 16) @@ -46,7 +46,7 @@ struct SearchBar: View { HStack { Image(systemName: "magnifyingglass") .renderingMode(.template) - .foregroundColor(theme.colors.quaternaryContent) + .foregroundColor(theme.colors.quarterlyContent) .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) if isEditing && !text.isEmpty { @@ -55,7 +55,7 @@ struct SearchBar: View { }) { Image(systemName: "multiply.circle.fill") .renderingMode(.template) - .foregroundColor(theme.colors.quaternaryContent) + .foregroundColor(theme.colors.quarterlyContent) } } } diff --git a/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift index a20eba58e..8f0eb6aac 100644 --- a/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift @@ -68,7 +68,7 @@ struct SecondaryActionButtonStyle_Previews: PreviewProvider { Text("Custom") .foregroundColor(theme.colors.secondaryContent) } - .buttonStyle(SecondaryActionButtonStyle(customColor: theme.colors.quaternaryContent)) + .buttonStyle(SecondaryActionButtonStyle(customColor: theme.colors.quarterlyContent)) } .padding() } diff --git a/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift b/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift index 4abada5e5..60f7315bb 100644 --- a/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift +++ b/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift @@ -89,7 +89,7 @@ struct WaitOverlay: ViewModifier { } .padding(12) .background(RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(theme.colors.system.opacity(0.9))) + .fill(theme.colors.navigation.opacity(0.9))) } .edgesIgnoringSafeArea(.all) .transition(.opacity) diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index afd2e47a5..1fd256b5a 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -87,7 +87,7 @@ struct LiveLocationSharingViewer: View { HStack(spacing: 10) { Image(uiImage: Asset.Images.locationLiveCellIcon.image) .renderingMode(.template) - .foregroundColor(theme.colors.quaternaryContent) + .foregroundColor(theme.colors.quarterlyContent) .frame(width: 40, height: 40) Text(VectorL10n.liveLocationSharingEnded) .font(theme.fonts.body) diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift index e1946a180..b6d0669cd 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -134,7 +134,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable { } failure: { [weak self] error in guard let self = self else { return } - MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))") + MXLog.error("[LocationSharingCoordinator] Failed sharing location", context: error) self.locationSharingViewModel.stopLoading(error: .locationSharingError) } } @@ -156,7 +156,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable { self.completion?() } case .failure(let error): - MXLog.error("[LocationSharingCoordinator] Failed to start live location sharing with error: \(String(describing: error))") + MXLog.error("[LocationSharingCoordinator] Failed to start live location sharing", context: error) DispatchQueue.main.async { self.locationSharingViewModel.stopLoading(error: .locationSharingError) diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift index 5884e363e..216a65ea4 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift @@ -72,7 +72,7 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = OnboardingAvatarViewModel(userId: parameters.userSession.userId, displayName: parameters.userSession.account.userDisplayName, - avatarColorCount: DefaultThemeSwiftUI().colors.contentAndAvatars.count) + avatarColorCount: DefaultThemeSwiftUI().colors.namesAndAvatars.count) viewModel.updateAvatarImage(with: parameters.avatar) let view = OnboardingAvatarScreen(viewModel: viewModel.context) diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift index 8982961f6..7cf96984a 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift @@ -44,7 +44,7 @@ enum MockOnboardingAvatarScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let avatarColorCount = DefaultThemeSwiftUI().colors.contentAndAvatars.count + let avatarColorCount = DefaultThemeSwiftUI().colors.namesAndAvatars.count let viewModel: OnboardingAvatarViewModel switch self { case .placeholderAvatar(let userId, let displayName): diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift index 0fe73b772..fd0f284d3 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift @@ -23,7 +23,7 @@ class OnboardingAvatarViewModelTests: XCTestCase { private enum Constants { static let userId = "@user:matrix.org" static let displayName = "Alice" - static let avatarColorCount = DefaultThemeSwiftUI().colors.contentAndAvatars.count + static let avatarColorCount = DefaultThemeSwiftUI().colors.namesAndAvatars.count static let avatarImage = Asset.Images.appSymbol.image } diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift index a94adf6f1..bfc53c682 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift @@ -37,7 +37,7 @@ struct OnboardingCongratulationsViewState: BindableState { let attributedMessage = NSMutableAttributedString(string: message) let range = (message as NSString).range(of: userId) if range.location != NSNotFound { - attributedMessage.addAttributes([.font: UIFont.element.body.bold], range: range) + attributedMessage.addAttributes([.font: UIFont.preferredFont(forTextStyle: .body).vc_bold], range: range) } return attributedMessage diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift index f2306eaf4..63e8507a0 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift @@ -47,7 +47,7 @@ struct OnboardingSplashScreenPageIndicator: View { ForEach(0.. + +class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnboardingViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((AllChatsOnboardingViewModelResult) -> Void)? + + // MARK: - Setup + + static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol { + return AllChatsOnboardingViewModel() + } + + private init() { + super.init(initialViewState: Self.defaultState()) + } + + private static func defaultState() -> AllChatsOnboardingViewState { + return AllChatsOnboardingViewState(pages: [ + AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding1.image, + title: VectorL10n.allChatsOnboardingPageTitle1, + message: VectorL10n.allChatsOnboardingPageMessage1), + AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding2.image, + title: VectorL10n.allChatsOnboardingPageTitle2, + message: VectorL10n.allChatsOnboardingPageMessage2), + AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding3.image, + title: VectorL10n.allChatsOnboardingPageTitle3, + message: VectorL10n.allChatsOnboardingPageMessage3) + ]) + } + + // MARK: - Public + + override func process(viewAction: AllChatsOnboardingViewAction) { + switch viewAction { + case .cancel: + completion?(.cancel) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift new file mode 100644 index 000000000..a0cc2137d --- /dev/null +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol AllChatsOnboardingViewModelProtocol { + + var completion: ((AllChatsOnboardingViewModelResult) -> Void)? { get set } + static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol + var context: AllChatsOnboardingViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift new file mode 100644 index 000000000..de793e30e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift @@ -0,0 +1,95 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import CommonKit + +/// All Chats onboarding screen +final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let hostingController: UIViewController + private var viewModel: AllChatsOnboardingViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + override init() { + let viewModel = AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel() + let view = AllChatsOnboarding(viewModel: viewModel.context) + self.viewModel = viewModel + self.hostingController = VectorHostingController(rootView: view) + self.indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) + + super.init() + + hostingController.presentationController?.delegate = self + } + + // MARK: - Public + + func start() { + MXLog.debug("[AllChatsOnboardingCoordinator] did start.") + viewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[AllChatsOnboardingCoordinator] AllChatsOnboardingViewModel did complete with result: \(result).") + switch result { + case .cancel: + self.completion?() + } + } + } + + func toPresentable() -> UIViewController { + return self.hostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate + +extension AllChatsOnboardingCoordinator: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + completion?() + } + +} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..b367175ef --- /dev/null +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift @@ -0,0 +1,65 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc protocol AllChatsOnboardingCoordinatorBridgePresenterDelegate { + func allChatsOnboardingCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter) +} + +/// `AllChatsOnboardingCoordinatorBridgePresenter` enables to start `AllChatsOnboardingCoordinator` from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). +/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class AllChatsOnboardingCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private var coordinator: AllChatsOnboardingCoordinator? + + // MARK: Public + + var completion: (() -> Void)? + + // MARK: - Public + + func present(from viewController: UIViewController, animated: Bool) { + let coordinator = AllChatsOnboardingCoordinator() + coordinator.completion = { [weak self] in + guard let self = self else { return } + self.completion?() + } + let presentable = coordinator.toPresentable() + viewController.present(presentable, animated: animated, completion: nil) + coordinator.start() + + self.coordinator = coordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + completion?() + } + } +} + diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift new file mode 100644 index 000000000..b13f9a287 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift @@ -0,0 +1,81 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AllChatsOnboarding: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + @State private var selectedTab = 0 + + // MARK: Public + + @ObservedObject var viewModel: AllChatsOnboardingViewModel.Context + + var body: some View { + VStack { + Text(VectorL10n.allChatsOnboardingTitle) + .font(theme.fonts.title3SB) + .foregroundColor(theme.colors.primaryContent) + .padding() + TabView(selection: $selectedTab) { + ForEach(viewModel.viewState.pages.indices) { index in + let page = viewModel.viewState.pages[index] + AllChatsOnboardingPage(image: page.image, + title: page.title, + message: page.message) + .tag(index) + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) + .indexViewStyle(.page(backgroundDisplayMode: .always)) + + Button { onCallToAction() } label: { + Text(selectedTab == viewModel.viewState.pages.count - 1 ? VectorL10n.allChatsOnboardingTryIt : VectorL10n.next) + .animation(nil) + } + .buttonStyle(PrimaryActionButtonStyle()) + .padding() + } + .background(theme.colors.background.ignoresSafeArea()) + .frame(maxHeight: .infinity) + } + + // MARK: - Private + + private func onCallToAction() { + if (selectedTab == viewModel.viewState.pages.count - 1) { + viewModel.send(viewAction: .cancel) + } else { + withAnimation { + selectedTab += 1 + } + } + } +} + +// MARK: - Previews + +struct AllChatsOnboarding_Previews: PreviewProvider { + static var previews: some View { + AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.light).preferredColorScheme(.light) + AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.dark).preferredColorScheme(.dark) + } +} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift new file mode 100644 index 000000000..04725ccae --- /dev/null +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift @@ -0,0 +1,63 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AllChatsOnboardingPage: View { + + // MARK: - Properties + + let image: UIImage + let title: String + let message: String + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + var body: some View { + VStack { + Spacer() + Image(uiImage: image) + Spacer() + Text(title) + .font(theme.fonts.title2B) + .foregroundColor(theme.colors.primaryContent) + .padding(.bottom, 16) + Text(message) + .multilineTextAlignment(.center) + .font(theme.fonts.callout) + .foregroundColor(theme.colors.primaryContent) + Spacer() + } + .padding(.horizontal) + } +} + +// MARK: - Previews + +struct AllChatsOnboardingPage_Previews: PreviewProvider { + static var previews: some View { + preview.theme(.light).preferredColorScheme(.light) + preview.theme(.dark).preferredColorScheme(.dark) + } + + static private var preview: some View { + AllChatsOnboardingPage(image: Asset.Images.allChatsOnboarding1.image, + title: VectorL10n.allChatsOnboardingPageTitle1, + message: VectorL10n.allChatsOnboardingPageMessage1) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index 50c951de2..14ed174de 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -83,7 +83,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { } failure: { [weak self] error in guard let self = self else { return } - MXLog.error("Failed creating poll with error: \(String(describing: error))") + MXLog.error("Failed creating poll", context: error) self.pollEditFormViewModel.stopLoading(errorAlertType: .failedCreatingPoll) } @@ -111,7 +111,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { } failure: { [weak self] error in guard let self = self else { return } - MXLog.error("Failed updating poll with error: \(String(describing: error))") + MXLog.error("Failed updating poll", context: error) self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll) } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift index dd16c54e4..1058f6b3b 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift @@ -100,7 +100,9 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { func applySelection(completion: @escaping () -> Void) { guard let room = session.room(withRoomId: currentRoomId) else { - MXLog.error("[RoomAccessTypeChooserService] applySelection: room with ID \(currentRoomId) not found") + MXLog.error("[RoomAccessTypeChooserService] applySelection: room with ID not found", context: [ + "room_id": currentRoomId + ]) return } @@ -164,7 +166,9 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { private func readRoomState() { guard let room = session.room(withRoomId: currentRoomId) else { - MXLog.error("[RoomAccessTypeChooserService] readRoomState: room with ID \(currentRoomId) not found") + MXLog.error("[RoomAccessTypeChooserService] readRoomState: room with ID not found", context: [ + "room_id": currentRoomId + ]) return } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift index 899025c92..a38cd0efe 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift @@ -45,7 +45,7 @@ struct RoomAccessTypeChooserRow: View { Spacer() Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .renderingMode(.template) - .foregroundColor(isSelected ? theme.colors.accent : theme.colors.quaternaryContent) + .foregroundColor(isSelected ? theme.colors.accent : theme.colors.quarterlyContent) } if let badgeText = badgeText { Text(badgeText) diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift index 625500bd4..3b52c9e41 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift @@ -37,7 +37,7 @@ struct RoomRestrictedAccessSpaceChooserSelector: View { Button(VectorL10n.cancel) { viewModel.send(viewAction: .cancel) } - .foregroundColor(viewModel.viewState.loading ? theme.colors.quaternaryContent : theme.colors.accent) + .foregroundColor(viewModel.viewState.loading ? theme.colors.quarterlyContent : theme.colors.accent) .opacity(viewModel.viewState.loading ? 0.7 : 1) .disabled(viewModel.viewState.loading) } @@ -45,7 +45,7 @@ struct RoomRestrictedAccessSpaceChooserSelector: View { Button(VectorL10n.done) { viewModel.send(viewAction: .done) } - .foregroundColor(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? theme.colors.quaternaryContent : theme.colors.accent) + .foregroundColor(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? theme.colors.quarterlyContent : theme.colors.accent) .opacity(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? 0.7 : 1) .disabled(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading) } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 2ac5741c0..7bdbcb77d 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -72,7 +72,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel localEcho: nil, success: nil) { [weak self] error in guard let self = self else { return } - MXLog.error("[TimelinePollCoordinator]] Failed submitting response with error \(String(describing: error))") + MXLog.error("[TimelinePollCoordinator]] Failed submitting response", context: error) self.viewModel.showAnsweringFailure() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index a7ac3d534..5a3498aa9 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -89,10 +89,10 @@ struct TimelinePollAnswerOptionButton: View { var progressViewAccentColor: Color { guard !poll.closed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quaternaryContent) + return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) } - return answerOption.selected ? theme.colors.accent : theme.colors.quaternaryContent + return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index 9b861df59..4d10fce83 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -117,7 +117,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr self.roomMembers = joinedMembers members(self.roomMembersToProviderMembers(joinedMembers)) }, failure: { error in - MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room with error: \(String(describing: error))") + MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error) }) } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift index beca8dcb5..3b35cd396 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift @@ -25,7 +25,7 @@ struct FormInputFieldStyle: TextFieldStyle { private var textColor: Color { if !isEnabled { - return theme.colors.quaternaryContent + return theme.colors.quarterlyContent } return theme.colors.primaryContent } diff --git a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift index 19ad74020..912ad551d 100644 --- a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift @@ -84,7 +84,7 @@ class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { case .success: self.leaveAllRooms(from: roomIds, at: index+1, completion: completion) case .failure(let error): - MXLog.error("[LeaveSpaceItemsProcessor] failed to leave room with error: \(error)") + MXLog.error("[LeaveSpaceItemsProcessor] failed to leave room", context: error) completion(.failure(error)) } } @@ -97,7 +97,7 @@ class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { case .success: completion(.success(())) case .failure(let error): - MXLog.error("[LeaveSpaceItemsProcessor] failed to leave space with error: \(error)") + MXLog.error("[LeaveSpaceItemsProcessor] failed to leave space", context: error) completion(.failure(error)) } }) diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift index 2759038d4..eeb7743d3 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift @@ -144,7 +144,7 @@ struct MatrixItemChooser: View { } .padding(.vertical, 4) .padding(.horizontal) - .background(theme.colors.legacyTile) + .background(theme.colors.tile) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift index a32578c5d..632403587 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift @@ -52,7 +52,7 @@ struct MatrixItemChooserSectionHeader: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 8) .padding(.horizontal) - .background(theme.colors.system) + .background(theme.colors.navigation) .cornerRadius(8) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift index 12f021459..592fe50bd 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift @@ -89,6 +89,8 @@ final class SpaceCreationCoordinator: Coordinator { func start() { MXLog.debug("[SpaceCreationCoordinator] did start.") + Analytics.shared.trackScreen(.createSpace) + let rootCoordinator = self.createMenuCoordinator(with: spaceVisibilityMenuParameters) rootCoordinator.start() diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinator.swift similarity index 63% rename from RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinator.swift rename to RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinator.swift index c649ed558..7237b0aa3 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinator.swift @@ -21,6 +21,7 @@ enum SpaceSelectorBottomSheetCoordinatorResult { case homeSelected case spaceSelected(_ item: SpaceSelectorListItemData) case createSpace(_ parentSpaceId: String?) + case spaceJoined(_ spaceId: String) } struct SpaceSelectorBottomSheetCoordinatorParameters { @@ -37,7 +38,7 @@ struct SpaceSelectorBottomSheetCoordinatorParameters { } } -final class SpaceSelectorBottomSheetCoordinator: Coordinator, Presentable { +final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presentable { // MARK: - Properties @@ -47,7 +48,6 @@ final class SpaceSelectorBottomSheetCoordinator: Coordinator, Presentable { private var spaceIdStack: [String] private weak var roomDetailCoordinator: SpaceChildRoomDetailCoordinator? - private weak var currentSpaceSelectorCoordinator: SpaceSelectorCoordinator? // MARK: - Public @@ -62,13 +62,17 @@ final class SpaceSelectorBottomSheetCoordinator: Coordinator, Presentable { self.parameters = parameters self.navigationRouter = navigationRouter self.spaceIdStack = [] + + super.init() + self.setupNavigationRouter() } // MARK: - Public func start() { - pushSpace(withId: nil) + Analytics.shared.trackScreen(.spaceBottomSheet) + push(createSpaceSelectorCoordinator(parentSpaceId: nil)) } func toPresentable() -> UIViewController { @@ -89,13 +93,31 @@ final class SpaceSelectorBottomSheetCoordinator: Coordinator, Presentable { sheetController.prefersGrabberVisible = true sheetController.selectedDetentIdentifier = .medium sheetController.prefersScrollingExpandsWhenScrolledToEdge = true + + self.navigationRouter.toPresentable().presentationController?.delegate = self } + private func push(_ coordinator: Coordinator & Presentable) { + if self.navigationRouter.modules.isEmpty { + self.navigationRouter.setRootModule(coordinator) + } else { + self.navigationRouter.push(coordinator.toPresentable(), animated: true) { [weak self] in + guard let self = self else { return } + + self.remove(childCoordinator: coordinator) + if coordinator is SpaceSelectorCoordinator { + self.spaceIdStack.removeLast() + } + } + } + } + private func createSpaceSelectorCoordinator(parentSpaceId: String?) -> SpaceSelectorCoordinator { let parameters = SpaceSelectorCoordinatorParameters(session: parameters.session, parentSpaceId: parentSpaceId, selectedSpaceId: parameters.selectedSpaceId, - showHomeSpace: parameters.showHomeSpace) + showHomeSpace: parameters.showHomeSpace, + showCancel: navigationRouter.modules.isEmpty) let coordinator = SpaceSelectorCoordinator(parameters: parameters) coordinator.completion = { [weak self] result in guard let self = self else { return } @@ -107,38 +129,52 @@ final class SpaceSelectorBottomSheetCoordinator: Coordinator, Presentable { self.trackSpaceSelection(with: nil) self.completion?(.homeSelected) case .spaceSelected(let item): - self.trackSpaceSelection(with: item.id) - self.completion?(.spaceSelected(item)) + if item.isJoined { + self.trackSpaceSelection(with: item.id) + self.completion?(.spaceSelected(item)) + } else { + self.push(self.createSpaceDetailCoordinator(forSpaceWithId: item.id)) + } case .spaceDisclosure(let item): - self.pushSpace(withId: item.id) + Analytics.shared.viewRoomTrigger = .spaceBottomSheet + self.push(self.createSpaceSelectorCoordinator(parentSpaceId: item.id)) case .createSpace(let parentSpaceId): self.completion?(.createSpace(parentSpaceId)) } } + coordinator.start() + + self.add(childCoordinator: coordinator) + + if let spaceId = parentSpaceId { + self.spaceIdStack.append(spaceId) + } + return coordinator } - - private func pushSpace(withId spaceId: String?) { - let coordinator = self.createSpaceSelectorCoordinator(parentSpaceId: spaceId) + + private func createSpaceDetailCoordinator(forSpaceWithId spaceId: String) -> SpaceDetailCoordinator { + let parameters = SpaceDetailCoordinatorParameters(spaceId: spaceId, session: parameters.session, showCancel: false) + let coordinator = SpaceDetailCoordinator(parameters: parameters) + coordinator.completion = { [weak self] result in + guard let self = self else { return } + + self.remove(childCoordinator: coordinator) + switch result { + case .join: + self.completion?(.spaceJoined(spaceId)) + case .open, .cancel, .dismiss: + self.navigationRouter.popModule(animated: true) + break + } + } coordinator.start() self.add(childCoordinator: coordinator) - self.currentSpaceSelectorCoordinator = coordinator - if let spaceId = spaceId { - self.spaceIdStack.append(spaceId) - } - - if self.navigationRouter.modules.isEmpty { - self.navigationRouter.setRootModule(coordinator) - } else { - self.navigationRouter.push(coordinator.toPresentable(), animated: true) { - self.remove(childCoordinator: coordinator) - self.spaceIdStack.removeLast() - } - } + return coordinator } private func trackSpaceSelection(with spaceId: String?) { @@ -147,6 +183,20 @@ final class SpaceSelectorBottomSheetCoordinator: Coordinator, Presentable { return } - Analytics.shared.trackInteraction(.spacePanelSwitchSpace) + if spaceIdStack.isEmpty { + Analytics.shared.trackInteraction(.spacePanelSwitchSpace) + } else { + Analytics.shared.trackInteraction(.spacePanelSwitchSubSpace) + } } } + +// MARK: - UIAdaptivePresentationControllerDelegate + +extension SpaceSelectorBottomSheetCoordinator: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + completion?(.cancel) + } + +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinatorBridgePresenter.swift similarity index 96% rename from RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinatorBridgePresenter.swift rename to RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinatorBridgePresenter.swift index 6226cd7a9..b80be9e30 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinatorBridgePresenter.swift @@ -70,6 +70,8 @@ final class SpaceSelectorBottomSheetCoordinatorBridgePresenter: NSObject { self.delegate?.spaceSelectorBottomSheetCoordinatorBridgePresenter(self, didSelectSpaceWithId: item.id) case .createSpace(let parentSpaceId): self.delegate?.spaceSelectorBottomSheetCoordinatorBridgePresenter(self, didCreateSpaceWithinSpaceWithId: parentSpaceId) + case .spaceJoined(let spaceId): + self.delegate?.spaceSelectorBottomSheetCoordinatorBridgePresenter(self, didSelectSpaceWithId: spaceId) } } let presentable = coordinator.toPresentable() diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift index 51766d9f5..8c13f0216 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift @@ -22,15 +22,18 @@ struct SpaceSelectorCoordinatorParameters { let parentSpaceId: String? let selectedSpaceId: String? let showHomeSpace: Bool + let showCancel: Bool init(session: MXSession, parentSpaceId: String? = nil, selectedSpaceId: String? = nil, - showHomeSpace: Bool = false) { + showHomeSpace: Bool = false, + showCancel: Bool) { self.session = session self.parentSpaceId = parentSpaceId self.selectedSpaceId = selectedSpaceId self.showHomeSpace = showHomeSpace + self.showCancel = showCancel } } @@ -58,7 +61,7 @@ final class SpaceSelectorCoordinator: Coordinator, Presentable { init(parameters: SpaceSelectorCoordinatorParameters) { self.parameters = parameters let service = SpaceSelectorService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, showHomeSpace: parameters.showHomeSpace, selectedSpaceId: parameters.selectedSpaceId) - let viewModel = SpaceSelectorViewModel.makeViewModel(service: service) + let viewModel = SpaceSelectorViewModel.makeViewModel(service: service, showCancel: parameters.showCancel) let view = SpaceSelector(viewModel: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) self.viewModel = viewModel @@ -72,6 +75,10 @@ final class SpaceSelectorCoordinator: Coordinator, Presentable { // MARK: - Public func start() { + if let room = parameters.session.room(withRoomId: parameters.parentSpaceId) { + Analytics.shared.trackViewRoom(room) + } + MXLog.debug("[SpaceSelectorCoordinator] did start.") viewModel.completion = { [weak self] result in guard let self = self else { return } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift index 261c39a90..f267ef6a9 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift @@ -23,8 +23,8 @@ enum MockSpaceSelectorScreenState: MockScreenState, CaseIterable { // A case for each state you want to represent // with specific, minimal associated data that will allow you // mock that screen. - case initialList case emptyList + case initialList case selection /// The associated screen @@ -34,21 +34,21 @@ enum MockSpaceSelectorScreenState: MockScreenState, CaseIterable { /// A list of screen state definitions static var allCases: [MockSpaceSelectorScreenState] { - [.initialList, .emptyList, .selection] + [.emptyList, .initialList, .selection] } /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { let service: MockSpaceSelectorService switch self { + case .emptyList: + service = MockSpaceSelectorService(spaceList: []) case .initialList: service = MockSpaceSelectorService() - case .emptyList: - service = MockSpaceSelectorService(spaceList: [MockSpaceSelectorService.homeItem]) case .selection: - service = MockSpaceSelectorService(selectedSpaceId: MockSpaceSelectorService.defaultSpaceList[2].id) + service = MockSpaceSelectorService(selectedSpaceId: MockSpaceSelectorService.defaultSpaceList[3].id) } - let viewModel = SpaceSelectorViewModel.makeViewModel(service: service) + let viewModel = SpaceSelectorViewModel.makeViewModel(service: service, showCancel: true) // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift index da40b076f..1ba496e9b 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift @@ -29,19 +29,47 @@ class SpaceSelectorService: SpaceSelectorServiceProtocol { private let showHomeSpace: Bool private var spaceList: [SpaceSelectorListItemData] { - var itemList = showHomeSpace && parentSpaceId == nil ? [SpaceSelectorListItemData(id: SpaceSelectorConstants.homeSpaceId, icon: Asset.Images.sideMenuActionIconFeedback.image, displayName: VectorL10n.allChatsTitle)] : [] - let notificationCounter = session.spaceService.notificationCounter + var invitedSpaces: [SpaceSelectorListItemData] = [] + var joinedSpaces: [SpaceSelectorListItemData] = [] if let parentSpaceId = parentSpaceId, let parentSpace = session.spaceService.getSpace(withId: parentSpaceId) { - itemList.append(contentsOf: parentSpace.childSpaces.compactMap { space in - SpaceSelectorListItemData.itemData(with: space, notificationCounter: notificationCounter) - }) + for space in parentSpace.childSpaces { + guard let item = SpaceSelectorListItemData.itemData(with: space, notificationCounter: notificationCounter) else { + continue + } + + if item.isJoined { + joinedSpaces.append(item) + } else { + invitedSpaces.append(item) + } + } } else { - itemList.append(contentsOf: session.spaceService.rootSpaces.compactMap { space in - SpaceSelectorListItemData.itemData(with: space, notificationCounter: notificationCounter) - }) + for space in session.spaceService.rootSpaces { + guard let item = SpaceSelectorListItemData.itemData(with: space, notificationCounter: notificationCounter) else { + continue + } + + if item.isJoined { + joinedSpaces.append(item) + } else { + invitedSpaces.append(item) + } + } } + + guard !invitedSpaces.isEmpty || !joinedSpaces.isEmpty else { + return [] + } + + var itemList: [SpaceSelectorListItemData] = [] + itemList.append(contentsOf: invitedSpaces) + if showHomeSpace && parentSpaceId == nil { + itemList.append(SpaceSelectorListItemData(id: SpaceSelectorConstants.homeSpaceId, icon: Asset.Images.sideMenuActionIconFeedback.image, displayName: VectorL10n.allChatsTitle, isJoined: true)) + } + itemList.append(contentsOf: joinedSpaces) + return itemList } @@ -71,6 +99,12 @@ class SpaceSelectorService: SpaceSelectorServiceProtocol { spaceListSubject.send(spaceList) parentSpaceNameSubject.send(parentSpaceName) + + NotificationCenter.default.addObserver(self, selector: #selector(self.spaceServiceDidUpdate), name: MXSpaceService.didBuildSpaceGraph, object: nil) + } + + @objc private func spaceServiceDidUpdate() { + spaceListSubject.send(spaceList) } } @@ -87,6 +121,7 @@ fileprivate extension SpaceSelectorListItemData { displayName: summary.displayname, notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0, highlightedNotificationCount: notificationState?.groupMissedDiscussionsHighlightedCount ?? 0, - hasSubItems: !space.childSpaces.isEmpty) + hasSubItems: !space.childSpaces.isEmpty, + isJoined: summary.isJoined) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift index d5bf023b3..5a3435252 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift @@ -23,10 +23,11 @@ class MockSpaceSelectorService: SpaceSelectorServiceProtocol { static let homeItem = SpaceSelectorListItemData(id: SpaceSelectorConstants.homeSpaceId, avatar: nil, icon: UIImage(systemName: "house"), displayName: "All Chats", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: false) static let defaultSpaceList = [ homeItem, - SpaceSelectorListItemData(id: "!aaabaa:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Default Space", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: false), - SpaceSelectorListItemData(id: "!zzasds:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with sub items", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: true), - SpaceSelectorListItemData(id: "!scthve:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with notifications", notificationCount: 55, highlightedNotificationCount: 0, hasSubItems: true), - SpaceSelectorListItemData(id: "!ferggs:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with highlight", notificationCount: 99, highlightedNotificationCount: 50, hasSubItems: false) + SpaceSelectorListItemData(id: "!lennfd:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Invited space", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: false, isJoined: false), + SpaceSelectorListItemData(id: "!aaabaa:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Default Space", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: false, isJoined: true), + SpaceSelectorListItemData(id: "!zzasds:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with sub items", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: true, isJoined: true), + SpaceSelectorListItemData(id: "!scthve:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with notifications", notificationCount: 55, highlightedNotificationCount: 0, hasSubItems: true, isJoined: true), + SpaceSelectorListItemData(id: "!ferggs:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with highlight", notificationCount: 99, highlightedNotificationCount: 50, hasSubItems: false, isJoined: true) ] var spaceListSubject: CurrentValueSubject<[SpaceSelectorListItemData], Never> diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift index ade9a78f0..1a9f16bc5 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift @@ -54,6 +54,8 @@ struct SpaceSelectorListItemData { let highlightedNotificationCount: UInt /// Indicates if the space has sub spaces (condition the display of the disclosure button) let hasSubItems: Bool + /// Indicates if the space has has already been joined + let isJoined: Bool init(id: String, avatar: AvatarInput? = nil, @@ -61,7 +63,8 @@ struct SpaceSelectorListItemData { displayName: String?, notificationCount: UInt = 0, highlightedNotificationCount: UInt = 0, - hasSubItems: Bool = false) { + hasSubItems: Bool = false, + isJoined: Bool = false) { self.id = id self.avatar = avatar self.icon = icon @@ -69,6 +72,7 @@ struct SpaceSelectorListItemData { self.notificationCount = notificationCount self.highlightedNotificationCount = highlightedNotificationCount self.hasSubItems = hasSubItems + self.isJoined = isJoined } } @@ -96,6 +100,8 @@ struct SpaceSelectorViewState: BindableState { var selectedSpaceId: String? /// String to be displayed as title for the navigation bar var navigationTitle: String + /// `true` if the view should display the cancel button in the navigation bar + let showCancel: Bool } enum SpaceSelectorViewAction { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift index fe3abd531..37b7bc787 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift @@ -35,22 +35,31 @@ class SpaceSelectorViewModel: SpaceSelectorViewModelType, SpaceSelectorViewModel // MARK: - Setup - static func makeViewModel(service: SpaceSelectorServiceProtocol) -> SpaceSelectorViewModelProtocol { - return SpaceSelectorViewModel(service: service) + static func makeViewModel(service: SpaceSelectorServiceProtocol, showCancel: Bool) -> SpaceSelectorViewModelProtocol { + return SpaceSelectorViewModel(service: service, showCancel: showCancel) } - private init(service: SpaceSelectorServiceProtocol) { + private init(service: SpaceSelectorServiceProtocol, showCancel: Bool) { self.service = service - super.init(initialViewState: Self.defaultState(service: service)) + super.init(initialViewState: Self.defaultState(service: service, showCancel: showCancel)) + setupObservers() } - private static func defaultState(service: SpaceSelectorServiceProtocol) -> SpaceSelectorViewState { + private static func defaultState(service: SpaceSelectorServiceProtocol, showCancel: Bool) -> SpaceSelectorViewState { let parentName = service.parentSpaceNameSubject.value return SpaceSelectorViewState(items: service.spaceListSubject.value, selectedSpaceId: service.selectedSpaceId, - navigationTitle: parentName ?? VectorL10n.spaceSelectorTitle) + navigationTitle: parentName ?? VectorL10n.spaceSelectorTitle, + showCancel: showCancel) } + private func setupObservers() { + service.spaceListSubject.sink { [weak self] spaceList in + self?.state.items = spaceList + } + .store(in: &cancellables) + } + // MARK: - Public override func process(viewAction: SpaceSelectorViewAction) { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift index eb53edc93..79263720d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift @@ -19,6 +19,6 @@ import Foundation protocol SpaceSelectorViewModelProtocol { var completion: ((SpaceSelectorViewModelResult) -> Void)? { get set } - static func makeViewModel(service: SpaceSelectorServiceProtocol) -> SpaceSelectorViewModelProtocol + static func makeViewModel(service: SpaceSelectorServiceProtocol, showCancel: Bool) -> SpaceSelectorViewModelProtocol var context: SpaceSelectorViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift index 4770a7602..020127ea3 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift @@ -26,10 +26,15 @@ class SpaceSelectorUITests: MockScreenTestCase { XCTAssertEqual(disclosureButtons.count, MockSpaceSelectorService.defaultSpaceList.filter { $0.hasSubItems }.count) let notificationBadges = app.staticTexts.matching(identifier: "notificationBadge").allElementsBoundByIndex - let itemsWithNotifications = MockSpaceSelectorService.defaultSpaceList.filter { $0.notificationCount > 0 } + let itemsWithNotifications = MockSpaceSelectorService.defaultSpaceList.filter { $0.notificationCount > 0 || !$0.isJoined } XCTAssertEqual(notificationBadges.count, itemsWithNotifications.count) for (index, notificationBadge) in notificationBadges.enumerated() { - XCTAssertEqual("\(itemsWithNotifications[index].notificationCount)", notificationBadge.label) + let item = itemsWithNotifications[index] + if item.isJoined { + XCTAssertEqual("\(item.notificationCount)", notificationBadge.label) + } else { + XCTAssertEqual("! ", notificationBadge.label) + } } let spaceItemNameList = app.staticTexts.matching(identifier: "itemName").allElementsBoundByIndex @@ -37,6 +42,28 @@ class SpaceSelectorUITests: MockScreenTestCase { for (index, item) in MockSpaceSelectorService.defaultSpaceList.enumerated() { XCTAssertEqual(item.displayName, spaceItemNameList[index].label) } + + checkIfEmptyPlaceholder(exists: false) + } + + func testEmptyList() { + app.goToScreenWithIdentifier(MockSpaceSelectorScreenState.emptyList.title) + + let disclosureButtons = app.buttons.matching(identifier: "disclosureButton").allElementsBoundByIndex + XCTAssertEqual(disclosureButtons.count, 0) + let notificationBadges = app.staticTexts.matching(identifier: "notificationBadge").allElementsBoundByIndex + XCTAssertEqual(notificationBadges.count, 0) + let spaceItemNameList = app.staticTexts.matching(identifier: "itemName").allElementsBoundByIndex + XCTAssertEqual(spaceItemNameList.count, 0) + checkIfEmptyPlaceholder(exists: true) + } + + // MARK: - Private methods + + private func checkIfEmptyPlaceholder(exists: Bool) { + XCTAssertEqual(app.staticTexts["emptyListPlaceholderTitle"].exists, exists) + XCTAssertEqual(app.staticTexts["emptyListPlaceholderMessage"].exists, exists) + XCTAssertEqual(app.buttons["createSpaceButton"].exists, exists) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift index 4d590a8ad..58d5f9304 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift @@ -28,7 +28,7 @@ class SpaceSelectorViewModelTests: XCTestCase { override func setUpWithError() throws { service = MockSpaceSelectorService() - viewModel = SpaceSelectorViewModel.makeViewModel(service: service) + viewModel = SpaceSelectorViewModel.makeViewModel(service: service, showCancel: true) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift index 11ea3aa0e..115a51444 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift @@ -29,6 +29,18 @@ struct SpaceSelector: View { @ObservedObject var viewModel: SpaceSelectorViewModel.Context var body: some View { + VStack { + if !viewModel.viewState.items.isEmpty { + itemListView + } else { + emptyListPlaceholder + } + } + .background(theme.colors.background.edgesIgnoringSafeArea(.all)) + .accentColor(theme.colors.accent) + } + + private var itemListView: some View { ScrollView { LazyVStack { ForEach(viewModel.viewState.items) { item in @@ -36,6 +48,7 @@ struct SpaceSelector: View { icon: item.icon, displayName: item.displayName, hasSubItems: item.hasSubItems, + isJoined: item.isJoined, isSelected: item.id == viewModel.viewState.selectedSpaceId, notificationCount: item.notificationCount, highlightedNotificationCount: item.highlightedNotificationCount, @@ -50,7 +63,6 @@ struct SpaceSelector: View { } } .frame(maxHeight: .infinity) - .background(theme.colors.background.edgesIgnoringSafeArea(.all)) .navigationTitle(viewModel.viewState.navigationTitle) .toolbar { ToolbarItem(placement: .confirmationAction) { @@ -59,12 +71,39 @@ struct SpaceSelector: View { } } ToolbarItem(placement: .cancellationAction) { - Button(VectorL10n.cancel) { - viewModel.send(viewAction: .cancel) + if viewModel.viewState.showCancel { + Button(VectorL10n.cancel) { + viewModel.send(viewAction: .cancel) + } } } } - .accentColor(theme.colors.accent) + } + + private var emptyListPlaceholder: some View { + VStack { + Spacer() + Text(VectorL10n.spaceSelectorEmptyViewTitle) + .foregroundColor(theme.colors.primaryContent) + .font(theme.fonts.title3SB) + .accessibility(identifier: "emptyListPlaceholderTitle") + Spacer() + .frame(height: 24) + Text(VectorL10n.spaceSelectorEmptyViewInformation) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.callout) + .multilineTextAlignment(.center) + .accessibility(identifier: "emptyListPlaceholderMessage") + Spacer() + Button { viewModel.send(viewAction: .createSpace) } label: { + Text(VectorL10n.spaceSelectorCreateSpace) + .font(theme.fonts.bodySB) + } + .buttonStyle(PrimaryActionButtonStyle()) + .accessibility(identifier: "createSpaceButton") + } + .padding(.horizontal, 24) + .padding(.bottom, 24) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift index 611475c52..aa60c5dd4 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift @@ -29,6 +29,7 @@ struct SpaceSelectorListRow: View { let icon: UIImage? let displayName: String? let hasSubItems: Bool + let isJoined: Bool let isSelected: Bool let notificationCount: UInt let highlightedNotificationCount: UInt @@ -61,17 +62,15 @@ struct SpaceSelectorListRow: View { .accessibility(identifier: "itemName") Spacer() if notificationCount > 0 { - Text("\(notificationCount)") - .multilineTextAlignment(.center) - .foregroundColor(theme.colors.background) - .font(theme.fonts.footnote) - .padding(.vertical, 2) - .padding(.horizontal, 6) - .background(highlightedNotificationCount > 0 ? theme.colors.alert : theme.colors.secondaryContent) - .clipShape(Capsule()) - .accessibility(identifier: "notificationBadge") + badge(with: "\(notificationCount)", color: highlightedNotificationCount > 0 ? theme.colors.alert : theme.colors.secondaryContent) } - if hasSubItems { + if !isJoined { + badge(with: "! ", color: theme.colors.alert) + Image(systemName: "chevron.right") + .renderingMode(.template) + .foregroundColor(theme.colors.secondaryContent) + } + if hasSubItems && isJoined { Button { disclosureAction?() } label: { @@ -91,6 +90,17 @@ struct SpaceSelectorListRow: View { .background(theme.colors.background) } + private func badge(with text: String, color: Color) -> some View { + return Text(text) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.background) + .font(theme.fonts.footnote) + .padding(.vertical, 2) + .padding(.horizontal, 6) + .background(color) + .clipShape(Capsule()) + .accessibility(identifier: "notificationBadge") + } } // MARK: - Previews @@ -104,11 +114,11 @@ struct SpaceSelectorListRow_Previews: PreviewProvider { static var sampleView: some View { VStack(spacing: 8) { - SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: false, isSelected: false, notificationCount: 0, highlightedNotificationCount: 0, disclosureAction: nil) - SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isSelected: false, notificationCount: 0, highlightedNotificationCount: 0, disclosureAction: nil) - SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isSelected: false, notificationCount: 99, highlightedNotificationCount: 0, disclosureAction: nil) - SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: false, isSelected: false, notificationCount: 99, highlightedNotificationCount: 1, disclosureAction: nil) - SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isSelected: true, notificationCount: 99, highlightedNotificationCount: 1, disclosureAction: nil) + SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: false, isJoined: true, isSelected: false, notificationCount: 0, highlightedNotificationCount: 0, disclosureAction: nil) + SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isJoined: true, isSelected: false, notificationCount: 0, highlightedNotificationCount: 0, disclosureAction: nil) + SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isJoined: true, isSelected: false, notificationCount: 99, highlightedNotificationCount: 0, disclosureAction: nil) + SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: false, isJoined: true, isSelected: false, notificationCount: 99, highlightedNotificationCount: 1, disclosureAction: nil) + SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isJoined: true, isSelected: true, notificationCount: 99, highlightedNotificationCount: 1, disclosureAction: nil) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift index 23f37bb37..42d8f71b3 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift @@ -39,7 +39,7 @@ struct SpaceSettings: View { .padding(.bottom, 32) } } - .background(theme.colors.system.ignoresSafeArea()) + .background(theme.colors.navigation.ignoresSafeArea()) .waitOverlay(show: viewModel.viewState.isLoading, allowUserInteraction: false) .ignoresSafeArea(.container, edges: .bottom) .frame(maxHeight: .infinity) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift index 18bef0d8f..6287ab24d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift @@ -67,7 +67,7 @@ struct SpaceSettingsOptionListItem: View { Image(systemName: "chevron.right") .renderingMode(.template) .font(.system(size: 16, weight: .regular)) - .foregroundColor(theme.colors.quaternaryContent) + .foregroundColor(theme.colors.quarterlyContent) } .opacity(isEnabled ? 1 : 0.5) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinator.swift new file mode 100644 index 000000000..42151e7cf --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinator.swift @@ -0,0 +1,76 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CommonKit + +struct UserSessionsFlowCoordinatorParameters { + let session: MXSession + let router: NavigationRouterType? +} + +final class UserSessionsFlowCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: UserSessionsFlowCoordinatorParameters + private let navigationRouter: NavigationRouterType + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + init(parameters: UserSessionsFlowCoordinatorParameters) { + self.parameters = parameters + + self.navigationRouter = parameters.router ?? NavigationRouter(navigationController: RiotNavigationController()) + } + + // MARK: - Public + + func start() { + MXLog.debug("[UserSessionsFlowCoordinator] did start.") + + let rootCoordinatorParameters = UserSessionsOverviewCoordinatorParameters(session: self.parameters.session) + + let rootCoordinator = UserSessionsOverviewCoordinator(parameters: rootCoordinatorParameters) + + rootCoordinator.start() + + self.add(childCoordinator: rootCoordinator) + + if self.navigationRouter.modules.isEmpty == false { + self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + self?.completion?() + }) + } else { + self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + self?.completion?() + } + } + } + + func toPresentable() -> UIViewController { + return self.navigationRouter.toPresentable() + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..d2a1a9ff0 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift @@ -0,0 +1,83 @@ +/* + Copyright 2022 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import MatrixSDK +import UIKit + +/// UserSessionsFlowCoordinatorBridgePresenter enables to start UserSessionsFlowCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). +/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class UserSessionsFlowCoordinatorBridgePresenter: NSObject { + + // MARK: - Constants + + // MARK: - Properties + + // MARK: Private + + private let mxSession: MXSession + private var coordinator: UserSessionsFlowCoordinator? + + // MARK: Public + + var completion: (() -> Void)? + + // MARK: - Setup + + init(mxSession: MXSession) { + self.mxSession = mxSession + super.init() + } + + // MARK: - Public + + func push(from navigationController: UINavigationController, animated: Bool) { + + self.startUserSessionsFlow(mxSession: self.mxSession, navigationController: navigationController) + } + + // MARK: - Private + + private func startUserSessionsFlow(mxSession: MXSession, navigationController: UINavigationController?) { + + var navigationRouter: NavigationRouterType? + + if let navigationController = navigationController { + navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) + } + + let coordinatorParameters = UserSessionsFlowCoordinatorParameters(session: mxSession, router: navigationRouter) + + let userSessionsFlowCoordinator = UserSessionsFlowCoordinator(parameters: coordinatorParameters) + + userSessionsFlowCoordinator.completion = { [weak self] in + + guard let self = self else { + return + } + + self.completion?() + self.coordinator = nil + } + + userSessionsFlowCoordinator.start() + + self.coordinator = userSessionsFlowCoordinator + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift new file mode 100644 index 000000000..5abebc3ab --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -0,0 +1,67 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import CommonKit + +struct UserSessionsOverviewCoordinatorParameters { + let session: MXSession +} + +final class UserSessionsOverviewCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: UserSessionsOverviewCoordinatorParameters + private let userSessionsOverviewHostingController: UIViewController + private var userSessionsOverviewViewModel: UserSessionsOverviewViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + init(parameters: UserSessionsOverviewCoordinatorParameters) { + self.parameters = parameters + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: UserSessionsOverviewService()) + let view = UserSessionsOverview(viewModel: viewModel.context) + userSessionsOverviewViewModel = viewModel + userSessionsOverviewHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + + func start() { + MXLog.debug("[UserSessionsOverviewCoordinator] did start.") + userSessionsOverviewViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).") + switch result { + case .done: + self.completion?() + } + } + } + + func toPresentable() -> UIViewController { + return self.userSessionsOverviewHostingController + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift new file mode 100644 index 000000000..a0b67dafc --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift @@ -0,0 +1,57 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case verifiedSession + + /// The associated screen + var screenType: Any.Type { + UserSessionsOverview.self + } + + /// A list of screen state definitions + static var allCases: [MockUserSessionsOverviewScreenState] { + // Each of the presence statuses + return [.verifiedSession] + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let service: MockUserSessionsOverviewService = MockUserSessionsOverviewService() + switch self { + case .verifiedSession: + break + } + + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) + + // can simulate service and viewModel actions here if needs be. + + return ( + [service, viewModel], + AnyView(UserSessionsOverview(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift new file mode 100644 index 000000000..1f280ad1a --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol { + + // MARK: - Setup + + init() { + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift new file mode 100644 index 000000000..61037fec8 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift @@ -0,0 +1,20 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift new file mode 100644 index 000000000..be124d126 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift @@ -0,0 +1,20 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol UserSessionsOverviewServiceProtocol { +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift new file mode 100644 index 000000000..aa0cf5e87 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import RiotSwiftUI + +class UserSessionsOverviewUITests: MockScreenTestCase { +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift new file mode 100644 index 000000000..d45804590 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift @@ -0,0 +1,33 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine + +@testable import RiotSwiftUI + +class UserSessionsOverviewViewModelTests: XCTestCase { + + var service: MockUserSessionsOverviewService! + var viewModel: UserSessionsOverviewViewModelProtocol! + var context: UserSessionsOverviewViewModelType.Context! + + override func setUpWithError() throws { + service = MockUserSessionsOverviewService() + viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) + context = viewModel.context + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift new file mode 100644 index 000000000..949617ca6 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -0,0 +1,38 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - Coordinator + +// MARK: View model + +enum UserSessionsOverviewViewModelResult { + case done +} + +// MARK: View + +struct UserSessionsOverviewViewState: BindableState { +} + +enum UserSessionsOverviewViewAction { + case verifyCurrentSession + case viewCurrentSessionDetails + case viewAllUnverifiedSessions + case viewAllInactiveSessions + case viewAllOtherSessions +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift new file mode 100644 index 000000000..93c28688c --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -0,0 +1,61 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +typealias UserSessionsOverviewViewModelType = StateStoreViewModel + +class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSessionsOverviewViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private let userSessionsOverviewService: UserSessionsOverviewServiceProtocol + + // MARK: Public + + var completion: ((UserSessionsOverviewViewModelResult) -> Void)? + + // MARK: - Setup + + init(userSessionsOverviewService: UserSessionsOverviewServiceProtocol) { + self.userSessionsOverviewService = userSessionsOverviewService + + let viewState = UserSessionsOverviewViewState() + + super.init(initialViewState: viewState) + } + + // MARK: - Public + + override func process(viewAction: UserSessionsOverviewViewAction) { + switch viewAction { + case .verifyCurrentSession: + break + case .viewCurrentSessionDetails: + break + case .viewAllUnverifiedSessions: + break + case .viewAllInactiveSessions: + break + case .viewAllOtherSessions: + break + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModelProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModelProtocol.swift new file mode 100644 index 000000000..f416bb16a --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol UserSessionsOverviewViewModelProtocol { + + var completion: ((UserSessionsOverviewViewModelResult) -> Void)? { get set } + + var context: UserSessionsOverviewViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift new file mode 100644 index 000000000..1dc2983fb --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -0,0 +1,47 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UserSessionsOverview: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: UserSessionsOverviewViewModel.Context + + var body: some View { + VStack { + } + .background(theme.colors.background) + .frame(maxHeight: .infinity) + .navigationTitle(VectorL10n.userSessionsOverviewTitle) + } +} + +// MARK: - Previews + +struct UserSessionsOverview_Previews: PreviewProvider { + static let stateRenderer = MockUserSessionsOverviewScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 461e43d68..428fbbdb2 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -30,7 +30,7 @@ targets: type: application platform: iOS dependencies: - - package: DesignKit + - target: DesignKit - package: Mapbox sources: - path: . diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 29261b72a..62b78e18a 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -32,7 +32,6 @@ targets: dependencies: - target: RiotSwiftUI - - package: DesignKit settings: base: diff --git a/RiotTests/AnalyticsTests.swift b/RiotTests/AnalyticsTests.swift index ac66e3bc4..0fda6af36 100644 --- a/RiotTests/AnalyticsTests.swift +++ b/RiotTests/AnalyticsTests.swift @@ -78,7 +78,7 @@ class AnalyticsTests: XCTestCase { XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.") // When updating the user properties - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5)) + client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5, allChatsActiveFilter: nil)) // Then the properties should be cached XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -90,27 +90,37 @@ class AnalyticsTests: XCTestCase { func testMergingUserProperties() { // Given a client with a cached use case user properties let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") XCTAssertNil(client.pendingUserProperties?.numFavouriteRooms, "The number of favorite rooms should not be set.") XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.") - // When updating the number of spaced - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5)) + // When updating the number of spaces + client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5, allChatsActiveFilter: nil)) // Then the new properties should be updated and the existing properties should remain unchanged XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection shouldn't have changed.") XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should have been updated.") XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.") + + // When updating the number of spaces + client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: .Favourites)) + + // Then the new properties should be updated and the existing properties should remain unchanged + XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") + XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection shouldn't have changed.") + XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should have been updated.") + XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.") + XCTAssertEqual(client.pendingUserProperties?.allChatsActiveFilter, .Favourites, "The All Chats active filter should have been updated.") } func testSendingUserProperties() { // Given a client with user properties set let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) client.start() XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -126,7 +136,7 @@ class AnalyticsTests: XCTestCase { func testSendingUserPropertiesWithIdentify() { // Given a client with user properties set let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) client.start() XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") diff --git a/docs/Customisation.md b/docs/Customisation.md index 7bf816815..b3de09469 100644 --- a/docs/Customisation.md +++ b/docs/Customisation.md @@ -12,6 +12,6 @@ Various features of Element iOS can be enabled/disabled/configured via flags in ## Theme -Element iOS has a [dependency](https://github.com/vector-im/element-ios/blob/92fc7046ede2720d4b46bffd07d97ce59b50d95f/project.yml#L42-L44) on our DesignKit package which supplies the fonts, colours and some common components we use across multiple apps. The fonts are defined [directly](https://github.com/vector-im/element-x-ios/tree/develop/DesignKit/Sources/Fonts) in this package. The colours come from a [dependency](https://github.com/vector-im/element-x-ios/blob/2f69c9978231b6e7cf0b0c3126846f2369e999bb/Package.swift#L13) on our [Design Tokens](https://github.com/vector-im/element-design-tokens) repo which is a style dictionary that allows to us share definitions across multiple platforms. +The themes used in Element iOS can be found in `Riot/Managers/Theme/Themes`. A newer theming system is available as nested `colors` and `fonts` properties on these themes and can be found in `DesignKit/Variants/Colors` and `DesignKit/Variants/Fonts` respectively. The newer system is used for screens built in UIKit with Swift and all of the SwiftUI screens. For logos, they're currently regular assets that can be found either in [Images.xcassets](https://github.com/vector-im/element-ios/tree/develop/Riot/Assets/Images.xcassets) or [SharedImages.xcassets](https://github.com/vector-im/element-ios/tree/develop/Riot/Assets/SharedImages.xcassets). diff --git a/project.yml b/project.yml index 5ff5e65ef..aa33c25d8 100644 --- a/project.yml +++ b/project.yml @@ -32,6 +32,7 @@ include: - path: RiotShareExtension/target.yml - path: SiriIntents/target.yml - path: RiotNSE/target.yml + - path: DesignKit/target.yml - path: RiotSwiftUI/target.yml - path: RiotSwiftUI/targetUnitTests.yml - path: RiotSwiftUI/targetUITests.yml @@ -39,9 +40,6 @@ include: - path: CommonKit/targetUnitTests.yml packages: - DesignKit: - url: https://github.com/vector-im/element-x-ios - exactVersion: 1.0.1-202207011447 Mapbox: url: https://github.com/maplibre/maplibre-gl-native-distribution minVersion: 5.12.2