Add bio editing and photo setup in voice chats

This commit is contained in:
Ilya Laktyushin 2021-03-27 10:23:35 +05:00
parent 1fd84c653b
commit 2c404a8b5c
27 changed files with 1743 additions and 1573 deletions

View File

@ -6317,6 +6317,7 @@ Sorry for the inconvenience.";
"VoiceChat.EditBioText" = "Any details such as age, occupation or city.";
"VoiceChat.EditBioPlaceholder" = "Bio";
"VoiceChat.EditBioSave" = "Save";
"VoiceChat.EditBioSuccess" = "Your bio is saved.";
"VoiceChat.SendPublicLinkText" = "%1$@ isn't a member of \"%2$@\" yet. Send them a public invite link instead?";
"VoiceChat.SendPublicLinkSend" = "Send";

View File

@ -41,6 +41,7 @@ public final class OpenChatMessageParams {
public let actionInteraction: GalleryControllerActionInteraction?
public let playlistLocation: PeerMessagesPlaylistLocation?
public let gallerySource: GalleryControllerItemSource?
public let centralItemUpdated: ((MessageId) -> Void)?
public init(
context: AccountContext,
@ -65,7 +66,8 @@ public final class OpenChatMessageParams {
chatAvatarHiddenMedia: @escaping (Signal<MessageId?, NoError>, Media) -> Void,
actionInteraction: GalleryControllerActionInteraction? = nil,
playlistLocation: PeerMessagesPlaylistLocation? = nil,
gallerySource: GalleryControllerItemSource? = nil
gallerySource: GalleryControllerItemSource? = nil,
centralItemUpdated: ((MessageId) -> Void)? = nil
) {
self.context = context
self.chatLocation = chatLocation
@ -90,5 +92,6 @@ public final class OpenChatMessageParams {
self.actionInteraction = actionInteraction
self.playlistLocation = playlistLocation
self.gallerySource = gallerySource
self.centralItemUpdated = centralItemUpdated
}
}

View File

@ -384,6 +384,8 @@ public class GalleryController: ViewController, StandalonePresentableController
private var screenCaptureEventsDisposable: Disposable?
public var centralItemUpdated: ((MessageId) -> Void)?
public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
self.context = context
self.source = source
@ -1192,6 +1194,9 @@ public class GalleryController: ViewController, StandalonePresentableController
}
if strongSelf.didSetReady {
strongSelf._hiddenMedia.set(.single(hiddenItem))
if let hiddenItem = hiddenItem {
strongSelf.centralItemUpdated?(hiddenItem.0)
}
}
}
}

View File

