From d2fa71c37f47f8939fbb1502d4846e28eef0e261 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 28 Jan 2026 14:33:28 +0100 Subject: [PATCH] refactored the flows to use a state and an event for the transfer ownership screen --- .../ChatsTabFlowCoordinator.swift | 35 ++++++++++++++++++- .../ChatsTabFlowCoordinatorStateMachine.swift | 11 ++++++ .../RoomFlowCoordinator.swift | 11 ++++-- .../RoomFlowCoordinatorStateMachine.swift | 9 +++++ .../SpaceFlowCoordinator.swift | 25 ++++++++++--- .../SpaceSettingsFlowCoordinator.swift | 26 +++++++++++--- .../View/RoomChangeRolesScreen.swift | 2 +- .../LeaveSpace/View/LeaveSpaceView.swift | 2 +- .../SpaceScreen/SpaceScreenCoordinator.swift | 2 +- .../SpaceScreen/SpaceScreenViewModel.swift | 4 +-- ...eSpaceView.Only-Admin-Rooms-iPad-en-GB.png | 4 +-- ...SpaceView.Only-Admin-Rooms-iPad-pseudo.png | 4 +-- ...paceView.Only-Admin-Rooms-iPhone-en-GB.png | 4 +-- ...aceView.Only-Admin-Rooms-iPhone-pseudo.png | 4 +-- 14 files changed, 116 insertions(+), 27 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift index cea87b7c7..d815cb43d 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift @@ -168,7 +168,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { if case .room(roomID) = stateMachine.state.detailState { roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) } else { - stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .transferOwnership)) + stateMachine.processEvent(.presentTransferOwnershipScreen(roomID: roomID)) } case .accountProvisioningLink, .settings, .chatBackupSettings, .call, .genericCallLink: break // These routes cannot be handled. @@ -251,6 +251,11 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { case (.declineAndBlockUserScreen, .dismissedDeclineAndBlockScreen, .roomList): break + case (.roomList, .presentTransferOwnershipScreen(let roomID), .transferOwnershipScreen): + Task { await self.presentTransferOwnershipScreen(roomID: roomID) } + case (.transferOwnershipScreen, .dismissedTransferOwnershipScreen, .roomList): + break + case (.roomList(let roomListSelectedRoomID), .showShareExtensionRoomList, .shareExtensionRoomList(let sharePayload)): Task { if roomListSelectedRoomID != nil { @@ -469,6 +474,34 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { } } + private func presentTransferOwnershipScreen(roomID: String) async { + guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { + return + } + await roomProxy.subscribeForUpdates() + + let parameters = RoomChangeRolesScreenCoordinatorParameters(mode: .owner, + roomProxy: roomProxy, + mediaProvider: userSession.mediaProvider, + userIndicatorController: flowParameters.userIndicatorController, + analytics: flowParameters.analytics) + let stackCoordinator = NavigationStackCoordinator() + let coordinator = RoomChangeRolesScreenCoordinator(parameters: parameters) + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .complete: + navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + stackCoordinator.setRootCoordinator(coordinator) + navigationSplitCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.stateMachine.processEvent(.dismissedTransferOwnershipScreen) + } + } + // MARK: Room Flow private func startRoomFlow(roomID: String, diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift index 9e4b56d26..66f3da203 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift @@ -52,6 +52,8 @@ class ChatsTabFlowCoordinatorStateMachine { case declineAndBlockUserScreen(detailState: DetailState?) + case transferOwnershipScreen(detailState: DetailState?) + /// The state of the currently selected room var detailState: DetailState? { switch self { @@ -65,6 +67,7 @@ class ChatsTabFlowCoordinatorStateMachine { .logoutConfirmationScreen(let detailState), .roomDirectorySearchScreen(let detailState), .reportRoomScreen(let detailState), + .transferOwnershipScreen(let detailState), .declineAndBlockUserScreen(let detailState): detailState } @@ -134,6 +137,9 @@ class ChatsTabFlowCoordinatorStateMachine { case presentDeclineAndBlockScreen(userID: String, roomID: String) case dismissedDeclineAndBlockScreen + + case presentTransferOwnershipScreen(roomID: String) + case dismissedTransferOwnershipScreen } private let stateMachine: StateMachine @@ -208,6 +214,11 @@ class ChatsTabFlowCoordinatorStateMachine { case (.declineAndBlockUserScreen(let detailState), .dismissedDeclineAndBlockScreen): return .roomList(detailState: detailState) + case (.roomList(let detailState), .presentTransferOwnershipScreen): + return .transferOwnershipScreen(detailState: detailState) + case (.transferOwnershipScreen(let detailState), .dismissedTransferOwnershipScreen): + return .roomList(detailState: detailState) + default: return nil } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 8936eae25..4abaede60 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -214,7 +214,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { await storeAndSubscribeToRoomProxy(roomProxy) } - presentTransferOwnershipScreen() + stateMachine.tryEvent(.presentTransferOwnershipScreen) } } } @@ -274,7 +274,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { .store(in: &cancellables) stackCoordinator.setRootCoordinator(coordinator) - navigationStackCoordinator.setSheetCoordinator(stackCoordinator, animated: true) + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissedTransferOwnershipScreen) + } } private func handleRoomRoute(roomID: String, via: [String], presentationAction: PresentationAction? = nil, animated: Bool) async { @@ -531,6 +533,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (_, .presentInviteUsersScreen, .inviteUsersScreen): presentInviteUsersScreen() + + case (_, .presentTransferOwnershipScreen, .transferOwnershipScreen): + presentTransferOwnershipScreen() default: break @@ -937,7 +942,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case .presentReportRoomScreen: stateMachine.tryEvent(.presentReportRoomScreen) case .transferOwnership: - presentTransferOwnershipScreen() + stateMachine.tryEvent(.presentTransferOwnershipScreen) } } .store(in: &cancellables) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift index 6c8475ffa..1dab20b7f 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift @@ -83,6 +83,7 @@ extension RoomFlowCoordinator { case manageAuthorizedSpacesScreen(previousState: State) case reportRoom(previousState: State) case declineAndBlockScreen + case transferOwnershipScreen(previousState: State) /// A child flow is in progress. case presentingChild(childRoomID: String, previousState: State) @@ -185,6 +186,9 @@ extension RoomFlowCoordinator { case presentDeclineAndBlockScreen(userID: String) case dismissDeclineAndBlockScreen + case presentTransferOwnershipScreen + case dismissedTransferOwnershipScreen + case startMembersFlow(entryPoint: RoomMembersFlowCoordinatorEntryPoint) case stopMembersFlow } @@ -392,6 +396,11 @@ extension RoomFlowCoordinator { return .inviteUsersScreen(previousState: fromState) case (.inviteUsersScreen(let previousState), .dismissInviteUsersScreen): return previousState + + case (_, .presentTransferOwnershipScreen): + return .transferOwnershipScreen(previousState: fromState) + case (.transferOwnershipScreen(let previousState), .dismissedTransferOwnershipScreen): + return previousState default: return nil diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index 5c7c6ddc2..488a147e6 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -69,9 +69,11 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case createChildRoomFlow case leftSpace + + case transferOwnership } - enum Event: EventType { + enum Event: EventType, Equatable, Hashable { /// The flow is being started. case start /// The flow is being started for an unjoined space. @@ -108,6 +110,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case startCreateChildRoomFlow case stopCreateChildRoomFlow + + case presentTransferOwnership + case dismissedTransferOwnership } private let stateMachine: StateMachine @@ -179,6 +184,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case .createChildRoomFlow: navigationStackCoordinator.setSheetCoordinator(nil) clearRoute(animated: animated) // Re-run with the state machine back in the .space state. + case .transferOwnership: + navigationStackCoordinator.setSheetCoordinator(nil) + clearRoute(animated: animated) // Re-run with the state machine back in the .space state. } } @@ -206,6 +214,12 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { } stateMachine.addRoutes(event: .dismissedAddRooms, transitions: [.addingRooms => .space]) + stateMachine.addRoutes(event: .presentTransferOwnership, transitions: [.space => .transferOwnership]) { [weak self] context in + guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } + self.presentTransferOwnershipScreen(roomProxy: roomProxy) + } + stateMachine.addRoutes(event: .dismissedTransferOwnership, transitions: [.transferOwnership => .space]) + stateMachine.addRouteMapping { event, fromState, userInfo in guard event == .startChildFlow else { return nil } guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") } @@ -350,7 +364,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case .displayCreateChildRoomFlow(let space): stateMachine.tryEvent(.startCreateChildRoomFlow, userInfo: space) case .displayTransferOwnership(let roomProxy): - presentTransferOwnershipScreen(roomProxy: roomProxy) + stateMachine.tryEvent(.presentTransferOwnership, userInfo: roomProxy) } } .store(in: &cancellables) @@ -450,16 +464,17 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { let stackCoordinator = NavigationStackCoordinator() let coordinator = RoomChangeRolesScreenCoordinator(parameters: parameters) coordinator.actionsPublisher.sink { [weak self] action in - guard let self else { return } switch action { case .complete: - navigationStackCoordinator.setSheetCoordinator(nil) + self?.navigationStackCoordinator.setSheetCoordinator(nil) } } .store(in: &cancellables) stackCoordinator.setRootCoordinator(coordinator) - navigationStackCoordinator.setSheetCoordinator(stackCoordinator) + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissedTransferOwnership) + } } // MARK: - Other flows diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift index 276de726e..75315efac 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -35,6 +35,8 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case membersFlow case manageAuthorizedSpacesScreen + + case transferOwnership } enum Event: EventType { @@ -60,6 +62,9 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case presentManageAuthorizedSpacesScreen case dismissedManageAuthorizedSpacesScreen + + case presentTransferOwnership + case dismissedTransferOwnership } private let roomProxy: JoinedRoomProxyProtocol @@ -135,6 +140,11 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { return .manageAuthorizedSpacesScreen case (.manageAuthorizedSpacesScreen, .dismissedManageAuthorizedSpacesScreen): return .securityAndPrivacy + + case (.spaceSettings, .presentTransferOwnership): + return .transferOwnership + case (.transferOwnership, .dismissedTransferOwnership): + return .spaceSettings case (.spaceSettings, .startMembersListFlow): return .membersFlow @@ -181,7 +191,12 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { presentManageAuthorizedSpacesScreen(selection: selection) case (.manageAuthorizedSpacesScreen, .dismissedManageAuthorizedSpacesScreen, .securityAndPrivacy): break - + + case (.spaceSettings, .presentTransferOwnership, .transferOwnership): + presentTransferOwnershipScreen() + case (.transferOwnership, .dismissedTransferOwnership, .spaceSettings): + break + case (.spaceSettings, .startMembersListFlow, .membersFlow): startMembersListFlow() case (.membersFlow, .stopMembersListFlow, .spaceSettings): @@ -223,7 +238,7 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { leftRoom = true navigationStackCoordinator.pop() case .transferOwnership: - presentTransferOwnershipScreen() + stateMachine.tryEvent(.presentTransferOwnership) case .presentRecipientDetails, .presentNotificationSettingsScreen, .presentReportRoomScreen, .presentInviteUsersScreen, .presentPollsHistory, .presentCall, .presentPinnedEventsTimeline, .presentMediaEventsTimeline, .presentKnockingRequestsListScreen: @@ -336,16 +351,17 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { let stackCoordinator = NavigationStackCoordinator() let coordinator = RoomChangeRolesScreenCoordinator(parameters: parameters) coordinator.actionsPublisher.sink { [weak self] action in - guard let self else { return } switch action { case .complete: - navigationStackCoordinator.setSheetCoordinator(nil) + self?.navigationStackCoordinator.setSheetCoordinator(nil) } } .store(in: &cancellables) stackCoordinator.setRootCoordinator(coordinator) - navigationStackCoordinator.setSheetCoordinator(stackCoordinator, animated: true) + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissedTransferOwnership) + } } // MARK: - Other flows diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift index d4bee1853..1e271b077 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift @@ -99,7 +99,7 @@ struct RoomChangeRolesScreen: View { .disabled(!context.viewState.hasChanges) } - if context.viewState.mode == .owner { + if context.viewState.mode == .owner || context.viewState.hasChanges { ToolbarItem(placement: .cancellationAction) { ToolbarButton(role: .cancel) { context.send(viewAction: .cancel) diff --git a/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift index 874714b76..a16e265f7 100644 --- a/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift @@ -66,7 +66,7 @@ struct LeaveSpaceView: View { Section { ForEach(context.viewState.leaveHandle.rooms, id: \.spaceServiceRoom.id) { room in LeaveSpaceRoomDetailsCell(room: room, - hideSelection: !context.viewState.leaveHandle.canLeave, + hideSelection: !room.canLeave, mediaProvider: context.mediaProvider) { context.send(viewAction: .toggleRoom(roomID: room.spaceServiceRoom.id)) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index 20da565e0..c932b5441 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -79,7 +79,7 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.addExistingChildren) case .displayCreateChildRoomFlow(let space): actionsSubject.send(.displayCreateChildRoomFlow(space: space)) - case .presentTransferOwnership(roomProxy: let roomProxy): + case .presentTransferOwnership(let roomProxy): actionsSubject.send(.displayTransferOwnership(roomProxy: roomProxy)) } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index 5185327e7..0af2d6aa4 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -254,7 +254,7 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc state.bindings.leaveSpaceViewModel = nil case .presentRolesAndPermissions: guard let roomProxy = state.roomProxy else { - fatalError("The space screen should always have a room proxy") + fatalError("There should always be a room proxy available for joined spaces.") } state.bindings.leaveSpaceViewModel = nil actionsSubject.send(.presentRolesAndPermissions(roomProxy: roomProxy)) @@ -263,7 +263,7 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc actionsSubject.send(.leftSpace) case .presentTransferOwnership: guard let roomProxy = state.roomProxy else { - fatalError("The space screen should always have a room proxy") + fatalError("There should always be a room proxy available for joined spaces.") } state.bindings.leaveSpaceViewModel = nil actionsSubject.send(.presentTransferOwnership(roomProxy: roomProxy)) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-en-GB.png index d623e6522..e58ac6aa4 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ad306e0be48b82c3d289c1900afdf79b2efcf0403f3e86cbb09013e7705a16 -size 143185 +oid sha256:60e6e432817c3a55835ce744166ff3a9f9af6977109b98abb5c1103a88480ae1 +size 138961 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png index 5223aabcd..dfb7b81fa 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:153e608bd51747e151a6901bfc4214ef355d4f2b422e28c16e888948306c6c70 -size 166095 +oid sha256:3b6a5f02f6177dd4bb2ea2897365c20339082fdbd08d0851246662937e746afd +size 162011 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-en-GB.png index 0018011fb..37e55b91a 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c1946343246044027e21fc27989935ee7fa80768f76542fc8a56c9eca4db792 -size 93713 +oid sha256:5fcd4c08581c9c01569b6297e7122067e30bf29a5c82ad142f41269d58939fe9 +size 90649 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-pseudo.png index 3266d7e17..dfb13355b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ed5e1592778ff49f919299c9f7fc2e530827890310f9af77133acdc12633a9f -size 121020 +oid sha256:d386b1c14b44d784e78d1f52ade674d4118f3aefc865f83a5c66aaeaff2f5861 +size 118139