diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index fae39a2a8..e2a8bfd79 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 2BA59D0AEFB4B82A2EC2A326 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90733775209F4D4D366A268F /* RootRouterType.swift */; }; + 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; @@ -96,6 +97,7 @@ 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; + 36C10EDEDC0466E3A9D63132 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */; }; 38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; }; @@ -151,6 +153,7 @@ 6298AB0906DDD3525CD78C6B /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210612D17A39369480FC183 /* MediaSource.swift */; }; 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; }; + 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */; }; 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; }; @@ -440,6 +443,7 @@ 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorViewPresentable.swift; sourceTree = ""; }; + 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = ""; }; 1C429043E986008B97736636 /* ab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ab; path = ab.lproj/Localizable.strings; sourceTree = ""; }; @@ -695,6 +699,7 @@ A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = ""; }; A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = ""; }; + A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = ""; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; @@ -840,6 +845,7 @@ F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; @@ -1361,6 +1367,7 @@ C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, A05707BF550D770168A406DB /* LoginViewModelTests.swift */, + F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, @@ -1388,6 +1395,7 @@ 289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */, A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */, F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */, + A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */, ); path = Items; sourceTree = ""; @@ -1697,6 +1705,7 @@ C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */, 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */, F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */, + 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */, ); path = Timeline; sourceTree = ""; @@ -2349,6 +2358,7 @@ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, + 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, @@ -2615,6 +2625,8 @@ 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */, 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */, AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */, + 36C10EDEDC0466E3A9D63132 /* VideoRoomTimelineItem.swift in Sources */, + 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */, 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */, 6DF37000571B1BC6D134CC9E /* WeakDictionary.swift in Sources */, 32BA37B01B05261FCF2D4B45 /* WeakDictionaryKeyReference.swift in Sources */, @@ -3244,7 +3256,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = "1.0.17-alpha"; + version = "1.0.18-alpha"; }; }; 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2a5c097b5..a22454570 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS", "state" : { - "revision" : "33660c271c961f8ce1084cc13f2ea8195e864f7d", - "version" : "1.5.0" + "revision" : "3d36a58a2b736f7bc499453e996a704929b25080", + "version" : "1.6.0" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/devicekit/DeviceKit", "state" : { - "revision" : "20e0991f3975916ab0f6d58db84d8bc64f883537", - "version" : "4.7.0" + "revision" : "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6", + "version" : "4.9.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher", "state" : { - "revision" : "022e4eeb79f817748544b318b991d9a70036bbf8", - "version" : "7.2.4" + "revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5", + "version" : "7.4.1" } }, { @@ -78,7 +78,7 @@ "location" : "https://github.com/matrix-org/matrix-analytics-events", "state" : { "branch" : "main", - "revision" : "53ad46ba1ea1ee8f21139dda3c351890846a202f" + "revision" : "4bcc7f566b165cd2d8fde07d23bda77e1d9fbb2d" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "85335b5f6766753100983dced122731dfa5fa308", - "version" : "1.0.17-alpha" + "revision" : "c8494d858cfb60eb6167d76790b819c9dc15e822", + "version" : "1.0.18-alpha" } }, { @@ -102,16 +102,16 @@ { "identity" : "sentry-cocoa", "kind" : "remoteSourceControl", - "location" : "https://github.com/getsentry/sentry-cocoa.git", + "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "f906913c14eadb6fd31988db1ea9ff10aec9f198", - "version" : "7.18.1" + "revision" : "71fd3032635fed58ae1c1ba22bb7ffa158dbb5ee", + "version" : "7.30.2" } }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { "revision" : "f29e2014f6230cf7d5138fc899da51c7f513d467", "version" : "1.10.0" diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 53717dcff..2cd87a0b1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -142,7 +142,7 @@ struct TimelineItemBubbledStylerView: View { } private var shouldAvoidBubbling: Bool { - timelineItem is ImageRoomTimelineItem + timelineItem is ImageRoomTimelineItem || timelineItem is VideoRoomTimelineItem } private var alignment: HorizontalAlignment { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift new file mode 100644 index 000000000..a6cb50d23 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift @@ -0,0 +1,112 @@ +// +// 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 + +struct VideoRoomTimelineView: View { + let timelineItem: VideoRoomTimelineItem + + var body: some View { + TimelineStyler(timelineItem: timelineItem) { + if let image = timelineItem.image { + thumbnail(with: image) + } else if let blurhash = timelineItem.blurhash, + // Build a small blurhash image so that it's fast + let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) { + thumbnail(with: image) + } else { + ZStack { + Rectangle() + .foregroundColor(.element.systemGray6) + .opacity(0.3) + + ProgressView("Loading") + .frame(maxWidth: .infinity) + } + .aspectRatio(timelineItem.aspectRatio, contentMode: .fit) + } + } + .id(timelineItem.id) + .animation(.elementDefault, value: timelineItem.image) + .frame(maxHeight: 1000.0) + } + + @ViewBuilder + private func thumbnail(with image: UIImage) -> some View { + ZStack { + Image(uiImage: image) + .resizable() + .aspectRatio(timelineItem.aspectRatio, contentMode: .fit) + Image(systemName: "play.circle.fill") + .resizable() + .frame(width: 50, height: 50) + .background(.ultraThinMaterial, in: Circle()) + .foregroundColor(.white) + } + } +} + +struct VideoRoomTimelineView_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + body.preferredColorScheme(.light) + .timelineStyle(.plain) + body.preferredColorScheme(.dark) + .timelineStyle(.plain) + } + + @ViewBuilder + static var body: some View { + VStack(spacing: 20.0) { + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, + text: "Some video", + timestamp: "Now", + inGroupState: .single, + isOutgoing: false, + senderId: "Bob", + duration: 21, + source: nil, + thumbnailSource: nil, + image: UIImage(systemName: "photo"))) + + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, + text: "Some other video", + timestamp: "Now", + inGroupState: .single, + isOutgoing: false, + senderId: "Bob", + duration: 22, + source: nil, + thumbnailSource: nil, + image: nil)) + + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, + text: "Blurhashed video", + timestamp: "Now", + inGroupState: .single, + isOutgoing: false, + senderId: "Bob", + duration: 23, + source: nil, + thumbnailSource: nil, + image: nil, + aspectRatio: 0.7, + blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")) + } + } +} diff --git a/ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift b/ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift index 7d251083d..b64cc7942 100644 --- a/ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift +++ b/ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift @@ -20,12 +20,7 @@ import Foundation public extension AnalyticsEvent.UserProperties { // Initializer for Element. Strips all Web properties. init(ftueUseCaseSelection: FtueUseCaseSelection?, numFavouriteRooms: Int?, numSpaces: Int?, allChatsActiveFilter: AllChatsActiveFilter?) { - self.init(WebMetaSpaceFavouritesEnabled: nil, - WebMetaSpaceHomeAllRooms: nil, - WebMetaSpaceHomeEnabled: nil, - WebMetaSpaceOrphansEnabled: nil, - WebMetaSpacePeopleEnabled: nil, - allChatsActiveFilter: nil, + self.init(allChatsActiveFilter: nil, ftueUseCaseSelection: ftueUseCaseSelection, numFavouriteRooms: numFavouriteRooms, numSpaces: numSpaces) diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index 288718827..f3aeb9e17 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -98,7 +98,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol { switch timelineItem { case let item as ImageRoomTimelineItem: - await loadImageForTimelineItem(item) + await loadImageForImageTimelineItem(item) + case let item as VideoRoomTimelineItem: + await loadImageForVideoTimelineItem(item) default: break } @@ -235,7 +237,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return .middle } - private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) async { + private func loadImageForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async { if timelineItem.image != nil { return } @@ -258,6 +260,30 @@ class RoomTimelineController: RoomTimelineControllerProtocol { break } } + + private func loadImageForVideoTimelineItem(_ timelineItem: VideoRoomTimelineItem) async { + if timelineItem.image != nil { + return + } + + guard let source = timelineItem.thumbnailSource else { + return + } + + switch await mediaProvider.loadImageFromSource(source) { + case .success(let image): + guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }), + var item = timelineItems[index] as? ImageRoomTimelineItem else { + return + } + + item.image = image + timelineItems[index] = item + callbacks.send(.updatedTimelineItem(timelineItem.id)) + case .failure: + break + } + } private func loadUserAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async { if timelineItem.shouldShowSenderDetails == false { diff --git a/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift index 1215c8d5a..291210805 100644 --- a/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift @@ -109,3 +109,35 @@ extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent content.info?.blurhash } } + +extension MatrixRustSDK.VideoMessageContent: MessageContentProtocol { } + +/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`. +extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent { + var source: MediaSource { + MediaSource(source: content.source) + } + + var thumbnailSource: MediaSource? { + guard let src = content.info?.thumbnailSource else { + return nil + } + return MediaSource(source: src) + } + + var duration: UInt64 { + content.info?.duration ?? 0 + } + + var width: CGFloat? { + content.info?.width.map(CGFloat.init) + } + + var height: CGFloat? { + content.info?.height.map(CGFloat.init) + } + + var blurhash: String? { + content.info?.blurhash + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/VideoRoomTimelineItem.swift new file mode 100644 index 000000000..455fcf70e --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/VideoRoomTimelineItem.swift @@ -0,0 +1,42 @@ +// +// 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 VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable { + let id: String + let text: String + let timestamp: String + let inGroupState: TimelineItemInGroupState + let isOutgoing: Bool + + let senderId: String + var senderDisplayName: String? + var senderAvatar: UIImage? + + let duration: UInt64 + let source: MediaSource? + let thumbnailSource: MediaSource? + var image: UIImage? + + var width: CGFloat? + var height: CGFloat? + var aspectRatio: CGFloat? + var blurhash: String? + + var properties = RoomTimelineItemProperties() +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index e7bcf6902..e79495a94 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -59,6 +59,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { case .image(content: let content): let message = MessageTimelineItem(item: eventItemProxy.item, content: content) return buildImageTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage) + case .video(let content): + let message = MessageTimelineItem(item: eventItemProxy.item, content: content) + return buildVideoTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage) case .notice(content: let content): let message = MessageTimelineItem(item: eventItemProxy.item, content: content) return buildNoticeTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage) @@ -187,6 +190,37 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: message.isEdited, reactions: aggregateReactions(message.reactions))) } + + private func buildVideoTimelineItemFromMessage(_ message: MessageTimelineItem, + _ isOutgoing: Bool, + _ inGroupState: TimelineItemInGroupState, + _ displayName: String?, + _ avatarImage: UIImage?) -> RoomTimelineItemProtocol { + var aspectRatio: CGFloat? + if let width = message.width, + let height = message.height { + aspectRatio = width / height + } + + return VideoRoomTimelineItem(id: message.id, + text: message.body, + timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened), + inGroupState: inGroupState, + isOutgoing: isOutgoing, + senderId: message.sender, + senderDisplayName: displayName, + senderAvatar: avatarImage, + duration: message.duration, + source: message.source, + thumbnailSource: message.thumbnailSource, + image: mediaProvider.imageFromSource(message.thumbnailSource), + width: message.width, + height: message.height, + aspectRatio: aspectRatio, + blurhash: message.blurhash, + properties: RoomTimelineItemProperties(isEdited: message.isEdited, + reactions: aggregateReactions(message.reactions))) + } private func buildNoticeTimelineItemFromMessage(_ message: MessageTimelineItem, _ isOutgoing: Bool, diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift index 2bb6c6ef6..0f1295faa 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift @@ -23,6 +23,8 @@ struct RoomTimelineViewFactory: RoomTimelineViewFactoryProtocol { return .text(item) case let item as ImageRoomTimelineItem: return .image(item) + case let item as VideoRoomTimelineItem: + return .video(item) case let item as SeparatorRoomTimelineItem: return .separator(item) case let item as NoticeRoomTimelineItem: diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift index 77b14d4aa..5d8107c2f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift @@ -21,6 +21,7 @@ enum RoomTimelineViewProvider: Identifiable, Equatable { case text(TextRoomTimelineItem) case separator(SeparatorRoomTimelineItem) case image(ImageRoomTimelineItem) + case video(VideoRoomTimelineItem) case emote(EmoteRoomTimelineItem) case notice(NoticeRoomTimelineItem) case redacted(RedactedRoomTimelineItem) @@ -34,6 +35,8 @@ enum RoomTimelineViewProvider: Identifiable, Equatable { return item.id case .image(let item): return item.id + case .video(let item): + return item.id case .emote(let item): return item.id case .notice(let item): @@ -55,6 +58,8 @@ extension RoomTimelineViewProvider: View { SeparatorRoomTimelineView(timelineItem: item) case .image(let item): ImageRoomTimelineView(timelineItem: item) + case .video(let item): + VideoRoomTimelineView(timelineItem: item) case .emote(let item): EmoteRoomTimelineView(timelineItem: item) case .notice(let item): diff --git a/changelog.d/237.feature b/changelog.d/237.feature new file mode 100644 index 000000000..831ee63a6 --- /dev/null +++ b/changelog.d/237.feature @@ -0,0 +1 @@ +Timeline: Display video messages. diff --git a/project.yml b/project.yml index abc58d9b5..f18f42f49 100644 --- a/project.yml +++ b/project.yml @@ -35,7 +35,7 @@ include: packages: MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.0.17-alpha + exactVersion: 1.0.18-alpha # path: ../matrix-rust-components-swift DesignKit: path: ./