@ -75,6 +75,7 @@ typedef enum {
- (TGNavigationBarPallete *)navigationBarPallete;
- (TGMenuSheetPallete *)menuSheetPallete;
- (TGMenuSheetPallete *)darkMenuSheetPallete;
- (TGMediaAssetsPallete *)mediaAssetsPallete;
- (TGCheckButtonPallete *)checkButtonPallete;

View File

@ -13,6 +13,8 @@ typedef void (^TGMediaAvatarPresentImpl)(id<LegacyComponentsContext>, void (^)(U
@interface TGMediaAvatarMenuMixin : NSObject
@property (nonatomic, assign) bool forceDark;
@property (nonatomic, copy) void (^didFinishWithImage)(UIImage *image);
@property (nonatomic, copy) void (^didFinishWithVideo)(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments);
@property (nonatomic, copy) void (^didFinishWithDelete)(void);

View File

@ -34,6 +34,8 @@
@property (nonatomic, assign) bool dismissesByOutsideTap;
@property (nonatomic, assign) bool hasSwipeGesture;
@property (nonatomic, assign) bool forceDark;
@property (nonatomic, assign) bool followsKeyboard;
@property (nonatomic, assign) bool ignoreNextDismissal;

View File

@ -73,7 +73,7 @@
- (TGMenuSheetController *)_presentAvatarMenu
{
__weak TGMediaAvatarMenuMixin *weakSelf = self;
TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false];
TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:self.forceDark];
controller.dismissesByOutsideTap = true;
controller.hasSwipeGesture = true;
controller.didDismiss = ^(bool manual)

View File

@ -95,7 +95,9 @@ typedef enum
_permittedArrowDirections = UIPopoverArrowDirectionDown;
_requiuresDimView = true;
if (!dark && [[LegacyComponentsGlobals provider] respondsToSelector:@selector(menuSheetPallete)])
if (dark && [[LegacyComponentsGlobals provider] respondsToSelector:@selector(darkMenuSheetPallete)])
self.pallete = [[LegacyComponentsGlobals provider] darkMenuSheetPallete];
else if (!dark && [[LegacyComponentsGlobals provider] respondsToSelector:@selector(menuSheetPallete)])
self.pallete = [[LegacyComponentsGlobals provider] menuSheetPallete];
self.wantsFullScreenLayout = true;

View File

@ -301,7 +301,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati
let presentationDisposable = context.sharedContext.presentationData.start(next: { [weak legacyController] presentationData in
if let legacyController = legacyController, let controller = legacyController.legacyController as? TGMenuSheetController {
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme)
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false)
}
})
legacyController.disposables.add(presentationDisposable)
@ -378,9 +378,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati
return controller
}
public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme) -> TGMenuSheetPallete {
let sheetTheme = theme.actionSheet
return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme, forceDark: Bool) -> TGMenuSheetPallete {
let sheetTheme: PresentationThemeActionSheet
if forceDark && !theme.overallDarkAppearance {
sheetTheme = defaultDarkColorPresentationTheme.actionSheet
} else {
sheetTheme = theme.actionSheet
}
return TGMenuSheetPallete(dark: forceDark || theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
}
public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, presentationData: PresentationData, images: [UIImage], presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController {
@ -441,7 +446,7 @@ public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLoca
let presentationDisposable = context.sharedContext.presentationData.start(next: { [weak legacyController] presentationData in
if let legacyController = legacyController, let controller = legacyController.legacyController as? TGMenuSheetController {
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme)
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false)
}
})
legacyController.disposables.add(presentationDisposable)

View File

