From e1848a3e15343b59a2d1b8ace412f972e3f840f2 Mon Sep 17 00:00:00 2001 From: fanyu Date: Tue, 24 May 2022 20:34:54 +0800 Subject: [PATCH 1/4] Send multiple images --- Mixin.xcodeproj/project.pbxproj | 24 ++ .../ic_video_bold.imageset/Contents.json | 22 ++ .../ic_video_bold@2x.png | Bin 0 -> 482 bytes .../ic_video_bold@3x.png | Bin 0 -> 714 bytes .../ic_media_close.imageset/Contents.json | 54 ++++ .../ic_media_close@2x.png | Bin 0 -> 1168 bytes .../ic_media_close@3x.png | Bin 0 -> 1673 bytes .../ic_media_close_dark@2x.png | Bin 0 -> 1475 bytes .../ic_media_close_dark@3x.png | Bin 0 -> 2096 bytes .../ic_photo_checkmark.imageset/Contents.json | 22 ++ .../ic_photo_checkmark@2x.png | Bin 0 -> 1387 bytes .../ic_photo_checkmark@3x.png | Bin 0 -> 1949 bytes .../Contents.json | 22 ++ .../ic_photo_unselected@2x.png | Bin 0 -> 1315 bytes .../ic_photo_unselected@3x.png | Bin 0 -> 1874 bytes .../Contents.json | 22 ++ .../ic_photo_unselected_narrow@2x.png | Bin 0 -> 1101 bytes .../ic_photo_unselected_narrow@3x.png | Bin 0 -> 1572 bytes .../Chat/Cells/PhotoInputGridCell.swift | 17 ++ .../Chat/Cells/SelectedMediaCell.swift | 50 ++++ .../ConversationInputViewController.swift | 9 +- .../Chat/PhotoInputGridViewController.swift | 44 +++- .../Chat/PhotoInputViewController.swift | 238 ++++++++++++++++++ ...electedPhotoInputItemsViewController.swift | 134 ++++++++++ .../Chat/Views/MediaTypeOverlayView.swift | 3 + .../Chat/Views/MediaTypeOverlayView.xib | 16 +- .../UserInterface/Storyboard/Chat.storyboard | 193 +++++++++++++- .../Windows/Cells/MediaPreviewCell.swift | 62 +++++ .../Windows/Cells/MediaPreviewCell.xib | 65 +++++ ...SelectedPhotoInputItemsPreviewWindow.swift | 181 +++++++++++++ .../SelectedPhotoInputItemsPreviewWindow.xib | 150 +++++++++++ 31 files changed, 1310 insertions(+), 18 deletions(-) create mode 100644 Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/Contents.json create mode 100644 Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@2x.png create mode 100644 Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@3x.png create mode 100644 Mixin/Assets.xcassets/ic_media_close.imageset/Contents.json create mode 100644 Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@2x.png create mode 100644 Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@3x.png create mode 100644 Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@2x.png create mode 100644 Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@3x.png create mode 100644 Mixin/Assets.xcassets/ic_photo_checkmark.imageset/Contents.json create mode 100644 Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@2x.png create mode 100644 Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@3x.png create mode 100644 Mixin/Assets.xcassets/ic_photo_unselected.imageset/Contents.json create mode 100644 Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@2x.png create mode 100644 Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@3x.png create mode 100644 Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/Contents.json create mode 100644 Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@2x.png create mode 100644 Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@3x.png create mode 100644 Mixin/UserInterface/Controllers/Chat/Cells/SelectedMediaCell.swift create mode 100644 Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift create mode 100644 Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift create mode 100644 Mixin/UserInterface/Windows/Cells/MediaPreviewCell.xib create mode 100644 Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift create mode 100644 Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib diff --git a/Mixin.xcodeproj/project.pbxproj b/Mixin.xcodeproj/project.pbxproj index 66627d459b..26d033e9cb 100644 --- a/Mixin.xcodeproj/project.pbxproj +++ b/Mixin.xcodeproj/project.pbxproj @@ -639,6 +639,12 @@ 7CE2DC9A28587DE100AF00AE /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B8BB58F234F36C000991ACB /* Colors.xcassets */; }; 7CE2DE102858B52000AF00AE /* WallpaperImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE2DE0F2858B52000AF00AE /* WallpaperImageView.swift */; }; 7CE3A25C2771A8AB006BE765 /* DeleteAccountVerifyCodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE3A25B2771A8AB006BE765 /* DeleteAccountVerifyCodeViewController.swift */; }; + 7CE4BA1F283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CE4BA1D283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib */; }; + 7CE4BA20283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA1E283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift */; }; + 7CE4BA23283CD297001C87D5 /* MediaPreviewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CE4BA21283CD297001C87D5 /* MediaPreviewCell.xib */; }; + 7CE4BA24283CD297001C87D5 /* MediaPreviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA22283CD297001C87D5 /* MediaPreviewCell.swift */; }; + 7CE4BA26283CD2B4001C87D5 /* SelectedPhotoInputItemsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA25283CD2B3001C87D5 /* SelectedPhotoInputItemsViewController.swift */; }; + 7CE4BA28283CD2C9001C87D5 /* SelectedMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA27283CD2C9001C87D5 /* SelectedMediaCell.swift */; }; 7CE5E7A8269BDA29000B7904 /* HomeAppsPinTipsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE5E7A6269BDA29000B7904 /* HomeAppsPinTipsViewController.swift */; }; 7CE5E7A9269BDA29000B7904 /* HomeAppsPinTipsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CE5E7A7269BDA29000B7904 /* HomeAppsPinTipsView.xib */; }; 7CF2FEA626AA89BA00D3A5B3 /* StickersAlbumPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CF2FEA526AA89BA00D3A5B3 /* StickersAlbumPreviewViewController.swift */; }; @@ -1658,6 +1664,12 @@ 7CDBA58D28F7B6CB00AC3777 /* TransferSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSearchViewController.swift; sourceTree = ""; }; 7CE2DE0F2858B52000AF00AE /* WallpaperImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperImageView.swift; sourceTree = ""; }; 7CE3A25B2771A8AB006BE765 /* DeleteAccountVerifyCodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountVerifyCodeViewController.swift; sourceTree = ""; }; + 7CE4BA1D283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SelectedPhotoInputItemsPreviewWindow.xib; sourceTree = ""; }; + 7CE4BA1E283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedPhotoInputItemsPreviewWindow.swift; sourceTree = ""; }; + 7CE4BA21283CD297001C87D5 /* MediaPreviewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MediaPreviewCell.xib; sourceTree = ""; }; + 7CE4BA22283CD297001C87D5 /* MediaPreviewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPreviewCell.swift; sourceTree = ""; }; + 7CE4BA25283CD2B3001C87D5 /* SelectedPhotoInputItemsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedPhotoInputItemsViewController.swift; sourceTree = ""; }; + 7CE4BA27283CD2C9001C87D5 /* SelectedMediaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedMediaCell.swift; sourceTree = ""; }; 7CE5E7A6269BDA29000B7904 /* HomeAppsPinTipsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAppsPinTipsViewController.swift; sourceTree = ""; }; 7CE5E7A7269BDA29000B7904 /* HomeAppsPinTipsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HomeAppsPinTipsView.xib; sourceTree = ""; }; 7CF2FEA526AA89BA00D3A5B3 /* StickersAlbumPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersAlbumPreviewViewController.swift; sourceTree = ""; }; @@ -2861,6 +2873,7 @@ 7B915F73215FB0C100A562C6 /* GiphySearchViewController.swift */, 7B05CFDC22293B72006DA9E3 /* PhotoInputViewController.swift */, 7B93CAA4222963120053AE90 /* PhotoInputGridViewController.swift */, + 7CE4BA25283CD2B3001C87D5 /* SelectedPhotoInputItemsViewController.swift */, 7B6A4045228400AF0037C7E5 /* MessageReceiverViewController.swift */, 7BFD3457228589ED00524EA0 /* ContactSelectorViewController.swift */, 7BEBCE2C228185130037BF18 /* MediaPreviewViewController.swift */, @@ -2976,6 +2989,8 @@ 7C0FAAC827E07A0A008D4021 /* ExpiredMessageTimePickerWindow.xib */, 7C47352828571CC900ECD293 /* AccessPhoneContactHintWindow.swift */, 7C47352A28571D0300ECD293 /* AccessPhoneContactHintWindow.xib */, + 7CE4BA1E283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift */, + 7CE4BA1D283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib */, ); path = Windows; sourceTree = ""; @@ -3039,6 +3054,8 @@ DF53BB6E202362E5002BF028 /* AuthorizationScopeCell.xib */, 7C53049828FE753400567CF6 /* AuthorizationScopeGroupCell.swift */, 7C53049928FE753400567CF6 /* AuthorizationScopeGroupCell.xib */, + 7CE4BA22283CD297001C87D5 /* MediaPreviewCell.swift */, + 7CE4BA21283CD297001C87D5 /* MediaPreviewCell.xib */, ); path = Cells; sourceTree = ""; @@ -3610,6 +3627,7 @@ 7B04142A240FA09F00BE8D73 /* LocationCell.swift */, 7BA1768D244ACE2E007D50FD /* PickerCell.swift */, 7B3CDA6324FFDD1D003A3E80 /* FavoriteStickerCell.swift */, + 7CE4BA27283CD2C9001C87D5 /* SelectedMediaCell.swift */, ); path = Cells; sourceTree = ""; @@ -3997,6 +4015,7 @@ 7B600D282453186B001D8146 /* DesktopTableHeaderView.xib in Resources */, 7BB7872B24514ACC0057B4ED /* PhoneContactsSettingTableHeaderView.xib in Resources */, 7B7B5DB5230EBEBA00D0F463 /* TransferTypeCell.xib in Resources */, + 7CE4BA23283CD297001C87D5 /* MediaPreviewCell.xib in Resources */, E0BEB85D236C1C49001FE534 /* ProfileMenuItemView.xib in Resources */, 7BACA8D523602BF8007E3381 /* RecorderLongPressHintView.xib in Resources */, 94C6AA0B280D36940011AB02 /* AttachmentDiagnosticView.xib in Resources */, @@ -4039,6 +4058,7 @@ 7B81BF2922893F8B00266A77 /* GroupParticipantCell.xib in Resources */, 7B36920A233B3650007321A7 /* MediaTypeOverlayView.xib in Resources */, 944ED8D0264640E200C97215 /* WebLoadingFailureView.xib in Resources */, + 7CE4BA1F283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib in Resources */, 7BB788B2216C5A4A00EDE7B4 /* LoadingIndicatorFooterView.xib in Resources */, DFDD89E922C4B8E600128991 /* DepositChooseNetworkWindow.xib in Resources */, DFA5B5911FB04C9C00549728 /* Wallet.storyboard in Resources */, @@ -4426,6 +4446,7 @@ files = ( 7BF49DD320C3DBAC00A8510E /* CaptchaManager.swift in Sources */, DF8CECE11FC3054700E40064 /* TransferTypeCell.swift in Sources */, + 7CE4BA26283CD2B4001C87D5 /* SelectedPhotoInputItemsViewController.swift in Sources */, 7BCB8C8422BB56B8002A13CC /* DataAndStorageSettingsViewController.swift in Sources */, 9BB351671FB19ECB00EDDD2C /* ConversationDateHeaderView.swift in Sources */, DF2819752014669E001EE5FA /* RefreshAccountJob.swift in Sources */, @@ -4819,6 +4840,7 @@ 7B51DDB2223A408A008ACDBB /* LoginMobileNumberViewController.swift in Sources */, 7B21782122C4E70B00C08106 /* OggOpusPlayer.swift in Sources */, 7B3CDA6424FFDD1D003A3E80 /* FavoriteStickerCell.swift in Sources */, + 7CE4BA20283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift in Sources */, 7BC3559F2265B7C30073C7BF /* DragDownIndicator.swift in Sources */, 7BA398CD242B539900DB5154 /* UIColor+Assets.swift in Sources */, 7B8BB588234F160C00991ACB /* SharedMediaCategorizer.swift in Sources */, @@ -4890,6 +4912,7 @@ 7B9D825A22F1BFEA0099381E /* NormalNetworkOperationIconSet.swift in Sources */, 7B6E5503223F69D90060E6FC /* KeyboardBasedLayoutViewController.swift in Sources */, 9B748DCD1FA71CEF00BC009B /* CameraViewController.swift in Sources */, + 7CE4BA24283CD297001C87D5 /* MediaPreviewCell.swift in Sources */, 7B54F95B22B24A5600908A9D /* CreateEmergencyContactVerificationCodeViewController.swift in Sources */, 7BB0F9512434DDD400BEDA97 /* CircleMemberSearchResult.swift in Sources */, 7B81D91423E93ECA0031E945 /* QuotedMessageView.swift in Sources */, @@ -4968,6 +4991,7 @@ 7BD7534C2182CD7A00BAC172 /* CallMessageViewModel.swift in Sources */, 7B2D174F22B11A8600AE3DD8 /* LoginInfoInputViewController.swift in Sources */, DF1ED8D920BBECFF003E10E8 /* AlbumViewController.swift in Sources */, + 7CE4BA28283CD2C9001C87D5 /* SelectedMediaCell.swift in Sources */, 7BEE5351222D0E5C008D3911 /* ConversationExtensionCell.swift in Sources */, DF8CECF21FC4256D00E40064 /* BlockUserCell.swift in Sources */, 7CC730502745F95D002780F5 /* StickerStore.swift in Sources */, diff --git a/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/Contents.json b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/Contents.json new file mode 100644 index 0000000000..89f4a91024 --- /dev/null +++ b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_video_bold@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_video_bold@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@2x.png b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d40d895c1e0def0f17f0af0ece9fa385c876d4a7 GIT binary patch literal 482 zcmV<80UiE{P)JsB7zadbNN3W3`58$qYy}M_NfSPvNzDx_5S{@O{YWUoOgai7zEV2v>>u+fC6q&w zI#`SGN0%$9flQ!w*Q+<@5NUfLb)-ww)dGGgAv35G#xJ~guNY1yr_f-*fpQaAoZ9U( z=@a9Ow!d%(atEeh67T@lZkAo&4xXIa4Nxidjdc98WkoT7CU8{%H;dO-kpYJ#?dt+LTVUorNqDsYiW#dyb>TQurv5kv-`h zbeSc_$IX*~|7EL4ICDmQ+q`e__EN9z%BRp^e#0I!Rwhta9eQuhG168Us!A#y-YKhG zuskg!;$|V`GHM4TlwqdCvQO;u6U3$_`2zQYCl|AsdmB&(&xrXMP`hsSguyTj!~9Ra Y0iLWFu-d2-)Bpeg07*qoM6N<$f-n@q00000 literal 0 HcmV?d00001 diff --git a/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@3x.png b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3179d4d2fb5f3160b4e20edba21350d05b117ae9 GIT binary patch literal 714 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ79h;%I?XTvD9BhG z869*1Fk@Na}yv+ME zW*qz03H(>xIjy*+;p@+Pf)#hO9Da5EHhcAEuNke%`zM-R0G(r*e;d zjtg3%$G&CBt*nJ@R=-8LC5t`c=Nx6>@MvhjP0Z!>2>rg3&1=_n(Vxp=cdZUI51V%T z;>FLs?xi#J?Za`OuxzqQ`EsG>xzB!TTJCKf011o=v>1 z9_l)8;T_SZPxlphbx!3bIH; zCpY*^{iVHCf63p)9IC1*d-;scTki?D^YyT)?#e0_#h1lWQ68)<>-JrL+Zi2k@N4Rn j5LV;dpzq9(1o871bDe*UvQhlO0+4{GtDnm{r-UW|eBeOL literal 0 HcmV?d00001 diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/Contents.json b/Mixin/Assets.xcassets/ic_media_close.imageset/Contents.json new file mode 100644 index 0000000000..2803457d58 --- /dev/null +++ b/Mixin/Assets.xcassets/ic_media_close.imageset/Contents.json @@ -0,0 +1,54 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_media_close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_media_close_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_media_close@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_media_close_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@2x.png b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f59d49cfa6e8719d876a6a93d718b3b74d3f5e55 GIT binary patch literal 1168 zcmV;B1aJF^P)p%-Xx0Gt3g0dNB733`Om696ZG{_9Xm!bbv$ow)lt z79-0_mlMmfPUyA8P#%{Z zF70r3U2r+#(xki*KN}*fZRD2`!^bIfP{v_A>BXpsN3s517%TV zp-fBlDmjj$t3Knh!Jr+=2zW~4ohjSs_O440-5$-OT6KXzYdVi&r*Ei6o6>OmDjdA_ z>*{81IOw7K7`25q6}95N6S5SzFZ?)?ZN4AS_06rW!MTK~URhqELcvw5w%L;hDZqH! zf9KK^Hb7lncXEf5A>XZd>bk@_Tovn4p5k2@4r}f;;UM7v^d$)!bVvMQ#V}2NT*z;A zZXa@4b6JFq<1Hgs{2H_-&u)tl%o>*^lC@B>S zltqZGiZ=1X4aZqYQ&>l%ka~PVnDq9I7o8GNyM0GpWfv?bb=qkgYaHkuKf=Z zR;2yWE~YsDPk0d{nwIP3CG??zI|f|DCM>ZRk@i2Y#ek>i`ns(DE8uBvH^#dm%Uf0=nhFFN}kzp{5Yy!w}9lnBdsaz^hnNJ8?pO@hShtsa#jKY0Q zeA$?{Zn$hw9>UE7m*X%Wv7@8wWRLQe1bQ(e&?xrU?yO)Iy-?x!3qK0Jwl?D;{~H2F iCo(0ZbLTJpH^g69;kD>+Ni@;`0000;0s zWuXBv2t@n-Jz!|+Ru41{`p>ySbX7rrUAKl?RitQ6GEK9@FEzHW*d98DYgJFyhGXY! zZ`kT=C#h8GoR(dHWZPpqGH2Hj+b&y`mQ8$9H8UPa9ej-ztpq^5ddv1BB`yBaYQmsi z9hgbEx@otornz{bwtuFON(Iem+P&@`+pi(YP^Z(SoBn`qZ*QrO*CDPCM|rZDG-Waw zGK?&nLAh1KdLKeBxJQ2)G;<(e2lj~Up~WvRUA5ZO>)lZArcY6{!tjMcJ`DWdY(Fu0 z!!M&RuxYmyz*c?oS)f;~&O{B)s{C4xikpQX;P2SB{h+nLL18biu44!FYICDtnU4Vd zoo>?N2Mkvc-5Bli^z6cFn@I?@d~d7dSKGMfZ3#Lpm15Wzq=CI|cj>?Xn~A|fc);&J zoKdIS(`EaX?U|IQtYL=zg@r zaA{6Nw_a71{P+Dw-3KpJn5f+QvLYoPv0HDuZ$>rjJc+8R1YBoXVIfRiHBD`zmd9Os zPn8>>%hYZqwXWONY)>J0@w4$aZqNKqmHVuDNo@`Q?t(PEHl9QUlM%OucT~AhyHR1& zsgyMjUR^N)mqFfKZqa z>|upKUZs3~O$n`nb+xv(LScm%TySgX5rAu|l&J0ux02+A*1X%^k|Mc|SMJcwZ`85TMYSV*(fRE;*mhfq-H zxL_e&$2oUkTd4p!OiDr`aFjt`CBLD^l{8MrEl@R737K@@O$kv!VO_-qty$G<09d)4 znayTt7KMd%6$i8?_^^QAC<|pn?e9#eR9x4>qJz?PWV1p`(_7XBfKOB@D@L>$Mu@l0 z`$Cf3-q^4A26ULva$Ev1dc_m54I?g%qKw8fyhD#4bkvS>byW;Hj`4|gRZ5yWttd~p zE9?qLZ(-DlM;nN-y695O)3EGjlH8T92Vr}R0pf; z?8l@C6VVo<>SK}9@4e{4x@2^hj4sD1lPxVassFUG0iuNUm zDZr73=c*%2V$kaxY7tM};WPj?$me-fNhP$|VsS$kjRE#0!#+_Z>VPpBIVAg3j8a(W zJ3VdkYckM?;40jCX7$IGUNh|XQlj5EEkL6^-`C}vce_TduJis>z=LVp(|AFT9-^kT z&;w`Vyxop(FWCi!;eQ{!WqUl+(L+}b^k1ZLZ&v`v4qofJ6M4G^VJU)ufS_ozIDPjx z$nPXVt<*Uk*>2EsEy|_3rs9MjE)}R( zrtRvxGr$i$kb9;*7l=)>@23cIZWQN7YHSba^Uv_Ao~#YBcRx-X`-km#UcK%o9T*;L T5x`w%00000NkvXXu0mjfJ)k3L literal 0 HcmV?d00001 diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@2x.png b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e80af0b68712c7ae175d987a1bcd88c91a8d8d6a GIT binary patch literal 1475 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 literal 0 HcmV?d00001 diff --git a/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@3x.png b/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9f774b1b685f336d127a80b47459f3072f975ae2 GIT binary patch literal 1949 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 Z@EaD6!D)GwKgs|A002ovPDHLkV1f{RV;=wj literal 0 HcmV?d00001 diff --git a/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@3x.png b/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d3fd566e73c9808bf7a5c13dcaa15be2f8fb3546 GIT binary patch literal 1874 zcmV-Y2d(&tP)^TMRDkOw^xr16-RRp*7zrt~IteI5?P} znVA_01OoGvMeuj}tE;PPd_LbVX`1$P>1Lu1=o+{dL86~W$VO|REo*CQQ!OnmQ@XAn z(Jg|1<5gEzSM!S(FRGqCed@DmW^k!xk_^fa851PB)E{JHYc@AGr<$6YMsAxJ875IF zGzuCN%(Tm8L=ck$L)@>bLsuFd9qsGv?EIJR5Fo0ruWvYoHo-3SlS4Qr2riW0LaI7w zO?i2FAKfFYtgJM@fB(MOraI)*%RO?^NN7$-mc!OePELNiZ{z^%?MCq5&d$!Fis}$Q zQ=60TNvlEpMZZ$q<|F_=+|kkTEmL=$YozINwdv+|DG3tZYJnt!oG;{90RA)nJM5-ZrBVD@l~8Jso!x=a z%l3#k*9eEhRgH~}!-^`UJ+dQcizMw?U0of4XCN0~V`HP^&6_v%lGq?cS4-+*V!WeS zW$0RNAqT*)1Or=1tdU|~nLv>6S^>Ec`YyQ)81*Z}t-?87A)$+@M$6mV+kd}){W?T0 zgiTk&%s*sa$XzU8y+GIM>FH4!G*S zBnCMF^TMh@jq(?`S$j-i%}ODzGe2C~AYq)htS#JZdt~hI?+=%jmR3pPGPrzGgZ%PM z3kwT6c?4JzR)_<5F*OLwr+%{rhU>|bC$;1uoS&aZq(Feq8dR&)AXd4&o=#i`J3BkG zlIRn>*(0&WoPg!!~Q0D=sw0GVCnQ9OJ0EGcG|q(MbRMaAS%a8OZx z_RHmtC99;cu+Xy_q z8MF%VqNza%;ntTgUsjOE@ZrOU8d-)|{UZ^JUbI&De7+j;7|P1Z$0_0pP64NK~ViFXVZ>p=SvrB+<9gvW&5bJokBuI2K-CBz{k>Br6m+qhUfX7zk zR_kV3o^>QWBj3zn=X!EEkbJHXPnsXB1cgcRW)&3`^={T8Xb-k9NMeJvCv09^5hS|V zfVCwI*Vx#YYi$v(B0B(hYF_K$LeUuq1b%(yDWz$O;1mE-`D*cFvzf*He1LYZOzPN zOJ;~{cEzl$tc>u2)4kfNfME$duT6Eg%B8E|IB+PwQ0WTcHZkzJ4_ywUeq^ntAgA3a zIly(cg=4D}PFhfI3rG4e@~d-gz%i-DyfFnaFvwprJznQl4iEw7E zM0*_&V!~Y4>c6hm+!|n;K$JM_DA(8*zwPM!qB}<{e#TfNcKe9L;>UsI2Qe~YajnWG zS^K#pC2KawDPr-h56JGcR_NdN!< M07*qoM6N<$g2Z)au>b%7 literal 0 HcmV?d00001 diff --git a/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/Contents.json b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/Contents.json new file mode 100644 index 0000000000..cfe7a5145c --- /dev/null +++ b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_photo_unselected_narrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_photo_unselected_narrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@2x.png b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bd6e1a4d2ae45ec5d9cd69803bd4d5134f89a8ab GIT binary patch literal 1101 zcmV-T1hV^yP)@5H>N5HF1%2HsYXD(@5h~ z?VwRB7?aYZ7>z>-OqwV?AKYi2h?J-Nye~QL9v<)A`*F^__uO-k=#j`~vkq?TEH6ac zECfigB!uj-L?uazQnkUPnpwOof3xcGi)ER`q-IR1oQFV+8!yX@eDCV&DtU2n@im!D z#v_r)_QAnHdVPI8gYSlhhV+AjgT~IzPNUgu?rmvldD+<5XjF6=X7RIC6ZBLtxoM@v z&d$yvD=RBw{FPOWEG{lO&(F^z1r~MDQ@|a}$-Sl1>F;j0`;}T^g@Qf`h|ZPbAU!|~ zE{(7_Zfp!_ZnDfZkwg2LoIc+uNGBrIF)Q-sW&w>y(SId^78VFh}{FQpaVA%_(>yB zaIu3Rc5&ix_JcHn)6-KqI5nA&mf;pip-~5cvQhH6)Ed!6m8_8Gj0~kJHIMsxxr97I8QMZt^xWk4t}l z|1i}VycR}<8pTB>n9Bj$+S-OWZ#7b#fU(6{5b)!oJo1C&bS9J8HW&;pssj{<)iAsF z9quV3iH{|PoB#<7)foHx`)>ug1PSW_w**BNmHWbCu}opIQjGxD+HAHd@$pVk=}s&b z`RIH@LxX;NeEefA<|D@N`~7lGJkK1$cQ_S$iRUBoV~1Q_s8#|GFToIETiIiB_QGdM z6^}sDetmtNiN#_Il}_X_=ipXhxhRXXSn;SNu(FkP!Ah0iP|8!3-$L{hpk5=2jkiQm zj8sbuMbCn|Ac~?h{~N#D60nK#+t8SNPc8w+-qqAUwOK-NL|oVthdi5EoqHv|MHrEh2-`!~ct61|q^ T>4f)500000NkvXXu0mjf5{vU& literal 0 HcmV?d00001 diff --git a/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@3x.png b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2eed5be70a5fcb14ace7b5dd6cf84cbea1de2c96 GIT binary patch literal 1572 zcmV+<2HW|GP)y(18IN* zQUGg&46uzuS_(r3 zL9XB5-|sCdDta!xHHj|XpX~P}MVOHdKxBH+adUGMT3lRw-PP3nqD3%%9n^su{5BIH#wly)KvQrBJM*v?U@Pm#`7p!^6X=s;a6f z$N}KE;jOH!^a@2PN<+LwOCH1r4gU&PR##UCbA>E{+7b)~UkgPF$};PMhA<_JANm=D zK1*arX{S;tiGX3v*CV*>$&8{W-xGXTA)nI_3jat`z;MnrKP14 zU;&(*oD2v98TI1^c@Q&l>0w}GY3iZT7%T#6E47)a)Q@N6Ix&RNx-_Z(z;&O`*Zo*i z&qF|MrL}=jqyzoI{R+K1?(j?vN4Kfop! z85#LgP<6L%1LNSqWRT+cP^ziVJKoSvSFgr5=N&$xDV zb#)Ck!}|KVNVuK|D_mP!TML2BK(TN^)dww&YdBAR0X76u{FlE@6ymWRJ`uJ%h_K2P z3UY(ZAaPQOa5RQnQBmOpn}JRy1yv`!#kHoUraG`0=onT|uZZv_*GfuCY)6>RoCS3= zrVrC$G}sIhrvMQSaE*2qbWjC00dbMC2{!ZC!Bc>j3=Iu^4K{(qh2mP5d>;Obm)6zQ zJqMdW;-W`#IN|6Sj9uKC=3mL3^>B}2nAQr1XNiEJ<}o2XA~sCR;O8%J0rJwyRwnC8 zxgb$)@=;6F%E!mYzaa5(uplJss%YKM`Frx_A^J;{mzRG(H#gS;7K4FxV={b<&WEE` zrlZHs&d%?wa)L3SqZB5?007I4c*FJT>T2)G%1X{INT_YpMxp2pa+o!$YnrFU zj$o)iN8{F%@Y&khnp2rPMAq$gj|oLvNVeXrexk%QB2E2S;br^#`%^VFH94^;AvcyR zt3rZoytpwfwy|gfVGBtQz*z6A$6T1uvuS|(Sh9pu+w*Z2B)09O?FLJ_nra^0i zj*gC>q~ubH+IW(=Cy`Xtl72Om1`q}^df@o@`0r8NLFrtUd4w zK&qEKXl|z55XIYjJX)AA*!zIv!X Void)? + + private var requestId: PHImageRequestID? + private lazy var imageRequestOptions: PHImageRequestOptions = { + let options = PHImageRequestOptions() + options.version = .current + options.deliveryMode = .opportunistic + options.isNetworkAccessAllowed = true + return options + }() + + override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + if let id = requestId { + PHCachingImageManager.default().cancelImageRequest(id) + } + } + + func load(asset: PHAsset, size: CGSize) { + if asset.mediaType == .video { + mediaTypeView.style = .video(duration: asset.duration) + } else { + if let uti = asset.uniformTypeIdentifier, UTTypeConformsTo(uti as CFString, kUTTypeGIF) { + mediaTypeView.style = .gif + } else { + mediaTypeView.style = .hidden + } + } + requestId = PHImageManager.default().requestImage(for: asset, targetSize: size * UIScreen.main.scale, contentMode: .aspectFill, options: imageRequestOptions) { [weak self] (image, info) in + self?.imageView.image = image + } + } + + @IBAction func closeAction(_ sender: Any) { + deselectAsset?() + } + +} diff --git a/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift b/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift index 47ef23d7f0..a23f09d489 100644 --- a/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift @@ -385,9 +385,11 @@ class ConversationInputViewController: UIViewController { self?.quote = nil } - let recognizer = InteractiveResizeGestureRecognizer(target: self, action: #selector(interactiveResizeAction(_:))) - recognizer.delegate = self - view.addGestureRecognizer(recognizer) + if ScreenHeight.current > .short { + let recognizer = InteractiveResizeGestureRecognizer(target: self, action: #selector(interactiveResizeAction(_:))) + recognizer.delegate = self + view.addGestureRecognizer(recognizer) + } } func update(opponentUser user: UserItem) { @@ -407,6 +409,7 @@ class ConversationInputViewController: UIViewController { if minimize { setPreferredContentHeightAnimated(.minimized) } + photoViewController.dismissSelectedPhotoInputItemsViewControllerIfNeeded() UIView.animate(withDuration: 0.5, delay: 0, options: .overdampedCurve) { self.customInputContainerView.alpha = 0 } completion: { _ in diff --git a/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift b/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift index 60a92354b8..5da949e333 100644 --- a/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift @@ -3,11 +3,20 @@ import Photos import MobileCoreServices import MixinServices +protocol PhotoInputGridViewControllerDelegate: AnyObject { + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didSelect asset: PHAsset) + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didDeselect asset: PHAsset) + func photoInputGridViewControllerDidTapCamera(_ controller: PhotoInputGridViewController) +} + class PhotoInputGridViewController: UIViewController, ConversationAccessible, ConversationInputAccessible { @IBOutlet weak var collectionView: UICollectionView! @IBOutlet weak var collectionViewLayout: UICollectionViewFlowLayout! + weak var delegate: PhotoInputGridViewControllerDelegate? + weak var photoInputViewController: PhotoInputViewController? + var fetchResult: PHFetchResult? { didSet { guard isViewLoaded else { @@ -20,10 +29,14 @@ class PhotoInputGridViewController: UIViewController, ConversationAccessible, Co var firstCellIsCamera = true + private let maxSelectedCount = 99 private let interitemSpacing: CGFloat = 0 private let columnCount: CGFloat = 3 private let imageManager = PHCachingImageManager() + private var selectedAssets: [PHAsset] { + photoInputViewController?.selectedAssets ?? [] + } private lazy var imageRequestOptions: PHImageRequestOptions = { let options = PHImageRequestOptions() options.version = .current @@ -71,6 +84,18 @@ class PhotoInputGridViewController: UIViewController, ConversationAccessible, Co } +extension PhotoInputGridViewController { + + func updateVisibleCellBadge() { + for indexPath in collectionView.indexPathsForVisibleItems { + if let asset = asset(at: indexPath), let cell = collectionView.cellForItem(at: indexPath) as? PhotoInputGridCell { + cell.updateBadge(with: selectedAssets.firstIndex(of: asset)) + } + } + } + +} + extension PhotoInputGridViewController: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { @@ -89,10 +114,15 @@ extension PhotoInputGridViewController: UICollectionViewDataSource { cell.imageView.image = R.image.conversation.ic_camera() cell.imageView.backgroundColor = R.color.camera_background() cell.mediaTypeView.style = .hidden + cell.indexLabel.isHidden = true + cell.statusImageView.isHidden = true } else if let asset = asset(at: indexPath) { cell.identifier = asset.localIdentifier cell.imageView.contentMode = .scaleAspectFill cell.imageView.backgroundColor = .background + cell.indexLabel.isHidden = false + cell.statusImageView.isHidden = false + cell.updateBadge(with: selectedAssets.firstIndex(of: asset)) imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: imageRequestOptions) { [weak cell] (image, _) in guard let cell = cell, cell.identifier == asset.localIdentifier else { return @@ -125,13 +155,15 @@ extension PhotoInputGridViewController: UICollectionViewDelegate { if firstCellIsCamera && indexPath.item == 0 { UIApplication.homeContainerViewController?.pipController?.pauseAction(self) conversationViewController?.imagePickerController.presentCamera() + delegate?.photoInputGridViewControllerDidTapCamera(self) } else if let asset = asset(at: indexPath) { - let vc = R.storyboard.chat.media_preview()! - vc.load(asset: asset) - vc.conversationInputViewController = conversationInputViewController - vc.transitioningDelegate = PopupPresentationManager.shared - vc.modalPresentationStyle = .custom - present(vc, animated: true, completion: nil) + if selectedAssets.contains(asset) { + delegate?.photoInputGridViewController(self, didDeselect: asset) + updateVisibleCellBadge() + } else if selectedAssets.count < maxSelectedCount { + delegate?.photoInputGridViewController(self, didSelect: asset) + updateVisibleCellBadge() + } } } diff --git a/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift b/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift index 6bb9168f9c..6c10f0bee6 100644 --- a/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift @@ -21,11 +21,19 @@ class PhotoInputViewController: UIViewController, ConversationInputAccessible { } } + private weak var selectedPhotoInputItemsViewControllerIfLoaded: SelectedPhotoInputItemsViewController? + private lazy var selectedPhotoInputItemsViewController: SelectedPhotoInputItemsViewController = { + let controller = R.storyboard.chat.selected_photo_input_items()! + controller.delegate = self + selectedPhotoInputItemsViewControllerIfLoaded = controller + return controller + }() private var allPhotos: PHFetchResult? private var smartAlbums: PHFetchResult? private var sortedSmartAlbums: [PHAssetCollection]? private var userCollections: PHFetchResult? private var gridViewController: PhotoInputGridViewController! + private(set) var selectedAssets: [PHAsset] = [] deinit { PHPhotoLibrary.shared().unregisterChangeObserver(self) @@ -70,6 +78,8 @@ class PhotoInputViewController: UIViewController, ConversationInputAccessible { if let vc = segue.destination as? PhotoInputGridViewController { vc.fetchResult = allPhotos gridViewController = vc + gridViewController.delegate = self + gridViewController.photoInputViewController = self } } @@ -182,6 +192,7 @@ extension PhotoInputViewController: PHPhotoLibraryChangeObserver { func photoLibraryDidChange(_ changeInstance: PHChange) { DispatchQueue.main.sync { + self.dismissSelectedPhotoInputItemsViewControllerIfNeeded() if let allPhotos = self.allPhotos, let changeDetails = changeInstance.changeDetails(for: allPhotos) { self.allPhotos = changeDetails.fetchResultAfterChanges } @@ -220,6 +231,233 @@ extension PhotoInputViewController: PHPickerViewControllerDelegate { } +extension PhotoInputViewController: PhotoInputGridViewControllerDelegate { + + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didSelect asset: PHAsset) { + guard !selectedAssets.contains(asset) else { + return + } + if selectedAssets.isEmpty { + presentSelectedPhotoInputItemsViewControllerAnimated() + } + selectedAssets.append(asset) + selectedPhotoInputItemsViewController.add(asset) + } + + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didDeselect asset: PHAsset) { + guard let index = selectedAssets.firstIndex(of: asset) else { + return + } + selectedAssets.remove(at: index) + if selectedAssets.isEmpty { + dismissSelectedPhotoInputItemsViewControllerAnimated() + } else { + selectedPhotoInputItemsViewController.remove(asset) + } + } + + func photoInputGridViewControllerDidTapCamera(_ controller: PhotoInputGridViewController) { + dismissSelectedPhotoInputItemsViewControllerIfNeeded() + } + +} + +extension PhotoInputViewController: SelectedPhotoInputItemsViewControllerDelegate { + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSend assets: [PHAsset]) { + sendItems(assets: assets) + } + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didCancelSend assets: [PHAsset]) { + conversationInputViewController?.dismiss() + } + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didDeselect asset: PHAsset) { + guard let index = selectedAssets.firstIndex(of: asset) else { + return + } + selectedAssets.remove(at: index) + gridViewController.updateVisibleCellBadge() + if selectedAssets.isEmpty { + dismissSelectedPhotoInputItemsViewControllerAnimated() + } + } + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSelectAssetAt index: Int) { + conversationInputViewController?.setPreferredContentHeightAnimated(.regular) + let window = SelectedPhotoInputItemsPreviewWindow.instance() + window.load(assets: selectedAssets, initIndex: index) + window.delegate = self + window.presentPopupControllerAnimated() + } + +} + +extension PhotoInputViewController: SelectedPhotoInputItemsPreviewWindowDelegate { + + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, willDismissWindow assets: [PHAsset]) { + if assets.isEmpty { + selectedAssets.removeAll() + dismissSelectedPhotoInputItemsViewControllerAnimated() + } else { + selectedAssets = assets + selectedPhotoInputItemsViewController.updateAssets(assets) + } + gridViewController.updateVisibleCellBadge() + } + + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendItems assets: [PHAsset]) { + sendItems(assets: assets) + } + + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendFiles assets: [PHAsset]) { + sendAsFiles(assets: assets) + } + +} + +extension PhotoInputViewController { + + private func sendItems(assets: [PHAsset]) { + guard let controller = conversationInputViewController else { + return + } + assets.forEach(controller.send(asset:)) + selectedAssets.removeAll() + gridViewController.updateVisibleCellBadge() + dismissSelectedPhotoInputItemsViewControllerAnimated() + } + + private func sendAsFiles(assets: [PHAsset]) { + guard let controller = conversationInputViewController else { + return + } + let hud = Hud() + hud.show(style: .busy, text: "", on: AppDelegate.current.mainWindow) + requestURLs(for: assets) { [weak self] urls in + hud.hide() + guard let self = self else { + return + } + urls.forEach(controller.sendFile(url:)) + self.selectedAssets.removeAll() + self.gridViewController.updateVisibleCellBadge() + self.dismissSelectedPhotoInputItemsViewControllerAnimated() + } + } + + func dismissSelectedPhotoInputItemsViewControllerIfNeeded() { + guard let controller = selectedPhotoInputItemsViewControllerIfLoaded, controller.parent != nil else { + return + } + selectedAssets.removeAll() + gridViewController.updateVisibleCellBadge() + gridViewController.view.isUserInteractionEnabled = false + controller.removeAllAssets() + controller.view.removeFromSuperview() + controller.removeFromParent() + controller.view.snp.removeConstraints() + gridViewController.view.isUserInteractionEnabled = true + } + + private func presentSelectedPhotoInputItemsViewControllerAnimated() { + guard + selectedPhotoInputItemsViewController.parent == nil, + let conversationInputViewController = conversationInputViewController, + let inputBarView = conversationInputViewController.inputBarView, + let conversationViewController = conversationInputViewController.parent + else { + return + } + gridViewController.view.isUserInteractionEnabled = false + let controller = selectedPhotoInputItemsViewController + let viewHeight = selectedPhotoInputItemsViewController.viewHeight + addChild(controller) + view.insertSubview(controller.view, at: 0) + controller.view.snp.makeConstraints({ (make) in + make.left.right.equalToSuperview() + make.top.equalTo(inputBarView.snp.bottom).offset(0) + }) + view.layoutIfNeeded() + controller.view.snp.updateConstraints { make in + make.top.equalTo(inputBarView.snp.bottom).offset(-viewHeight) + } + UIView.animate(withDuration: 0.3, delay: 0, options: .overdampedCurve) { + self.view.layoutIfNeeded() + } completion: { _ in + conversationViewController.addChild(controller) + conversationViewController.view.addSubview(controller.view) + controller.view.snp.remakeConstraints({ (make) in + make.left.right.equalToSuperview() + make.top.equalTo(inputBarView.snp.bottom).offset(-viewHeight) + }) + self.gridViewController.view.isUserInteractionEnabled = true + } + } + + private func dismissSelectedPhotoInputItemsViewControllerAnimated() { + guard + selectedPhotoInputItemsViewController.parent != nil, + let conversationInputViewController = conversationInputViewController, + let inputBarView = conversationInputViewController.inputBarView + else { + return + } + gridViewController.view.isUserInteractionEnabled = false + let controller = selectedPhotoInputItemsViewController + let viewHeight = selectedPhotoInputItemsViewController.viewHeight + addChild(controller) + view.insertSubview(controller.view, at: 0) + controller.view.snp.remakeConstraints({ (make) in + make.left.right.equalToSuperview() + make.top.equalTo(inputBarView.snp.bottom).offset(-viewHeight) + }) + view.layoutIfNeeded() + controller.view.snp.updateConstraints { make in + make.top.equalTo(inputBarView.snp.bottom).offset(0) + } + UIView.animate(withDuration: 0.3, delay: 0, options: .overdampedCurve) { + self.view.layoutIfNeeded() + } completion: { _ in + controller.removeAllAssets() + controller.view.removeFromSuperview() + controller.removeFromParent() + controller.view.snp.removeConstraints() + self.gridViewController.view.isUserInteractionEnabled = true + } + } + + private func requestURLs(for assets: [PHAsset], completion: @escaping ((_ urls : [URL]) -> Void)) { + let group = DispatchGroup() + let queue = DispatchQueue(label: "one.mixin.messager.PhotoInputViewController.requestPHAssetsURLs", attributes: .concurrent) + var urls: [URL?] = Array(repeating: nil, count: assets.count) + for (index, asset) in assets.enumerated() { + group.enter() + queue.async(group: group) { + if asset.mediaType == .image { + let options = PHContentEditingInputRequestOptions() + options.canHandleAdjustmentData = { (adjustmeta: PHAdjustmentData) -> Bool in true } + asset.requestContentEditingInput(with: options, completionHandler: { (contentEditingInput, info) in + urls.insert(contentEditingInput?.fullSizeImageURL, at: index) + group.leave() + }) + } else if asset.mediaType == .video { + let options: PHVideoRequestOptions = PHVideoRequestOptions() + options.version = .original + PHImageManager.default().requestAVAsset(forVideo: asset, options: options, resultHandler: { (asset, audioMix, info) in + urls.insert((asset as? AVURLAsset)?.url, at: index) + group.leave() + }) + } + } + } + group.notify(queue: .main) { + completion(urls.compactMap { $0 }) + } + } + +} + fileprivate let collectionSubtypeOrder: [PHAssetCollectionSubtype: Int] = { var idx = -1 var autoIncrement: Int { diff --git a/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift b/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift new file mode 100644 index 0000000000..e88f62572a --- /dev/null +++ b/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift @@ -0,0 +1,134 @@ +import UIKit +import Photos + +protocol SelectedPhotoInputItemsViewControllerDelegate: AnyObject { + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSend assets: [PHAsset]) + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didDeselect asset: PHAsset) + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSelectAssetAt index: Int) + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didCancelSend assets: [PHAsset]) +} + +final class SelectedPhotoInputItemsViewController: UIViewController { + + let viewHeight: CGFloat = 224 + + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var sendButton: UIButton! + + weak var delegate: SelectedPhotoInputItemsViewControllerDelegate? + + private var cellSizeCache = [String: CGSize]() + private var assets = [PHAsset]() { + didSet { + sendButton.setTitle(R.string.localizable.send_count(assets.count), for: .normal) + } + } + + @IBAction func cancelAction(_ sender: Any) { + delegate?.selectedPhotoInputItemsViewController(self, didCancelSend: assets) + } + + @IBAction func sendAction(_ sender: Any) { + delegate?.selectedPhotoInputItemsViewController(self, didSend: assets) + } + +} + +extension SelectedPhotoInputItemsViewController { + + func add(_ asset: PHAsset) { + guard !assets.contains(asset) else { + return + } + assets.append(asset) + let index = IndexPath(item: assets.count - 1, section: 0) + collectionView.insertItems(at: [index]) + collectionView.scrollToItem(at: index, at: .centeredHorizontally, animated: true) + } + + func remove(_ asset: PHAsset) { + guard let index = assets.firstIndex(of: asset) else { + return + } + assets.remove(at: index) + cellSizeCache.removeValue(forKey: asset.localIdentifier) + collectionView.deleteItems(at: [IndexPath(item: index, section: 0)]) + } + + func removeAllAssets() { + cellSizeCache.removeAll() + assets.removeAll() + collectionView.reloadData() + } + + func updateAssets(_ selectedAssets: [PHAsset]) { + cellSizeCache.removeAll() + assets = selectedAssets + collectionView.reloadData() + } + +} + +extension SelectedPhotoInputItemsViewController: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + assets.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.selected_media, for: indexPath)! + if indexPath.item < assets.count { + let asset = assets[indexPath.item] + cell.load(asset: asset, size: cellSizeForItemAt(indexPath.item)) + cell.deselectAsset = { [weak self] in + guard let self = self else { + return + } + self.remove(asset) + self.delegate?.selectedPhotoInputItemsViewController(self, didDeselect: asset) + } + } + return cell + } + +} + +extension SelectedPhotoInputItemsViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + cellSizeForItemAt(indexPath.item) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + delegate?.selectedPhotoInputItemsViewController(self, didSelectAssetAt: indexPath.item) + } + +} + +extension SelectedPhotoInputItemsViewController { + + private func cellSizeForItemAt(_ index: Int) -> CGSize { + guard index < assets.count else { + return .zero + } + let asset = assets[index] + if let size = cellSizeCache[asset.localIdentifier] { + return size + } else { + let height: CGFloat = 160 + let width: CGFloat + let ratio = CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight) + if ratio > 1 { + width = ceil(height / 3 * 4) + } else if ratio < 1 { + width = ceil(height / 4 * 3) + } else { + width = height + } + let size = CGSize(width: width, height: height) + cellSizeCache[asset.localIdentifier] = size + return size + } + } + +} diff --git a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift index 4a27ecc559..8c27666ea5 100644 --- a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift +++ b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift @@ -13,6 +13,9 @@ class MediaTypeOverlayView: UIView, XibDesignable { @IBOutlet weak var gifFileTypeView: UILabel! @IBOutlet weak var videoTypeView: UIStackView! @IBOutlet weak var videoDurationLabel: UILabel! + @IBOutlet weak var videoImageView: UIImageView! + + @IBOutlet weak var typeViewBottomConstraint: NSLayoutConstraint! class var backgroundImage: UIImage? { return R.image.conversation.bg_photo_bottom_shadow() diff --git a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib index 368dd0924c..285f20ca37 100644 --- a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib +++ b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib @@ -1,9 +1,9 @@ - + - + @@ -11,7 +11,9 @@ + + @@ -24,7 +26,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2354,20 +2506,52 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -2387,14 +2571,17 @@ + + + - + @@ -2806,6 +2993,8 @@ + + @@ -2836,7 +3025,7 @@ - + diff --git a/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift new file mode 100644 index 0000000000..e984451e34 --- /dev/null +++ b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift @@ -0,0 +1,62 @@ +import UIKit +import Photos +import SDWebImage +import CoreServices +import MixinServices + +class MediaPreviewCell: UICollectionViewCell { + + static let cellSize = CGSize(width: 312, height: 312) + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var selectedStatusImageView: UIImageView! + @IBOutlet weak var mediaTypeView: MediaTypeOverlayView! + + private var requestId: PHImageRequestID? + private lazy var imageRequestOptions: PHImageRequestOptions = { + let options = PHImageRequestOptions() + options.version = .current + options.deliveryMode = .opportunistic + options.isNetworkAccessAllowed = true + return options + }() + + override func awakeFromNib() { + super.awakeFromNib() + selectedStatusImageView.isHidden = false + mediaTypeView.videoTypeView.spacing = 8 + mediaTypeView.typeViewBottomConstraint.constant = 8 + mediaTypeView.gifFileTypeView.font = .systemFont(ofSize: 16) + mediaTypeView.videoDurationLabel.font = .systemFont(ofSize: 16) + mediaTypeView.videoImageView.image = R.image.conversation.ic_video_bold() + } + + override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + if let id = requestId { + PHCachingImageManager.default().cancelImageRequest(id) + } + } + + func load(asset: PHAsset) { + if asset.mediaType == .video { + mediaTypeView.style = .video(duration: asset.duration) + } else { + if let uti = asset.uniformTypeIdentifier, UTTypeConformsTo(uti as CFString, kUTTypeGIF) { + mediaTypeView.style = .gif + } else { + mediaTypeView.style = .hidden + } + } + let targetSize = Self.cellSize * UIScreen.main.scale + requestId = PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: imageRequestOptions) { [weak self] (image, info) in + self?.imageView.image = image + } + } + + func updateSelectedStatus(isSelected: Bool) { + selectedStatusImageView.image = isSelected ? R.image.ic_photo_checkmark() : R.image.ic_photo_unselected() + } + +} diff --git a/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.xib b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.xib new file mode 100644 index 0000000000..31ea635d0e --- /dev/null +++ b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift new file mode 100644 index 0000000000..6151540c38 --- /dev/null +++ b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift @@ -0,0 +1,181 @@ +import UIKit +import Photos + +protocol SelectedPhotoInputItemsPreviewWindowDelegate: AnyObject { + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendItems assets: [PHAsset]) + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendFiles assets: [PHAsset]) + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, willDismissWindow assets: [PHAsset]) +} + +final class SelectedPhotoInputItemsPreviewWindow: BottomSheetView { + + @IBOutlet weak var label: UILabel! + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var sendPhotoButton: RoundedButton! + @IBOutlet weak var sendFileButton: UIButton! + @IBOutlet weak var flowLayout: SnapCenterFlowLayout! + + @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint! + + weak var delegate: SelectedPhotoInputItemsPreviewWindowDelegate? + + private var assets = [PHAsset]() + private var selectedAssets = [PHAsset]() + private var lastWidth: CGFloat = 0 + private var isSending = false + + override func awakeFromNib() { + super.awakeFromNib() + sendFileButton.setTitleColor(.theme, for: .normal) + sendFileButton.setTitleColor(R.color.button_background_disabled(), for: .disabled) + collectionView.decelerationRate = .fast + collectionView.isPagingEnabled = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.allowsMultipleSelection = true + collectionView.register(R.nib.mediaPreviewCell) + } + + override func layoutSubviews() { + super.layoutSubviews() + let width = bounds.width + if lastWidth != width { + lastWidth = width + let inset = (width - collectionViewHeightConstraint.constant) / 2 + flowLayout.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset) + } + } + + override func dismissPopupControllerAnimated() { + if !isSending { + delegate?.selectedPhotoInputItemsPreviewWindow(self, willDismissWindow: selectedAssets) + } + super.dismissPopupControllerAnimated() + } + + @IBAction func closeAction(_ sender: Any) { + isSending = false + dismissPopupControllerAnimated() + } + + @IBAction func sendPhotosAction(_ sender: Any) { + isSending = true + delegate?.selectedPhotoInputItemsPreviewWindow(self, didTapSendItems: selectedAssets) + dismissPopupControllerAnimated() + } + + @IBAction func sendAsFilesAction(_ sender: Any) { + isSending = true + delegate?.selectedPhotoInputItemsPreviewWindow(self, didTapSendFiles: selectedAssets) + dismissPopupControllerAnimated() + } + + func load(assets: [PHAsset], initIndex: Int) { + self.assets = assets + selectedAssets = assets + updateUI() + collectionView.reloadData() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.collectionView.scrollToItem(at: IndexPath(item: initIndex, section: 0), at: .centeredHorizontally, animated: false) + } + } + + class func instance() -> SelectedPhotoInputItemsPreviewWindow { + R.nib.selectedPhotoInputItemsPreviewWindow(owner: nil)! + } + +} + +extension SelectedPhotoInputItemsPreviewWindow: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + assets.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.media_preview, for: indexPath)! + if indexPath.item < assets.count { + let asset = assets[indexPath.item] + cell.load(asset: asset) + cell.updateSelectedStatus(isSelected: selectedAssets.contains(asset)) + } + return cell + } + +} + +extension SelectedPhotoInputItemsPreviewWindow: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + MediaPreviewCell.cellSize + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + guard let cell = collectionView.cellForItem(at: indexPath) as? MediaPreviewCell else { + return + } + let asset = assets[indexPath.item] + if let index = selectedAssets.firstIndex(of: asset) { + selectedAssets.remove(at: index) + cell.updateSelectedStatus(isSelected: false) + } else { + selectedAssets.append(asset) + cell.updateSelectedStatus(isSelected: true) + } + updateUI() + } + +} + +extension SelectedPhotoInputItemsPreviewWindow { + + private func updateUI() { + let title: String + let sendPhotoButtonTitle: String + let sendFileButtonTitle: String + let isEnabled: Bool + if selectedAssets.count == 0 { + title = R.string.localizable.no_items_selected() + sendPhotoButtonTitle = R.string.localizable.send_item_count(0) + sendFileButtonTitle = R.string.localizable.send_as_files() + isEnabled = false + } else if selectedAssets.count == 1 { + switch selectedAssets[0].mediaType { + case .image: + title = R.string.localizable.selected_photo() + sendPhotoButtonTitle = R.string.localizable.send_photo() + case .video: + title = R.string.localizable.selected_video() + sendPhotoButtonTitle = R.string.localizable.send_video() + default: + title = R.string.localizable.selected_item() + sendPhotoButtonTitle = R.string.localizable.send_item() + } + sendFileButtonTitle = R.string.localizable.send_as_file() + isEnabled = true + } else { + let count = selectedAssets.count + let isAllImages = selectedAssets.allSatisfy { $0.mediaType == .image } + let isAllVideos = selectedAssets.allSatisfy { $0.mediaType == .video } + if isAllImages { + title = R.string.localizable.selected_photo_count(count) + sendPhotoButtonTitle = R.string.localizable.send_photo_count(count) + } else if isAllVideos { + title = R.string.localizable.selected_video_count(count) + sendPhotoButtonTitle = R.string.localizable.send_video_count(count) + } else { + title = R.string.localizable.selected_item_count(count) + sendPhotoButtonTitle = R.string.localizable.send_item_count(count) + } + sendFileButtonTitle = R.string.localizable.send_as_files() + isEnabled = true + } + label.text = title + sendPhotoButton.setTitle(sendPhotoButtonTitle, for: .normal) + sendFileButton.setTitle(sendFileButtonTitle, for: .normal) + sendPhotoButton.isEnabled = isEnabled + sendFileButton.isEnabled = isEnabled + } + +} diff --git a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib new file mode 100644 index 0000000000..4c3c1819bf --- /dev/null +++ b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0d3e80a46227cb40137ef1b0c8174eb21f3d58f4 Mon Sep 17 00:00:00 2001 From: fanyu Date: Wed, 1 Jun 2022 11:16:50 +0800 Subject: [PATCH 2/4] Update cell size --- ...electedPhotoInputItemsViewController.swift | 12 ++--------- .../Windows/Cells/MediaPreviewCell.swift | 7 ++----- ...SelectedPhotoInputItemsPreviewWindow.swift | 21 +++++++++++++++++-- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift b/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift index e88f62572a..b6820b7826 100644 --- a/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift @@ -116,16 +116,8 @@ extension SelectedPhotoInputItemsViewController { return size } else { let height: CGFloat = 160 - let width: CGFloat - let ratio = CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight) - if ratio > 1 { - width = ceil(height / 3 * 4) - } else if ratio < 1 { - width = ceil(height / 4 * 3) - } else { - width = height - } - let size = CGSize(width: width, height: height) + let width: CGFloat = ceil(height / CGFloat(asset.pixelHeight) * CGFloat(asset.pixelWidth)) + let size = CGSize(width: min(160, max(width, 62)), height: height) cellSizeCache[asset.localIdentifier] = size return size } diff --git a/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift index e984451e34..bb7bd27ec8 100644 --- a/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift +++ b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift @@ -6,8 +6,6 @@ import MixinServices class MediaPreviewCell: UICollectionViewCell { - static let cellSize = CGSize(width: 312, height: 312) - @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var selectedStatusImageView: UIImageView! @IBOutlet weak var mediaTypeView: MediaTypeOverlayView! @@ -39,7 +37,7 @@ class MediaPreviewCell: UICollectionViewCell { } } - func load(asset: PHAsset) { + func load(asset: PHAsset, size: CGSize) { if asset.mediaType == .video { mediaTypeView.style = .video(duration: asset.duration) } else { @@ -49,8 +47,7 @@ class MediaPreviewCell: UICollectionViewCell { mediaTypeView.style = .hidden } } - let targetSize = Self.cellSize * UIScreen.main.scale - requestId = PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: imageRequestOptions) { [weak self] (image, info) in + requestId = PHImageManager.default().requestImage(for: asset, targetSize: size * UIScreen.main.scale, contentMode: .aspectFill, options: imageRequestOptions) { [weak self] (image, info) in self?.imageView.image = image } } diff --git a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift index 6151540c38..7059c2503f 100644 --- a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift +++ b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift @@ -23,6 +23,7 @@ final class SelectedPhotoInputItemsPreviewWindow: BottomSheetView { private var selectedAssets = [PHAsset]() private var lastWidth: CGFloat = 0 private var isSending = false + private var cellSizeCache = [String: CGSize]() override func awakeFromNib() { super.awakeFromNib() @@ -96,7 +97,7 @@ extension SelectedPhotoInputItemsPreviewWindow: UICollectionViewDataSource { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.media_preview, for: indexPath)! if indexPath.item < assets.count { let asset = assets[indexPath.item] - cell.load(asset: asset) + cell.load(asset: asset, size: cellSizeForItemAt(indexPath.item)) cell.updateSelectedStatus(isSelected: selectedAssets.contains(asset)) } return cell @@ -107,7 +108,7 @@ extension SelectedPhotoInputItemsPreviewWindow: UICollectionViewDataSource { extension SelectedPhotoInputItemsPreviewWindow: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - MediaPreviewCell.cellSize + cellSizeForItemAt(indexPath.item) } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { @@ -178,4 +179,20 @@ extension SelectedPhotoInputItemsPreviewWindow { sendFileButton.isEnabled = isEnabled } + private func cellSizeForItemAt(_ index: Int) -> CGSize { + guard index < assets.count else { + return .zero + } + let asset = assets[index] + if let size = cellSizeCache[asset.localIdentifier] { + return size + } else { + let height: CGFloat = 312 + let width: CGFloat = ceil(height / CGFloat(asset.pixelHeight) * CGFloat(asset.pixelWidth)) + let size = CGSize(width: min(312, max(width, 120)), height: height) + cellSizeCache[asset.localIdentifier] = size + return size + } + } + } From f1c76bce3a3335918164b16afb8bb631076ed0d1 Mon Sep 17 00:00:00 2001 From: fanyu Date: Wed, 1 Jun 2022 15:21:33 +0800 Subject: [PATCH 3/4] Update local title --- Mixin/UserInterface/Storyboard/Chat.storyboard | 4 ++-- .../Windows/SelectedPhotoInputItemsPreviewWindow.xib | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Mixin/UserInterface/Storyboard/Chat.storyboard b/Mixin/UserInterface/Storyboard/Chat.storyboard index 7b21ad4e73..f081987301 100644 --- a/Mixin/UserInterface/Storyboard/Chat.storyboard +++ b/Mixin/UserInterface/Storyboard/Chat.storyboard @@ -1553,7 +1553,7 @@ - + @@ -3025,7 +3025,7 @@ - + diff --git a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib index 4c3c1819bf..1d85a1ebb1 100644 --- a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib +++ b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib @@ -91,7 +91,6 @@ - From 8b863c6eadad5dcdd33720a14f2f6665dfede789 Mon Sep 17 00:00:00 2001 From: fanyu Date: Tue, 16 Aug 2022 10:37:24 +0800 Subject: [PATCH 4/4] Apply code style --- .../Windows/SelectedPhotoInputItemsPreviewWindow.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift index 7059c2503f..c84429516b 100644 --- a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift +++ b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift @@ -47,28 +47,28 @@ final class SelectedPhotoInputItemsPreviewWindow: BottomSheetView { } } - override func dismissPopupControllerAnimated() { + override func dismissPopupController(animated: Bool) { if !isSending { delegate?.selectedPhotoInputItemsPreviewWindow(self, willDismissWindow: selectedAssets) } - super.dismissPopupControllerAnimated() + super.dismissPopupController(animated: animated) } @IBAction func closeAction(_ sender: Any) { isSending = false - dismissPopupControllerAnimated() + dismissPopupController(animated: true) } @IBAction func sendPhotosAction(_ sender: Any) { isSending = true delegate?.selectedPhotoInputItemsPreviewWindow(self, didTapSendItems: selectedAssets) - dismissPopupControllerAnimated() + dismissPopupController(animated: true) } @IBAction func sendAsFilesAction(_ sender: Any) { isSending = true delegate?.selectedPhotoInputItemsPreviewWindow(self, didTapSendFiles: selectedAssets) - dismissPopupControllerAnimated() + dismissPopupController(animated: true) } func load(assets: [PHAsset], initIndex: Int) {