mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:02:10 +00:00
Implement tab bar badges, visibility and selection. (#4373)
This commit is contained in:
@@ -9188,7 +9188,7 @@
|
|||||||
repositoryURL = "https://github.com/element-hq/compound-ios";
|
repositoryURL = "https://github.com/element-hq/compound-ios";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = revision;
|
kind = revision;
|
||||||
revision = afc59afb1b1e4f4960e2f2a15e52d4e2e33fc889;
|
revision = 8d7ca2e413026735b724044847ff1ae3b40563f0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/element-hq/compound-ios",
|
"location" : "https://github.com/element-hq/compound-ios",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "afc59afb1b1e4f4960e2f2a15e52d4e2e33fc889"
|
"revision" : "8d7ca2e413026735b724044847ff1ae3b40563f0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,22 +9,35 @@ import Compound
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Class responsible for displaying an arbitrary number of coordinators within the tab bar.
|
/// Class responsible for displaying an arbitrary number of coordinators within the tab bar.
|
||||||
@Observable class NavigationTabCoordinator: CoordinatorProtocol, CustomStringConvertible {
|
@Observable class NavigationTabCoordinator<Tag: Hashable>: CoordinatorProtocol, CustomStringConvertible {
|
||||||
struct Tab {
|
struct Tab {
|
||||||
let coordinator: CoordinatorProtocol
|
let coordinator: CoordinatorProtocol
|
||||||
|
let details: TabDetails
|
||||||
|
var dismissalCallback: (() -> Void)?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Observable class TabDetails {
|
||||||
|
/// A unique tab that identifies the tab for selection.
|
||||||
|
let tag: Tag
|
||||||
let title: String
|
let title: String
|
||||||
let icon: KeyPath<CompoundIcons, Image>
|
let icon: KeyPath<CompoundIcons, Image>
|
||||||
let selectedIcon: KeyPath<CompoundIcons, Image>
|
let selectedIcon: KeyPath<CompoundIcons, Image>
|
||||||
var dismissalCallback: (() -> Void)?
|
var badgeCount = 0
|
||||||
|
var barVisibility: Visibility = .automatic
|
||||||
|
|
||||||
|
init(tag: Tag, title: String, icon: KeyPath<CompoundIcons, Image>, selectedIcon: KeyPath<CompoundIcons, Image>) {
|
||||||
|
self.tag = tag
|
||||||
|
self.title = title
|
||||||
|
self.icon = icon
|
||||||
|
self.selectedIcon = selectedIcon
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Tabs
|
// MARK: Tabs
|
||||||
|
|
||||||
fileprivate struct TabModule: Identifiable {
|
fileprivate struct TabModule: Identifiable {
|
||||||
let module: NavigationModule
|
let module: NavigationModule
|
||||||
let title: String
|
let details: TabDetails
|
||||||
let icon: KeyPath<CompoundIcons, Image>
|
|
||||||
let selectedIcon: KeyPath<CompoundIcons, Image>
|
|
||||||
|
|
||||||
var id: ObjectIdentifier { module.id }
|
var id: ObjectIdentifier { module.id }
|
||||||
@MainActor var coordinator: CoordinatorProtocol? { module.coordinator }
|
@MainActor var coordinator: CoordinatorProtocol? { module.coordinator }
|
||||||
@@ -57,13 +70,15 @@ import SwiftUI
|
|||||||
transaction.disablesAnimations = !animated
|
transaction.disablesAnimations = !animated
|
||||||
|
|
||||||
withTransaction(transaction) {
|
withTransaction(transaction) {
|
||||||
tabModules = tabs.map { TabModule(module: .init($0.coordinator, dismissalCallback: $0.dismissalCallback),
|
tabModules = tabs.map { TabModule(module: .init($0.coordinator, dismissalCallback: $0.dismissalCallback), details: $0.details) }
|
||||||
title: $0.title,
|
|
||||||
icon: $0.icon,
|
|
||||||
selectedIcon: $0.selectedIcon) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedTab = tabModules.first?.details.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The currently selected tab's tag.
|
||||||
|
var selectedTab: Tag?
|
||||||
|
|
||||||
// MARK: Sheets
|
// MARK: Sheets
|
||||||
|
|
||||||
fileprivate var sheetModule: NavigationModule? {
|
fileprivate var sheetModule: NavigationModule? {
|
||||||
@@ -186,26 +201,29 @@ import SwiftUI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct NavigationTabCoordinatorView: View {
|
private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
||||||
@Bindable var navigationTabCoordinator: NavigationTabCoordinator
|
@Bindable var navigationTabCoordinator: NavigationTabCoordinator<Tag>
|
||||||
@State private var selectedTab: ObjectIdentifier?
|
|
||||||
|
@State private var standardAppearance = UITabBarAppearance()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $selectedTab) {
|
TabView(selection: $navigationTabCoordinator.selectedTab) {
|
||||||
ForEach(navigationTabCoordinator.tabModules) { module in
|
ForEach(navigationTabCoordinator.tabModules) { module in
|
||||||
module.coordinator?.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
|
.id(module.id)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label {
|
Label {
|
||||||
Text(module.title)
|
Text(module.details.title)
|
||||||
} icon: {
|
} icon: {
|
||||||
CompoundIcon(module.id == selectedTab ? module.selectedIcon : module.icon)
|
CompoundIcon(module.details.tag == navigationTabCoordinator.selectedTab ? module.details.selectedIcon : module.details.icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tag(module.id)
|
.tag(module.details.tag)
|
||||||
.id(module.id)
|
.badge(module.details.badgeCount)
|
||||||
.toolbar(.hidden, for: .tabBar)
|
.toolbar(module.details.barVisibility, for: .tabBar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.introspect(.tabView, on: .supportedVersions, customize: configureAppearance)
|
||||||
.sheet(item: $navigationTabCoordinator.sheetModule) { module in
|
.sheet(item: $navigationTabCoordinator.sheetModule) { module in
|
||||||
module.coordinator?.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
.id(module.id)
|
.id(module.id)
|
||||||
@@ -215,4 +233,10 @@ private struct NavigationTabCoordinatorView: View {
|
|||||||
.id(module.id)
|
.id(module.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureAppearance(_ tabBarController: UITabBarController) {
|
||||||
|
standardAppearance.configureWithDefaultBackground()
|
||||||
|
standardAppearance.stackedLayoutAppearance.normal.badgeBackgroundColor = .compound.iconAccentPrimary
|
||||||
|
tabBarController.tabBar.standardAppearance = standardAppearance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,19 @@ enum UserSessionFlowCoordinatorAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||||
|
enum HomeTab: Hashable { case chats, spaces }
|
||||||
|
|
||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||||
private let navigationTabCoordinator: NavigationTabCoordinator
|
private let navigationTabCoordinator: NavigationTabCoordinator<HomeTab>
|
||||||
private let appMediator: AppMediatorProtocol
|
private let appMediator: AppMediatorProtocol
|
||||||
private let appSettings: AppSettings
|
private let appSettings: AppSettings
|
||||||
|
|
||||||
private let onboardingFlowCoordinator: OnboardingFlowCoordinator
|
private let onboardingFlowCoordinator: OnboardingFlowCoordinator
|
||||||
private let onboardingStackCoordinator: NavigationStackCoordinator
|
private let onboardingStackCoordinator: NavigationStackCoordinator
|
||||||
private let chatsFlowCoordinator: ChatsFlowCoordinator
|
private let chatsFlowCoordinator: ChatsFlowCoordinator
|
||||||
|
private let chatsTabDetails: NavigationTabCoordinator<HomeTab>.TabDetails
|
||||||
|
private let spacesTabDetails: NavigationTabCoordinator<HomeTab>.TabDetails
|
||||||
|
|
||||||
private let actionsSubject: PassthroughSubject<UserSessionFlowCoordinatorAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<UserSessionFlowCoordinatorAction, Never> = .init()
|
||||||
var actionsPublisher: AnyPublisher<UserSessionFlowCoordinatorAction, Never> {
|
var actionsPublisher: AnyPublisher<UserSessionFlowCoordinatorAction, Never> {
|
||||||
@@ -68,6 +72,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
analytics: analytics,
|
analytics: analytics,
|
||||||
notificationManager: notificationManager,
|
notificationManager: notificationManager,
|
||||||
isNewLogin: isNewLogin)
|
isNewLogin: isNewLogin)
|
||||||
|
chatsTabDetails = .init(tag: HomeTab.chats, title: L10n.screenHomeTabChats, icon: \.chat, selectedIcon: \.chatSolid)
|
||||||
|
chatsTabDetails.barVisibility = .hidden
|
||||||
|
spacesTabDetails = .init(tag: HomeTab.spaces, title: L10n.screenHomeTabSpaces, icon: \.space, selectedIcon: \.spaceSolid)
|
||||||
|
|
||||||
onboardingStackCoordinator = NavigationStackCoordinator()
|
onboardingStackCoordinator = NavigationStackCoordinator()
|
||||||
onboardingFlowCoordinator = OnboardingFlowCoordinator(userSession: userSession,
|
onboardingFlowCoordinator = OnboardingFlowCoordinator(userSession: userSession,
|
||||||
@@ -81,8 +88,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
isNewLogin: isNewLogin)
|
isNewLogin: isNewLogin)
|
||||||
|
|
||||||
navigationTabCoordinator.setTabs([
|
navigationTabCoordinator.setTabs([
|
||||||
.init(coordinator: chatsSplitCoordinator, title: L10n.screenHomeTabChats, icon: \.chat, selectedIcon: \.chatSolid)
|
.init(coordinator: chatsSplitCoordinator, details: chatsTabDetails),
|
||||||
// .init(coordinator: BlankFormCoordinator(), title: L10n.screenHomeTabSpaces, icon: \.space, selectedIcon: \.spaceSolid)
|
.init(coordinator: BlankFormCoordinator(), details: spacesTabDetails)
|
||||||
])
|
])
|
||||||
|
|
||||||
setupObservers()
|
setupObservers()
|
||||||
@@ -103,17 +110,17 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||||
// There aren't any routes that directly target this flow yet, so pass them directly to the
|
// There aren't any routes that directly target this flow yet, so pass them directly to the
|
||||||
// chats flow coordinator.
|
// chats flow coordinator.
|
||||||
#warning("This should switch tabs to make sure the route is visible.")
|
|
||||||
chatsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
chatsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
|
navigationTabCoordinator.selectedTab = .chats
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearRoute(animated: Bool) {
|
func clearRoute(animated: Bool) {
|
||||||
chatsFlowCoordinator.clearRoute(animated: animated)
|
chatsFlowCoordinator.clearRoute(animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
#warning("This should use a publisher, combining it with the active tab.")
|
|
||||||
func isDisplayingRoomScreen(withRoomID roomID: String) -> Bool {
|
func isDisplayingRoomScreen(withRoomID roomID: String) -> Bool {
|
||||||
chatsFlowCoordinator.isDisplayingRoomScreen(withRoomID: roomID)
|
guard navigationTabCoordinator.selectedTab == .chats else { return false }
|
||||||
|
return chatsFlowCoordinator.isDisplayingRoomScreen(withRoomID: roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import XCTest
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class NavigationTabCoordinatorTests: XCTestCase {
|
class NavigationTabCoordinatorTests: XCTestCase {
|
||||||
private var navigationTabCoordinator: NavigationTabCoordinator!
|
enum TestTab { case tab, chats, spaces }
|
||||||
|
private var navigationTabCoordinator: NavigationTabCoordinator<TestTab>!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
navigationTabCoordinator = NavigationTabCoordinator()
|
navigationTabCoordinator = NavigationTabCoordinator()
|
||||||
@@ -21,21 +22,21 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
|||||||
XCTAssertTrue(navigationTabCoordinator.tabCoordinators.isEmpty)
|
XCTAssertTrue(navigationTabCoordinator.tabCoordinators.isEmpty)
|
||||||
|
|
||||||
let someCoordinator = SomeTestCoordinator()
|
let someCoordinator = SomeTestCoordinator()
|
||||||
navigationTabCoordinator.setTabs([.init(coordinator: someCoordinator, title: "Whatever", icon: \.help, selectedIcon: \.helpSolid)])
|
navigationTabCoordinator.setTabs([.init(coordinator: someCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [someCoordinator])
|
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [someCoordinator])
|
||||||
|
|
||||||
let chatsCoordinator = SomeTestCoordinator()
|
let chatsCoordinator = SomeTestCoordinator()
|
||||||
let spacesCoordinator = SomeTestCoordinator()
|
let spacesCoordinator = SomeTestCoordinator()
|
||||||
navigationTabCoordinator.setTabs([
|
navigationTabCoordinator.setTabs([
|
||||||
.init(coordinator: chatsCoordinator, title: "Chats", icon: \.chat, selectedIcon: \.chatSolid),
|
.init(coordinator: chatsCoordinator, details: .init(tag: .chats, title: "Chats", icon: \.chat, selectedIcon: \.chatSolid)),
|
||||||
.init(coordinator: spacesCoordinator, title: "Spaces", icon: \.space, selectedIcon: \.spaceSolid)
|
.init(coordinator: spacesCoordinator, details: .init(tag: .spaces, title: "Spaces", icon: \.space, selectedIcon: \.spaceSolid))
|
||||||
])
|
])
|
||||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSingleSheet() {
|
func testSingleSheet() {
|
||||||
let tabCoordinator = SomeTestCoordinator()
|
let tabCoordinator = SomeTestCoordinator()
|
||||||
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, title: "Tab", icon: \.help, selectedIcon: \.helpSolid)])
|
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||||
|
|
||||||
let coordinator = SomeTestCoordinator()
|
let coordinator = SomeTestCoordinator()
|
||||||
navigationTabCoordinator.setSheetCoordinator(coordinator)
|
navigationTabCoordinator.setSheetCoordinator(coordinator)
|
||||||
@@ -51,7 +52,7 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
|||||||
|
|
||||||
func testMultipleSheets() {
|
func testMultipleSheets() {
|
||||||
let tabCoordinator = SomeTestCoordinator()
|
let tabCoordinator = SomeTestCoordinator()
|
||||||
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, title: "Tab", icon: \.help, selectedIcon: \.helpSolid)])
|
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||||
|
|
||||||
let sheetCoordinator = SomeTestCoordinator()
|
let sheetCoordinator = SomeTestCoordinator()
|
||||||
navigationTabCoordinator.setSheetCoordinator(sheetCoordinator)
|
navigationTabCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||||
@@ -66,6 +67,22 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
|||||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationTabCoordinator.sheetCoordinator)
|
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationTabCoordinator.sheetCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFullScreenCover() {
|
||||||
|
let tabCoordinator = SomeTestCoordinator()
|
||||||
|
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||||
|
|
||||||
|
let coordinator = SomeTestCoordinator()
|
||||||
|
navigationTabCoordinator.setFullScreenCoverCoordinator(coordinator)
|
||||||
|
|
||||||
|
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
|
||||||
|
assertCoordinatorsEqual(coordinator, navigationTabCoordinator.fullScreenCoverCoordinator)
|
||||||
|
|
||||||
|
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
|
||||||
|
|
||||||
|
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
|
||||||
|
XCTAssertNil(navigationTabCoordinator.fullScreenCoverCoordinator)
|
||||||
|
}
|
||||||
|
|
||||||
func testTabDismissalCallbacks() {
|
func testTabDismissalCallbacks() {
|
||||||
let chatsCoordinator = SomeTestCoordinator()
|
let chatsCoordinator = SomeTestCoordinator()
|
||||||
let spacesCoordinator = SomeTestCoordinator()
|
let spacesCoordinator = SomeTestCoordinator()
|
||||||
@@ -74,12 +91,12 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
|||||||
expectation.expectedFulfillmentCount = 2
|
expectation.expectedFulfillmentCount = 2
|
||||||
|
|
||||||
navigationTabCoordinator.setTabs([
|
navigationTabCoordinator.setTabs([
|
||||||
.init(coordinator: chatsCoordinator, title: "Chats", icon: \.chat, selectedIcon: \.chatSolid) { expectation.fulfill() },
|
.init(coordinator: chatsCoordinator, details: .init(tag: .chats, title: "Chats", icon: \.chat, selectedIcon: \.chatSolid)) { expectation.fulfill() },
|
||||||
.init(coordinator: spacesCoordinator, title: "Spaces", icon: \.space, selectedIcon: \.spaceSolid) { expectation.fulfill() }
|
.init(coordinator: spacesCoordinator, details: .init(tag: .spaces, title: "Spaces", icon: \.space, selectedIcon: \.spaceSolid)) { expectation.fulfill() }
|
||||||
])
|
])
|
||||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
||||||
|
|
||||||
navigationTabCoordinator.setTabs([.init(coordinator: SomeTestCoordinator(), title: "Whatever", icon: \.help, selectedIcon: \.helpSolid)])
|
navigationTabCoordinator.setTabs([.init(coordinator: SomeTestCoordinator(), details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||||
waitForExpectations(timeout: 1.0)
|
waitForExpectations(timeout: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +111,17 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
|||||||
waitForExpectations(timeout: 1.0)
|
waitForExpectations(timeout: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFullScreenCoverDismissalCallback() {
|
||||||
|
let coordinator = SomeTestCoordinator()
|
||||||
|
let expectation = expectation(description: "Wait for callback")
|
||||||
|
navigationTabCoordinator.setFullScreenCoverCoordinator(coordinator) {
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
|
||||||
|
waitForExpectations(timeout: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ packages:
|
|||||||
# path: ../matrix-rust-sdk
|
# path: ../matrix-rust-sdk
|
||||||
Compound:
|
Compound:
|
||||||
url: https://github.com/element-hq/compound-ios
|
url: https://github.com/element-hq/compound-ios
|
||||||
revision: afc59afb1b1e4f4960e2f2a15e52d4e2e33fc889
|
revision: 8d7ca2e413026735b724044847ff1ae3b40563f0
|
||||||
# path: ../compound-ios
|
# path: ../compound-ios
|
||||||
AnalyticsEvents:
|
AnalyticsEvents:
|
||||||
url: https://github.com/matrix-org/matrix-analytics-events
|
url: https://github.com/matrix-org/matrix-analytics-events
|
||||||
|
|||||||
Reference in New Issue
Block a user