@ -279,6 +279,22 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone
return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
}
func darkMenuSheetPallete() -> TGMenuSheetPallete! {
let theme: PresentationTheme
if let legacyContext = legacyContext {
let presentationData = legacyContext.sharedContext.currentPresentationData.with { $0 }
if presentationData.theme.overallDarkAppearance {
theme = presentationData.theme
} else {
theme = defaultDarkColorPresentationTheme
}
} else {
theme = defaultDarkColorPresentationTheme
}
let sheetTheme = theme.actionSheet
return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
}
func mediaAssetsPallete() -> TGMediaAssetsPallete! {
let presentationTheme: PresentationTheme
if let legacyContext = legacyContext {

View File

@ -69,6 +69,10 @@ public enum AvatarGalleryEntry: Equatable {
case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, GalleryItemIndexData?, Data?, String?)
case image(MediaId, TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, Int32?, GalleryItemIndexData?, MessageId?, Data?, String?)
public init(representation: TelegramMediaImageRepresentation, peer: Peer) {
self = .topImage([ImageRepresentationWithReference(representation: representation, reference: MediaResourceReference.standalone(resource: representation.resource))], [], peer, nil, nil, nil)
}
public var id: AvatarGalleryEntryId {
switch self {
case let .topImage(representations, _, _, _, _, _):

View File

@ -125,8 +125,9 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
private let statusNode: RadialStatusNode
private var playerStatus: MediaPlayerStatus?
private var isLoading = ValuePromise<Bool>(false)
private var loadingProgress = ValuePromise<Float?>(nil)
private var isLoading = Promise<Bool>(false)
private var loadingProgress = Promise<Float?>(nil)
private var progress: Signal<Float?, NoError>?
private var loadingProgressDisposable = MetaDisposable()
private var hasProgress = false
@ -241,8 +242,11 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
bufferingProgress = nil
}
}
self.loadingProgress.set(bufferingProgress)
self.isLoading.set(bufferingProgress != nil)
if self.progress == nil {
self.loadingProgress.set(.single(bufferingProgress))
self.isLoading.set(.single(bufferingProgress != nil))
}
}
public func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) {
@ -310,8 +314,16 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
self.isReady.set(videoNode.ready |> map { return true })
}
func setup(item: PeerInfoAvatarListItem, synchronous: Bool, fullSizeOnly: Bool = false) {
func setup(item: PeerInfoAvatarListItem, progress: Signal<Float?, NoError>? = nil, synchronous: Bool, fullSizeOnly: Bool = false) {
self.item = item
self.progress = progress
if let progress = progress {
self.loadingProgress.set((progress
|> beforeNext { [weak self] next in
self?.isLoading.set(.single(next != nil))
}))
}
let representations: [ImageRepresentationWithReference]
let videoRepresentations: [VideoRepresentationWithReference]
@ -912,7 +924,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
return items.count == 0
}
public func update(size: CGSize, peer: Peer?, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
private var additionalEntryProgress: Signal<Float?, NoError>? = nil
public func update(size: CGSize, peer: Peer?, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError> = .single(nil), isExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = size
let previousExpanded = self.isExpanded
self.isExpanded = isExpanded
@ -924,17 +937,23 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
if let peer = peer, !self.initializedList {
self.initializedList = true
self.disposable.set((peerInfoProfilePhotosWithCache(context: self.context, peerId: peer.id)
|> deliverOnMainQueue).start(next: { [weak self] (complete, entries) in
let entry = additionalEntry
|> map { representation -> AvatarGalleryEntry? in
return representation.flatMap { AvatarGalleryEntry(representation: $0.0, peer: peer) }
}
self.disposable.set(combineLatest(queue: Queue.mainQueue(), peerInfoProfilePhotosWithCache(context: self.context, peerId: peer.id), entry).start(next: { [weak self] completeAndEntries, entry in
guard let strongSelf = self else {
return
}
var (complete, entries) = completeAndEntries
if strongSelf.galleryEntries.count > 1, entries.count == 1 && !complete {
return
}
var entries = entries
var synchronous = false
if !strongSelf.galleryEntries.isEmpty, let updated = entries.first, case let .image(image) = updated, !image.3.isEmpty, let previous = strongSelf.galleryEntries.first, case let .topImage(topImage) = previous {
let firstEntry = AvatarGalleryEntry.image(image.0, image.1, topImage.0, image.3, image.4, image.5, image.6, image.7, image.8, image.9)
@ -943,6 +962,15 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
synchronous = true
}
if let entry = entry {
entries.insert(entry, at: 0)
strongSelf.additionalEntryProgress = additionalEntry
|> map { value -> Float? in
return value?.1
}
}
if strongSelf.ignoreNextProfilePhotoUpdate {
if entries.count == 1, let first = entries.first, case .topImage = first {
return
@ -1067,7 +1095,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
wasAdded = true
let addedItemNode = PeerInfoAvatarListItemNode(context: self.context, peer: peer)
itemNode = addedItemNode
addedItemNode.setup(item: self.items[i], synchronous: (i == 0 && i == self.currentIndex) || (synchronous && i == self.currentIndex), fullSizeOnly: self.firstFullSizeOnly && i == 0)
addedItemNode.setup(item: self.items[i], progress: i == 0 ? self.additionalEntryProgress : nil, synchronous: (i == 0 && i == self.currentIndex) || (synchronous && i == self.currentIndex), fullSizeOnly: self.firstFullSizeOnly && i == 0)
self.itemNodes[self.items[i].id] = addedItemNode
self.contentNode.addSubnode(addedItemNode)
}

View File

@ -44,6 +44,7 @@ swift_library(
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
"//submodules/WebSearchUI:WebSearchUI",
"//submodules/MapResourceToAvatarSizes:MapResourceToAvatarSizes",
],
visibility = [
"//visibility:public",

View File

@ -1401,13 +1401,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
)
self.temporaryParticipantsContext = nil
self.participantsContext = participantsContext
let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in
if let peer = transaction.getPeer(myPeerId) {
return (peer, transaction.getPeerCachedData(peerId: myPeerId))
let myPeer = self.accountContext.account.postbox.peerView(id: myPeerId)
|> map { view -> (Peer, CachedPeerData?)? in
if let peer = peerViewMainPeer(view) {
return (peer, view.cachedData)
} else {
return nil
}
}
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
participantsContext.state,
participantsContext.activeSpeakers,
@ -1507,6 +1509,19 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
if participant.peer.id == strongSelf.joinAsPeerId {
if let (myPeer, cachedData) = myPeerAndCachedData {
let about: String?
if let cachedData = cachedData as? CachedUserData {
about = cachedData.about
} else if let cachedData = cachedData as? CachedChannelData {
about = cachedData.about
} else {
about = nil
}
participant.peer = myPeer
participant.about = about
}
var filteredMuteState = participant.muteState
if isReconnectingAsSpeaker || strongSelf.currentConnectionMode != .rtc {
filteredMuteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: false)

View File

@ -28,6 +28,7 @@ import LegacyUI
import LegacyComponents
import LegacyMediaPickerUI
import WebSearchUI
import MapResourceToAvatarSizes
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
@ -381,6 +382,8 @@ public final class VoiceChatController: ViewController {
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
var updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil)
init(
updateIsMuted: @escaping (PeerId, Bool) -> Void,
openPeer: @escaping (PeerId) -> Void,
@ -652,7 +655,7 @@ public final class VoiceChatController: ViewController {
let revealOptions: [VoiceChatParticipantItem.RevealOption] = []
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, ssrc: peerEntry.ssrc, presence: peerEntry.presence, text: text, expandedText: expandedText, icon: icon, enabled: true, selectable: !peerEntry.isMyPeer || peerEntry.canManageCall || peerEntry.raisedHand, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, getVideo: {
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, ssrc: peerEntry.ssrc, presence: peerEntry.presence, text: text, expandedText: expandedText, icon: icon, enabled: true, selectable: true, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, getVideo: {
if let ssrc = peerEntry.ssrc {
return interaction.getPeerVideo(ssrc)
} else {
@ -664,6 +667,8 @@ public final class VoiceChatController: ViewController {
interaction.peerContextAction(peerEntry, node, nil)
}, contextAction: nil, getIsExpanded: {
return interaction.isExpanded
}, getUpdatingAvatar: {
return interaction.updateAvatarPromise.get()
})
}
}
@ -787,6 +792,10 @@ public final class VoiceChatController: ViewController {
private var currentDominantSpeakerWithVideo: (PeerId, UInt32)?
private var updateAvatarDisposable = MetaDisposable()
private let updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil)
private var currentUpdatingAvatar: TelegramMediaImageRepresentation?
init(controller: VoiceChatController, sharedContext: SharedAccountContext, call: PresentationGroupCall) {
self.controller = controller
self.sharedContext = sharedContext
@ -1239,6 +1248,12 @@ public final class VoiceChatController: ViewController {
}
if peer.id == strongSelf.callState?.myPeerId {
let maxLength: Int
if peer.id.namespace == Namespaces.Peer.CloudUser {
maxLength = 70
} else {
maxLength = 100
}
if entry.raisedHand {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_CancelSpeakRequest, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor)
@ -1251,35 +1266,41 @@ public final class VoiceChatController: ViewController {
f(.default)
})))
}
// items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ChangePhoto, icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor)
// }, action: { _, f in
// guard let strongSelf = self else {
// return
// }
//
// f(.default)
//
// strongSelf.openAvatarForEditing(fromGallery: false, completion: {})
// })))
// items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditBio, icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
// }, action: { _, f in
// guard let strongSelf = self else {
// return
// }
// f(.default)
//
// let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, apply: { bio in
// if let strongSelf = self {
// let _ = updateAbout(account: strongSelf.context.account, about: bio)
// |> `catch` { _ -> Signal<Void, NoError> in
// return .complete()
// }
// }
// })
// self?.controller?.present(controller, in: .window(.root))
// })))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ChangePhoto, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
f(.default)
Queue.mainQueue().after(0.1) {
strongSelf.openAvatarForEditing(fromGallery: false, completion: {})
}
})))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditBio, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
f(.default)
Queue.mainQueue().after(0.1) {
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, maxLength: maxLength, apply: { bio in
if let strongSelf = self {
let _ = (updateAbout(account: strongSelf.context.account, about: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false })
}
})
self?.controller?.present(controller, in: .window(.root))
}
})))
} else {
if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) {
if callState.adminIds.contains(peer.id) {
@ -1355,8 +1376,8 @@ public final class VoiceChatController: ViewController {
openTitle = strongSelf.presentationData.strings.VoiceChat_OpenChannel
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels")
} else {
openTitle = strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile
openIcon = UIImage(bundleImageName: "Chat/Context Menu/User")
openTitle = strongSelf.presentationData.strings.Conversation_ContextMenuSendMessage
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message")
}
items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in
return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor)
@ -1368,24 +1389,7 @@ public final class VoiceChatController: ViewController {
let context = strongSelf.context
strongSelf.controller?.dismiss(completion: {
Queue.mainQueue().after(0.3) {
if peer.id.namespace == Namespaces.Peer.CloudUser {
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peer.id)
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
var expandAvatar = true
if peer.smallProfileImage == nil {
expandAvatar = false
}
if let (validLayout, _) = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet {
expandAvatar = false
}
if let strongSelf = self, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar, fromChat: false) {
navigationController.pushViewController(controller)
}
})
} else {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer.id), keepStack: .always, purposefulAction: {}, peekData: nil))
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer.id), keepStack: .always, purposefulAction: {}, peekData: nil))
}
})
@ -1473,6 +1477,7 @@ public final class VoiceChatController: ViewController {
}
return nil
})
self.itemInteraction?.updateAvatarPromise = self.updateAvatarPromise
self.topPanelNode.addSubnode(self.topPanelEdgeNode)
self.topPanelNode.addSubnode(self.topPanelBackgroundNode)
@ -1851,6 +1856,7 @@ public final class VoiceChatController: ViewController {
self.memberEventsDisposable.dispose()
self.reconnectedAsEventsDisposable.dispose()
self.voiceSourcesDisposable.dispose()
self.updateAvatarDisposable.dispose()
}
private func openContextMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
@ -1911,7 +1917,7 @@ public final class VoiceChatController: ViewController {
return
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, apply: { title in
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title {
strongSelf.call.updateTitle(title)
@ -1989,7 +1995,7 @@ public final class VoiceChatController: ViewController {
return
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, apply: { title in
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title {
strongSelf.call.setShouldBeRecording(true, title: title)
@ -3251,8 +3257,13 @@ public final class VoiceChatController: ViewController {
}
}
var memberPeer = member.peer
if member.peer.id == self.callState?.myPeerId, let user = memberPeer as? TelegramUser, let photo = self.currentUpdatingAvatar {
memberPeer = user.withUpdatedPhoto([photo])
}
entries.append(.peer(PeerEntry(
peer: member.peer,
peer: memberPeer,
about: member.about,
isMyPeer: self.callState?.myPeerId == member.peer.id,
ssrc: member.ssrc,
@ -3567,7 +3578,7 @@ public final class VoiceChatController: ViewController {
let presentationData = strongSelf.presentationData
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
let legacyController = LegacyController(presentation: .custom, theme: strongSelf.darkTheme)
legacyController.statusBar.statusBarStyle = .Ignore
let emptyController = LegacyEmptyController(context: legacyController.context)!
@ -3598,6 +3609,7 @@ public final class VoiceChatController: ViewController {
// }
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
mixin.forceDark = true
mixin.stickersContext = paintStickersContext
let _ = strongSelf.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in
@ -3606,7 +3618,7 @@ public final class VoiceChatController: ViewController {
}
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
assetsController?.dismiss()
// self?.updateProfilePhoto(result)
self?.updateProfilePhoto(result)
}))
controller.navigationPresentation = .modal
strongSelf.controller?.push(controller)
@ -3618,7 +3630,7 @@ public final class VoiceChatController: ViewController {
mixin.didFinishWithImage = { [weak self] image in
if let image = image {
completion()
// self?.updateProfilePhoto(image)
self?.updateProfilePhoto(image)
}
}
mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in
@ -3697,6 +3709,41 @@ public final class VoiceChatController: ViewController {
}
})
}
private func updateProfilePhoto(_ image: UIImage) {
guard let data = image.jpegData(compressionQuality: 0.6), let peerId = self.callState?.myPeerId else {
return
}
let resource = LocalFileMediaResource(fileId: arc4random64())
self.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [])
self.currentUpdatingAvatar = representation
self.updateAvatarPromise.set(.single((representation, 0.0)))
let postbox = self.call.account.postbox
let signal = peerId.namespace == Namespaces.Peer.CloudUser ? updateAccountPhoto(account: self.call.account, resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
}) : updatePeerPhoto(postbox: postbox, network: self.call.account.network, stateManager: self.call.account.stateManager, accountPeerId: self.context.account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: postbox, network: self.call.account.network, resource: resource), mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
self.updateAvatarDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .complete:
strongSelf.updateAvatarPromise.set(.single(nil))
case let .progress(value):
strongSelf.updateAvatarPromise.set(.single((representation, value)))
}
}))
self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
}
}
private let sharedContext: SharedAccountContext

View File

@ -79,8 +79,9 @@ final class VoiceChatParticipantItem: ListViewItem {
let action: ((ASDisplayNode) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
let getIsExpanded: () -> Bool
let getUpdatingAvatar: () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, ssrc: UInt32?, presence: PeerPresence?, text: ParticipantText, expandedText: ParticipantText?, icon: Icon, enabled: Bool, selectable: Bool, getAudioLevel: (() -> Signal<Float, NoError>)?, getVideo: @escaping () -> GroupVideoNode?, revealOptions: [RevealOption], revealed: Bool?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, action: ((ASDisplayNode) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getIsExpanded: @escaping () -> Bool) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, ssrc: UInt32?, presence: PeerPresence?, text: ParticipantText, expandedText: ParticipantText?, icon: Icon, enabled: Bool, selectable: Bool, getAudioLevel: (() -> Signal<Float, NoError>)?, getVideo: @escaping () -> GroupVideoNode?, revealOptions: [RevealOption], revealed: Bool?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, action: ((ASDisplayNode) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getIsExpanded: @escaping () -> Bool, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -101,6 +102,7 @@ final class VoiceChatParticipantItem: ListViewItem {
self.action = action
self.contextAction = contextAction
self.getIsExpanded = getIsExpanded
self.getUpdatingAvatar = getUpdatingAvatar
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -408,7 +410,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
avatarListWrapperNode.addSubnode(avatarListContainerNode)
avatarListNode.update(size: targetRect.size, peer: item.peer, isExpanded: true, transition: .immediate)
avatarListNode.update(size: targetRect.size, peer: item.peer, additionalEntry: item.getUpdatingAvatar(), isExpanded: true, transition: .immediate)
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
strongSelf.audioLevelView?.alpha = 0.0

View File

@ -41,8 +41,11 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
}
}
init(theme: PresentationTheme, placeholder: String) {
private let maxLength: Int
init(theme: PresentationTheme, placeholder: String, maxLength: Int) {
self.theme = theme
self.maxLength = maxLength
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
@ -135,7 +138,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
if updatedText.count > 40 {
if updatedText.count > maxLength {
self.textInputNode.layer.addShakeAnimation()
return false
}
@ -205,7 +208,7 @@ private final class VoiceChatTitleEditAlertContentNode: AlertContentNode {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?) {
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int) {
self.strings = strings
self.title = title
self.text = text
@ -215,7 +218,7 @@ private final class VoiceChatTitleEditAlertContentNode: AlertContentNode {
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 8
self.inputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: placeholder)
self.inputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength)
self.inputFieldNode.text = value ?? ""
self.actionNodesSeparator = ASDisplayNode()
@ -408,7 +411,7 @@ private final class VoiceChatTitleEditAlertContentNode: AlertContentNode {
}
}
func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, apply: @escaping (String?) -> Void) -> AlertController {
func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void) -> AlertController {
var presentationData = sharedContext.currentPresentationData.with { $0 }
if let forceTheme = forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
@ -423,7 +426,7 @@ func voiceChatTitleEditController(sharedContext: SharedAccountContext, account:
applyImpl?()
})]
let contentNode = VoiceChatTitleEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value)
let contentNode = VoiceChatTitleEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength)
contentNode.complete = {
applyImpl?()
}

View File

@ -8527,7 +8527,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let presentationDisposable = strongSelf.context.sharedContext.presentationData.start(next: { [weak controller] presentationData in
if let controller = controller {
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme)
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false)
}
})
legacyController.disposables.add(presentationDisposable)

View File

@ -166,6 +166,9 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
params.dismissInput()
let _ = (gallery
|> deliverOnMainQueue).start(next: { gallery in
gallery.centralItemUpdated = { messageId in
params.centralItemUpdated?(messageId)
}
params.present(gallery, GalleryControllerPresentationArguments(transitionArguments: { messageId, media in
let selectedTransitionNode = params.transitionNode(messageId, media)
if let selectedTransitionNode = selectedTransitionNode {

View File

@ -124,6 +124,9 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
self.disposable?.dispose()
}
func ensureMessageIsVisible(id: MessageId) {
}
func scrollToTop() -> Bool {
if !self.listNode.scrollToOffsetFromTop(0.0) {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })

View File

@ -138,6 +138,10 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
self.playlistPreloadDisposable?.dispose()
}
func ensureMessageIsVisible(id: MessageId) {
}
func scrollToTop() -> Bool {
let offset = self.listNode.visibleContentOffset()
switch offset {

View File

@ -168,6 +168,9 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
deinit {
}
func ensureMessageIsVisible(id: MessageId) {
}
func scrollToTop() -> Bool {
if !self.listNode.scrollToOffsetFromTop(0.0) {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })

View File

@ -872,6 +872,21 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self.animationTimer?.invalidate()
}
func ensureMessageIsVisible(id: MessageId) {
let activeRect = self.scrollNode.bounds
for item in self.mediaItems {
if item.message.id == id {
if let itemNode = self.visibleMediaItems[item.message.stableId] {
if !activeRect.contains(itemNode.frame) {
let targetContentOffset = CGPoint(x: 0.0, y: max(-self.scrollNode.view.contentInset.top, itemNode.frame.minY - (self.scrollNode.frame.height - itemNode.frame.height) / 2.0))
self.scrollNode.view.setContentOffset(targetContentOffset, animated: false)
}
}
break
}
}
}
private func requestHistoryAroundVisiblePosition() {
if self.isRequestingView {
return

View File

@ -24,6 +24,7 @@ protocol PeerInfoPaneNode: ASDisplayNode {
func addToTransitionSurface(view: UIView)
func updateHiddenMedia()
func updateSelectedMessages(animated: Bool)
func ensureMessageIsVisible(id: MessageId)
}
final class PeerInfoPaneWrapper {

View File

@ -3107,7 +3107,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
})
}
})
})))
}), centralItemUpdated: { [weak self] messageId in
self?.paneContainerNode.requestExpandTabs?()
self?.paneContainerNode.currentPane?.node.ensureMessageIsVisible(id: messageId)
}))
}
private func openResolved(_ result: ResolvedUrl) {
guard let navigationController = self.controller?.navigationController as? NavigationController else {