mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-25 09:32:46 +00:00
Timecode message text entities
Languages in settings search
This commit is contained in:
parent
a2ee837531
commit
dbfa8cc3ea
@ -94,7 +94,7 @@
|
||||
09B4EE4D21A7B73800847FA6 /* PermissionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4C21A7B73800847FA6 /* PermissionController.swift */; };
|
||||
09B4EE4F21A7B75D00847FA6 /* PermissionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */; };
|
||||
09B4EE5221A7CC3E00847FA6 /* SolidRoundedButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */; };
|
||||
09B4EE5621A8149C00847FA6 /* PermissionInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5521A8149C00847FA6 /* PermissionInfoItem.swift */; };
|
||||
09B4EE5621A8149C00847FA6 /* ItemListInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5521A8149C00847FA6 /* ItemListInfoItem.swift */; };
|
||||
09B4EE5E21AC626B00847FA6 /* PermissionContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */; };
|
||||
09B4EE6021AD4A0E00847FA6 /* InstantPageContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */; };
|
||||
09B4EE6221AD791600847FA6 /* InstantPageStoredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */; };
|
||||
@ -1257,7 +1257,7 @@
|
||||
09B4EE4C21A7B73800847FA6 /* PermissionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionController.swift; sourceTree = "<group>"; };
|
||||
09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionControllerNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidRoundedButtonNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE5521A8149C00847FA6 /* PermissionInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionInfoItem.swift; sourceTree = "<group>"; };
|
||||
09B4EE5521A8149C00847FA6 /* ItemListInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListInfoItem.swift; sourceTree = "<group>"; };
|
||||
09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionContentNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageContentNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageStoredState.swift; sourceTree = "<group>"; };
|
||||
@ -2922,7 +2922,6 @@
|
||||
D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */,
|
||||
D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */,
|
||||
D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */,
|
||||
09B4EE5521A8149C00847FA6 /* PermissionInfoItem.swift */,
|
||||
);
|
||||
name = Notifications;
|
||||
sourceTree = "<group>";
|
||||
@ -4153,6 +4152,7 @@
|
||||
D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */,
|
||||
D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */,
|
||||
D0BFAE5A20AB35D200793CF2 /* IconSwitchNode.swift */,
|
||||
09B4EE5521A8149C00847FA6 /* ItemListInfoItem.swift */,
|
||||
);
|
||||
name = Items;
|
||||
sourceTree = "<group>";
|
||||
@ -5879,7 +5879,7 @@
|
||||
09749BC521F0E024008FDDE9 /* StickersChatInputPanelItem.swift in Sources */,
|
||||
D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */,
|
||||
D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */,
|
||||
09B4EE5621A8149C00847FA6 /* PermissionInfoItem.swift in Sources */,
|
||||
09B4EE5621A8149C00847FA6 /* ItemListInfoItem.swift in Sources */,
|
||||
D0EC6DD11EB9F58900EBF1C3 /* HashtagChatInputPanelItem.swift in Sources */,
|
||||
D0EC6DD21EB9F58900EBF1C3 /* MentionChatInputContextPanelNode.swift in Sources */,
|
||||
D00701A22029F6D0006B9E34 /* TGMimeTypeMap.m in Sources */,
|
||||
|
||||
@ -108,7 +108,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
|
||||
break
|
||||
case .ignore:
|
||||
return .fail
|
||||
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage:
|
||||
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage, .timecode:
|
||||
return .waitForSingleTap
|
||||
}
|
||||
}
|
||||
@ -301,15 +301,15 @@ final class ChatBotInfoItemNode: ListViewItemNode {
|
||||
case .none, .ignore:
|
||||
break
|
||||
case let .url(url, _):
|
||||
item.controllerInteraction.longTap(.url(url))
|
||||
item.controllerInteraction.longTap(.url(url), nil)
|
||||
case let .peerMention(peerId, mention):
|
||||
item.controllerInteraction.longTap(.peerMention(peerId, mention))
|
||||
item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)
|
||||
case let .textMention(name):
|
||||
item.controllerInteraction.longTap(.mention(name))
|
||||
item.controllerInteraction.longTap(.mention(name), nil)
|
||||
case let .botCommand(command):
|
||||
item.controllerInteraction.longTap(.command(command))
|
||||
item.controllerInteraction.longTap(.command(command), nil)
|
||||
case let .hashtag(_, hashtag):
|
||||
item.controllerInteraction.longTap(.hashtag(hashtag))
|
||||
item.controllerInteraction.longTap(.hashtag(hashtag), nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -860,7 +860,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
}
|
||||
})
|
||||
}
|
||||
}, longTap: { [weak self] action in
|
||||
}, longTap: { [weak self] action, messageId in
|
||||
if let strongSelf = self {
|
||||
switch action {
|
||||
case let .url(url):
|
||||
@ -1026,6 +1026,30 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
])])
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .timecode(timecode, text):
|
||||
guard let messageId = messageId else {
|
||||
return
|
||||
}
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: text),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction?.seekToTimecode(messageId, timecode)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = text
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
}
|
||||
}, openCheckoutOrReceipt: { [weak self] messageId in
|
||||
@ -1192,6 +1216,24 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, seekToTimecode: { [weak self] messageId, timestamp in
|
||||
if let strongSelf = self {
|
||||
let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId)
|
||||
var completed = false
|
||||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
if !completed, let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.message.id == messageId, let (action, _, _, _, _) = itemNode.playMediaWithSound() {
|
||||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
||||
action(Double(timestamp))
|
||||
} else if let message = message {
|
||||
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
|
||||
}
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
if !completed, let message = message {
|
||||
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
|
||||
}
|
||||
}
|
||||
}, requestMessageUpdate: { [weak self] id in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||||
@ -3287,7 +3329,25 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
return
|
||||
}
|
||||
strongSelf.videoUnmuteTooltipController?.dismiss()
|
||||
strongSelf.chatDisplayNode.playFirstMediaWithSound()
|
||||
|
||||
var actions: [(Bool, (Double?) -> Void)] = []
|
||||
var hasUnconsumed = false
|
||||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
|
||||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
||||
actions.insert((isUnconsumed, action), at: 0)
|
||||
if !hasUnconsumed && isUnconsumed {
|
||||
hasUnconsumed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (isUnconsumed, action) in actions {
|
||||
if (!hasUnconsumed || isUnconsumed) {
|
||||
action(nil)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
@ -3511,7 +3571,13 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
}
|
||||
let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false
|
||||
if self.validLayout != nil && orientation.isLandscape && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) {
|
||||
self.chatDisplayNode.openCurrentPlayingWithSoundMedia()
|
||||
var completed = false
|
||||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled {
|
||||
let _ = self.controllerInteraction?.openMessage(message, .landscape)
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ public enum ChatControllerInteractionLongTapAction {
|
||||
case peerMention(PeerId, String)
|
||||
case command(String)
|
||||
case hashtag(String)
|
||||
case timecode(Double, String)
|
||||
}
|
||||
|
||||
public enum ChatControllerInteractionOpenMessageMode {
|
||||
@ -42,6 +43,7 @@ public enum ChatControllerInteractionOpenMessageMode {
|
||||
case stream
|
||||
case automaticPlayback
|
||||
case landscape
|
||||
case timecode(Double)
|
||||
}
|
||||
|
||||
struct ChatInterfacePollActionState: Equatable {
|
||||
@ -75,7 +77,7 @@ public final class ChatControllerInteraction {
|
||||
let navigationController: () -> NavigationController?
|
||||
let presentGlobalOverlayController: (ViewController, Any?) -> Void
|
||||
let callPeer: (PeerId) -> Void
|
||||
let longTap: (ChatControllerInteractionLongTapAction) -> Void
|
||||
let longTap: (ChatControllerInteractionLongTapAction, MessageId?) -> Void
|
||||
let openCheckoutOrReceipt: (MessageId) -> Void
|
||||
let openSearch: () -> Void
|
||||
let setupReply: (MessageId) -> Void
|
||||
@ -87,6 +89,7 @@ public final class ChatControllerInteraction {
|
||||
let requestSelectMessagePollOption: (MessageId, Data) -> Void
|
||||
let openAppStorePage: () -> Void
|
||||
let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
|
||||
let seekToTimecode: (MessageId, Double) -> Void
|
||||
|
||||
let requestMessageUpdate: (MessageId) -> Void
|
||||
let cancelInteractiveKeyboardGestures: () -> Void
|
||||
@ -99,7 +102,7 @@ public final class ChatControllerInteraction {
|
||||
var pollActionState: ChatInterfacePollActionState
|
||||
var searchTextHighightState: String?
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) {
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, MessageId?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (MessageId, Double) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
@ -138,6 +141,7 @@ public final class ChatControllerInteraction {
|
||||
self.requestSelectMessagePollOption = requestSelectMessagePollOption
|
||||
self.openAppStorePage = openAppStorePage
|
||||
self.displayMessageTooltip = displayMessageTooltip
|
||||
self.seekToTimecode = seekToTimecode
|
||||
|
||||
self.requestMessageUpdate = requestMessageUpdate
|
||||
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
||||
@ -152,7 +156,7 @@ public final class ChatControllerInteraction {
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
return false
|
||||
}, navigateToFirstDateMessage: { _ in
|
||||
@ -162,6 +166,7 @@ public final class ChatControllerInteraction {
|
||||
}, requestSelectMessagePollOption: { _, _ in
|
||||
}, openAppStorePage: {
|
||||
}, displayMessageTooltip: { _, _, _, _ in
|
||||
}, seekToTimecode: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
||||
@ -1488,43 +1488,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.historyNode.prefetchManager.updateAutoDownloadSettings(settings)
|
||||
}
|
||||
|
||||
func playFirstMediaWithSound() {
|
||||
var actions: [(CGFloat, Bool, () -> Void)] = []
|
||||
var hasUnconsumed = false
|
||||
self.historyNode.forEachVisibleItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
|
||||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
||||
actions.insert((fraction, isUnconsumed, action), at: 0)
|
||||
if !hasUnconsumed && isUnconsumed {
|
||||
hasUnconsumed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (_, isUnconsumed, action) in actions {
|
||||
if (!hasUnconsumed || isUnconsumed) {
|
||||
action()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openCurrentPlayingWithSoundMedia() {
|
||||
var result: (Message?, ListViewItemNode)?
|
||||
self.historyNode.forEachVisibleItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMessageItemView, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled {
|
||||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
||||
result = (itemNode.item?.message, itemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (message, _) = result {
|
||||
if let message = message {
|
||||
let _ = self.controllerInteraction.openMessage(message, .landscape)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isInputViewFocused: Bool {
|
||||
if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
|
||||
return inputPanelNode.isFocused
|
||||
|
||||
@ -248,7 +248,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
|
||||
TelegramTextAttributes.PeerMention,
|
||||
TelegramTextAttributes.PeerTextMention,
|
||||
TelegramTextAttributes.BotCommand,
|
||||
TelegramTextAttributes.Hashtag]
|
||||
TelegramTextAttributes.Hashtag,
|
||||
TelegramTextAttributes.Timecode]
|
||||
|
||||
for attribute in highlightedAttributes {
|
||||
if let _ = attributes[NSAttributedStringKey(rawValue: attribute)] {
|
||||
@ -308,10 +309,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
|
||||
|
||||
private func actionForAttributes(_ attributes: [NSAttributedStringKey: Any]) -> GalleryControllerInteractionTapAction? {
|
||||
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
// var concealed = true
|
||||
// if let attributeText = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
// concealed = !doesUrlMatchText(url: url, text: attributeText)
|
||||
// }
|
||||
return .url(url: url, concealed: false)
|
||||
} else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
||||
return .peerMention(peerMention.peerId, peerMention.mention)
|
||||
@ -321,6 +318,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
|
||||
return .botCommand(botCommand)
|
||||
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||
return .hashtag(hashtag.peerName, hashtag.hashtag)
|
||||
} else if let timecode = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
||||
return .timecode(timecode.time, timecode.text)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1017,7 +1017,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return self.contentImageNode?.playMediaWithSound()
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +73,7 @@ enum ChatMessageBubbleContentTapAction {
|
||||
case wallpaper
|
||||
case call(PeerId)
|
||||
case openMessage
|
||||
case timecode(Double, String)
|
||||
case ignore
|
||||
}
|
||||
|
||||
@ -147,7 +148,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
||||
func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) {
|
||||
}
|
||||
|
||||
func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -257,7 +257,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
break
|
||||
case .ignore:
|
||||
return .fail
|
||||
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage:
|
||||
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage, .timecode:
|
||||
return .waitForSingleTap
|
||||
}
|
||||
}
|
||||
@ -1727,6 +1727,37 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
break loop
|
||||
case let .timecode(timecode, _):
|
||||
foundTapAction = true
|
||||
if let item = self.item {
|
||||
var messageId: MessageId?
|
||||
for media in item.message.media {
|
||||
if let file = media as? TelegramMediaFile, file.duration != nil {
|
||||
messageId = item.message.id
|
||||
}
|
||||
}
|
||||
if messageId == nil {
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? ReplyMessageAttribute {
|
||||
if let replyMessage = item.message.associatedMessages[attribute.messageId] {
|
||||
for media in replyMessage.media {
|
||||
if let file = media as? TelegramMediaFile, file.duration != nil {
|
||||
messageId = replyMessage.id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if messageId == nil {
|
||||
messageId = item.message.id
|
||||
}
|
||||
if let messageId = messageId {
|
||||
item.controllerInteraction.seekToTimecode(messageId, timecode)
|
||||
}
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
if !foundTapAction {
|
||||
@ -1734,6 +1765,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
case .longTap, .doubleTap:
|
||||
if let item = self.item, self.backgroundNode.frame.contains(location) {
|
||||
let messageId = item.message.id
|
||||
|
||||
var foundTapAction = false
|
||||
var tapMessage: Message? = item.content.firstMessage
|
||||
var selectAll = true
|
||||
@ -1750,23 +1783,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
break
|
||||
case let .url(url, _):
|
||||
foundTapAction = true
|
||||
item.controllerInteraction.longTap(.url(url))
|
||||
item.controllerInteraction.longTap(.url(url), messageId)
|
||||
break loop
|
||||
case let .peerMention(peerId, mention):
|
||||
foundTapAction = true
|
||||
item.controllerInteraction.longTap(.peerMention(peerId, mention))
|
||||
item.controllerInteraction.longTap(.peerMention(peerId, mention), messageId)
|
||||
break loop
|
||||
case let .textMention(name):
|
||||
foundTapAction = true
|
||||
item.controllerInteraction.longTap(.mention(name))
|
||||
item.controllerInteraction.longTap(.mention(name), messageId)
|
||||
break loop
|
||||
case let .botCommand(command):
|
||||
foundTapAction = true
|
||||
item.controllerInteraction.longTap(.command(command))
|
||||
item.controllerInteraction.longTap(.command(command), messageId)
|
||||
break loop
|
||||
case let .hashtag(_, hashtag):
|
||||
foundTapAction = true
|
||||
item.controllerInteraction.longTap(.hashtag(hashtag))
|
||||
item.controllerInteraction.longTap(.hashtag(hashtag), messageId)
|
||||
break loop
|
||||
case .instantPage:
|
||||
break
|
||||
@ -1777,6 +1810,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
case .openMessage:
|
||||
foundTapAction = false
|
||||
break
|
||||
case let .timecode(timecode, text):
|
||||
foundTapAction = true
|
||||
item.controllerInteraction.longTap(.timecode(timecode, text), messageId)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
if !foundTapAction, let tapMessage = tapMessage {
|
||||
@ -1932,7 +1969,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
|
||||
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
for contentNode in self.contentNodes {
|
||||
if let playMediaWithSound = contentNode.playMediaWithSound() {
|
||||
return playMediaWithSound
|
||||
|
||||
@ -709,7 +709,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return self.interactiveVideoNode.playMediaWithSound()
|
||||
}
|
||||
}
|
||||
|
||||
@ -504,7 +504,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if strongSelf.consumableContentNode.image !== consumableContentIcon {
|
||||
strongSelf.consumableContentNode.image = consumableContentIcon
|
||||
}
|
||||
strongSelf.consumableContentNode.frame = CGRect(origin: CGPoint(x: descriptionFrame.maxX + 2.0, y: descriptionFrame.minY + 5.0), size: consumableContentIcon.size)
|
||||
strongSelf.consumableContentNode.frame = CGRect(origin: CGPoint(x: descriptionFrame.maxX + 5.0, y: descriptionFrame.minY + 5.0), size: consumableContentIcon.size)
|
||||
} else if strongSelf.consumableContentNode.supernode != nil {
|
||||
strongSelf.consumableContentNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
@ -749,7 +749,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func playMediaWithSound() -> (action: () -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
|
||||
func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
|
||||
if let item = self.item {
|
||||
var isUnconsumed = false
|
||||
for attribute in item.message.attributes {
|
||||
@ -761,7 +761,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
return ({
|
||||
return ({ _ in
|
||||
if !self.infoBackgroundNode.alpha.isZero {
|
||||
let _ = (item.context.sharedContext.mediaManager.globalMediaPlayerState
|
||||
|> take(1)
|
||||
|
||||
@ -1210,7 +1210,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
|
||||
func playMediaWithSound() -> (action: () -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
|
||||
func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
|
||||
var isAnimated = false
|
||||
if let file = self.media as? TelegramMediaFile, file.isAnimated {
|
||||
isAnimated = true
|
||||
@ -1224,25 +1224,30 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let videoNode = self.videoNode, let context = self.context, (self.automaticPlayback ?? false) && !isAnimated {
|
||||
return ({
|
||||
let _ = (context.sharedContext.mediaManager.globalMediaPlayerState
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { playlistStateAndType in
|
||||
var canPlay = true
|
||||
if let (_, state, _) = playlistStateAndType {
|
||||
switch state {
|
||||
case let .state(state):
|
||||
if case .playing = state.status.status {
|
||||
canPlay = false
|
||||
}
|
||||
case .loading:
|
||||
break
|
||||
return ({ timecode in
|
||||
if let timecode = timecode {
|
||||
context.sharedContext.mediaManager.playlistControl(.playback(.pause))
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seek: .timecode(timecode), actionAtEnd: actionAtEnd)
|
||||
} else {
|
||||
let _ = (context.sharedContext.mediaManager.globalMediaPlayerState
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { playlistStateAndType in
|
||||
var canPlay = true
|
||||
if let (_, state, _) = playlistStateAndType {
|
||||
switch state {
|
||||
case let .state(state):
|
||||
if case .playing = state.status.status {
|
||||
canPlay = false
|
||||
}
|
||||
case .loading:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if canPlay {
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: actionAtEnd)
|
||||
}
|
||||
})
|
||||
if canPlay {
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: actionAtEnd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, (self.playerStatus?.soundEnabled ?? false), false, false, self.badgeNode)
|
||||
} else {
|
||||
return nil
|
||||
|
||||
@ -697,7 +697,7 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
func updateAutomaticMediaDownloadSettings() {
|
||||
}
|
||||
|
||||
func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -757,7 +757,7 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
if let item = self.item {
|
||||
switch button.action {
|
||||
case let .url(url):
|
||||
item.controllerInteraction.longTap(.url(url))
|
||||
item.controllerInteraction.longTap(.url(url), item.message.id)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -320,7 +320,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return mediaHidden
|
||||
}
|
||||
|
||||
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return self.interactiveImageNode.playMediaWithSound()
|
||||
}
|
||||
|
||||
|
||||
@ -139,9 +139,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let attributedText: NSAttributedString
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
|
||||
var mediaDuration: Double? = nil
|
||||
var isUnsupportedMedia = false
|
||||
for media in item.message.media {
|
||||
if let _ = media as? TelegramMediaUnsupported {
|
||||
if let file = media as? TelegramMediaFile, let duration = file.duration {
|
||||
mediaDuration = Double(duration)
|
||||
}
|
||||
else if media is TelegramMediaUnsupported {
|
||||
isUnsupportedMedia = true
|
||||
}
|
||||
}
|
||||
@ -154,7 +158,15 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
messageEntities = attribute.entities
|
||||
break
|
||||
} else if mediaDuration == nil, let attribute = attribute as? ReplyMessageAttribute {
|
||||
if let replyMessage = item.message.associatedMessages[attribute.messageId] {
|
||||
for media in replyMessage.media {
|
||||
if let file = media as? TelegramMediaFile, let duration = file.duration {
|
||||
mediaDuration = Double(duration)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,8 +178,17 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
entities = cached.entities
|
||||
} else {
|
||||
entities = messageEntities
|
||||
|
||||
if entities == nil && mediaDuration != nil {
|
||||
entities = []
|
||||
}
|
||||
|
||||
if let entitiesValue = entities {
|
||||
if let result = addLocallyGeneratedEntities(rawText, enabledTypes: .all, entities: entitiesValue) {
|
||||
var enabledTypes: EnabledEntityTypes = .all
|
||||
if mediaDuration != nil {
|
||||
enabledTypes.insert(.timecode)
|
||||
}
|
||||
if let result = addLocallyGeneratedEntities(rawText, enabledTypes: enabledTypes, entities: entitiesValue, mediaDuration: mediaDuration) {
|
||||
entities = result
|
||||
}
|
||||
} else {
|
||||
@ -372,6 +393,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return .botCommand(botCommand)
|
||||
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||
return .hashtag(hashtag.peerName, hashtag.hashtag)
|
||||
} else if let timecode = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
||||
return .timecode(timecode.time, timecode.text)
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
@ -391,7 +414,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
TelegramTextAttributes.PeerMention,
|
||||
TelegramTextAttributes.PeerTextMention,
|
||||
TelegramTextAttributes.BotCommand,
|
||||
TelegramTextAttributes.Hashtag
|
||||
TelegramTextAttributes.Hashtag,
|
||||
TelegramTextAttributes.Timecode
|
||||
]
|
||||
for name in possibleNames {
|
||||
if let _ = attributes[NSAttributedStringKey(rawValue: name)] {
|
||||
|
||||
@ -369,7 +369,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
|
||||
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return self.contentNode.playMediaWithSound()
|
||||
}
|
||||
|
||||
|
||||
@ -220,133 +220,156 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, presentController: { _, _ in
|
||||
}, navigationController: { [weak self] in
|
||||
return self?.getNavigationController()
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action, messageId in
|
||||
if let strongSelf = self {
|
||||
switch action {
|
||||
case let .url(url):
|
||||
var cleanUrl = url
|
||||
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
|
||||
var canAddToReadingList = true
|
||||
let mailtoString = "mailto:"
|
||||
let telString = "tel:"
|
||||
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
|
||||
if cleanUrl.hasPrefix(mailtoString) {
|
||||
canAddToReadingList = false
|
||||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
|
||||
} else if cleanUrl.hasPrefix(telString) {
|
||||
canAddToReadingList = false
|
||||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
|
||||
openText = strongSelf.presentationData.strings.Conversation_Call
|
||||
} else if canOpenIn {
|
||||
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
|
||||
}
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: cleanUrl))
|
||||
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.openUrl(url)
|
||||
case let .url(url):
|
||||
var cleanUrl = url
|
||||
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
|
||||
var canAddToReadingList = true
|
||||
let mailtoString = "mailto:"
|
||||
let telString = "tel:"
|
||||
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
|
||||
if cleanUrl.hasPrefix(mailtoString) {
|
||||
canAddToReadingList = false
|
||||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
|
||||
} else if cleanUrl.hasPrefix(telString) {
|
||||
canAddToReadingList = false
|
||||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
|
||||
openText = strongSelf.presentationData.strings.Conversation_Call
|
||||
} else if canOpenIn {
|
||||
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
|
||||
}
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = cleanUrl
|
||||
}))
|
||||
if canAddToReadingList {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let link = URL(string: url) {
|
||||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||||
}
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .peerMention(peerId, mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
var items: [ActionSheetItem] = []
|
||||
if !mention.isEmpty {
|
||||
items.append(ActionSheetTextItem(title: mention))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.openPeer(peerId: peerId, peer: nil)
|
||||
}
|
||||
}))
|
||||
if !mention.isEmpty {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .mention(mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: mention),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: cleanUrl))
|
||||
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.openPeerMention(mention)
|
||||
strongSelf.openUrl(url)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
UIPasteboard.general.string = cleanUrl
|
||||
}))
|
||||
if canAddToReadingList {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let link = URL(string: url) {
|
||||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||||
}
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .command(command):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: command),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = command
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .hashtag(hashtag):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: hashtag),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .peerMention(peerId, mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
var items: [ActionSheetItem] = []
|
||||
if !mention.isEmpty {
|
||||
items.append(ActionSheetTextItem(title: mention))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
let searchController = HashtagSearchController(context: strongSelf.context, peer: strongSelf.peer, query: hashtag)
|
||||
strongSelf.pushController(searchController)
|
||||
strongSelf.openPeer(peerId: peerId, peer: nil)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = hashtag
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
}))
|
||||
if !mention.isEmpty {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .mention(mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: mention),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.openPeerMention(mention)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .command(command):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: command),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = command
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .hashtag(hashtag):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: hashtag),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
let searchController = HashtagSearchController(context: strongSelf.context, peer: strongSelf.peer, query: hashtag)
|
||||
strongSelf.pushController(searchController)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = hashtag
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
case let .timecode(timecode, text):
|
||||
guard let messageId = messageId else {
|
||||
return
|
||||
}
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: text),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction?.seekToTimecode(messageId, timecode)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = text
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.presentController(actionSheet, nil)
|
||||
}
|
||||
}
|
||||
}, openCheckoutOrReceipt: { _ in
|
||||
@ -364,6 +387,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
strongSelf.context.sharedContext.applicationBindings.openAppStorePage()
|
||||
}
|
||||
}, displayMessageTooltip: { _, _, _, _ in
|
||||
}, seekToTimecode: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
||||
@ -163,7 +163,7 @@ enum ContactListPeer: Equatable {
|
||||
private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
case search(PresentationTheme, PresentationStrings)
|
||||
case sort(PresentationTheme, PresentationStrings, ContactsSortOrder)
|
||||
case permissionInfo(PresentationTheme, PresentationStrings, Bool)
|
||||
case permissionInfo(PresentationTheme, String, String, Bool)
|
||||
case permissionEnable(PresentationTheme, String)
|
||||
case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings)
|
||||
case peer(Int, ContactListPeer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool)
|
||||
@ -204,8 +204,8 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
return ContactListActionItem(theme: theme, title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: {
|
||||
interaction.openSortMenu()
|
||||
})
|
||||
case let .permissionInfo(theme, strings, suppressed):
|
||||
return PermissionInfoItem(theme: theme, strings: strings, subject: .contacts, type: .denied, style: .plain, suppressed: suppressed, close: {
|
||||
case let .permissionInfo(theme, title, text, suppressed):
|
||||
return InfoListItem(theme: theme, title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : {
|
||||
interaction.suppressWarning()
|
||||
})
|
||||
case let .permissionEnable(theme, text):
|
||||
@ -250,8 +250,8 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .permissionInfo(lhsTheme, lhsStrings, lhsSuppressed):
|
||||
if case let .permissionInfo(rhsTheme, rhsStrings, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsSuppressed == rhsSuppressed {
|
||||
case let .permissionInfo(lhsTheme, lhsTitle, lhsText, lhsSuppressed):
|
||||
if case let .permissionInfo(rhsTheme, rhsTitle, rhsText, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSuppressed == rhsSuppressed {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -441,15 +441,17 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer]
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
let (suppressed, syncDisabled) = warningSuppressed
|
||||
if !peers.isEmpty && !syncDisabled {
|
||||
let title = strings.Contacts_PermissionsTitle
|
||||
let text = strings.Contacts_PermissionsText
|
||||
switch authorizationStatus {
|
||||
case .denied:
|
||||
entries.append(.permissionInfo(theme, strings, suppressed))
|
||||
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0))
|
||||
addHeader = true
|
||||
case .notDetermined:
|
||||
entries.append(.permissionInfo(theme, strings, false))
|
||||
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllow_v0))
|
||||
addHeader = true
|
||||
case .denied:
|
||||
entries.append(.permissionInfo(theme, title, text, suppressed))
|
||||
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0))
|
||||
addHeader = true
|
||||
case .notDetermined:
|
||||
entries.append(.permissionInfo(theme, title, text, false))
|
||||
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllow_v0))
|
||||
addHeader = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String {
|
||||
return message.text
|
||||
}
|
||||
|
||||
func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }) -> GalleryItem? {
|
||||
func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }) -> GalleryItem? {
|
||||
switch entry {
|
||||
case let .MessageEntry(message, _, location, _, _):
|
||||
if let (media, mediaImage) = mediaForMessage(message: message) {
|
||||
@ -157,8 +157,14 @@ func galleryItemForEntry(context: AccountContext, presentationData: Presentation
|
||||
break
|
||||
}
|
||||
}
|
||||
let caption = galleryCaptionStringWithAppliedEntities(galleryMessageCaptionText(message), entities: entities)
|
||||
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions)
|
||||
|
||||
let text = galleryMessageCaptionText(message)
|
||||
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
|
||||
entities = result
|
||||
}
|
||||
|
||||
let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities)
|
||||
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions)
|
||||
} else {
|
||||
if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" {
|
||||
var pixelsCount: Int = 0
|
||||
@ -194,7 +200,7 @@ func galleryItemForEntry(context: AccountContext, presentationData: Presentation
|
||||
}
|
||||
}
|
||||
if let content = content {
|
||||
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: performAction, openActionOptions: openActionOptions)
|
||||
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, performAction: performAction, openActionOptions: openActionOptions)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -251,6 +257,11 @@ enum GalleryControllerInteractionTapAction {
|
||||
case peerMention(PeerId, String)
|
||||
case botCommand(String)
|
||||
case hashtag(String?, String)
|
||||
case timecode(Double, String)
|
||||
}
|
||||
|
||||
public enum GalleryControllerItemNodeAction {
|
||||
case timecode(Double)
|
||||
}
|
||||
|
||||
final class GalleryControllerActionInteraction {
|
||||
@ -298,6 +309,7 @@ class GalleryController: ViewController {
|
||||
var temporaryDoNotWaitForReady = false
|
||||
private let fromPlayingVideo: Bool
|
||||
private let landscape: Bool
|
||||
private let timecode: Double?
|
||||
|
||||
private let accountInUseDisposable = MetaDisposable()
|
||||
private let disposable = MetaDisposable()
|
||||
@ -323,7 +335,7 @@ class GalleryController: ViewController {
|
||||
private var performAction: (GalleryControllerInteractionTapAction) -> Void
|
||||
private var openActionOptions: (GalleryControllerInteractionTapAction) -> Void
|
||||
|
||||
init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
|
||||
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, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
|
||||
self.context = context
|
||||
self.source = source
|
||||
self.replaceRootController = replaceRootController
|
||||
@ -332,6 +344,7 @@ class GalleryController: ViewController {
|
||||
self.streamVideos = streamSingleVideo
|
||||
self.fromPlayingVideo = fromPlayingVideo
|
||||
self.landscape = landscape
|
||||
self.timecode = timecode
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -438,7 +451,7 @@ class GalleryController: ViewController {
|
||||
if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == strongSelf.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions) {
|
||||
if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions) {
|
||||
if isCentral {
|
||||
centralItemIndex = items.count
|
||||
}
|
||||
@ -536,7 +549,10 @@ class GalleryController: ViewController {
|
||||
|
||||
performActionImpl = { [weak self] action in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
if case .timecode = action {
|
||||
} else {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
}
|
||||
switch action {
|
||||
case let .url(url, concealed):
|
||||
strongSelf.actionInteraction?.openUrl(url, concealed)
|
||||
@ -548,6 +564,8 @@ class GalleryController: ViewController {
|
||||
strongSelf.actionInteraction?.openBotCommand(command)
|
||||
case let .hashtag(peerName, hashtag):
|
||||
strongSelf.actionInteraction?.openHashtag(peerName, hashtag)
|
||||
case let .timecode(timecode, _):
|
||||
strongSelf.galleryNode.pager.centralItemNode()?.processAction(.timecode(timecode))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -555,149 +573,171 @@ class GalleryController: ViewController {
|
||||
openActionOptionsImpl = { [weak self] action in
|
||||
if let strongSelf = self {
|
||||
switch action {
|
||||
case let .url(url, _):
|
||||
var cleanUrl = url
|
||||
var canAddToReadingList = true
|
||||
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
|
||||
let mailtoString = "mailto:"
|
||||
let telString = "tel:"
|
||||
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
|
||||
var phoneNumber: String?
|
||||
if cleanUrl.hasPrefix(mailtoString) {
|
||||
canAddToReadingList = false
|
||||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
|
||||
} else if cleanUrl.hasPrefix(telString) {
|
||||
canAddToReadingList = false
|
||||
phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
|
||||
cleanUrl = phoneNumber!
|
||||
openText = strongSelf.presentationData.strings.UserInfo_PhoneCall
|
||||
} else if canOpenIn {
|
||||
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
|
||||
}
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: cleanUrl))
|
||||
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
if canOpenIn {
|
||||
strongSelf.actionInteraction?.openUrlIn(url)
|
||||
} else {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.openUrl(url, false)
|
||||
}
|
||||
case let .url(url, _):
|
||||
var cleanUrl = url
|
||||
var canAddToReadingList = true
|
||||
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
|
||||
let mailtoString = "mailto:"
|
||||
let telString = "tel:"
|
||||
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
|
||||
var phoneNumber: String?
|
||||
if cleanUrl.hasPrefix(mailtoString) {
|
||||
canAddToReadingList = false
|
||||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
|
||||
} else if cleanUrl.hasPrefix(telString) {
|
||||
canAddToReadingList = false
|
||||
phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
|
||||
cleanUrl = phoneNumber!
|
||||
openText = strongSelf.presentationData.strings.UserInfo_PhoneCall
|
||||
} else if canOpenIn {
|
||||
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
|
||||
}
|
||||
}))
|
||||
if let phoneNumber = phoneNumber {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: cleanUrl))
|
||||
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
if canOpenIn {
|
||||
strongSelf.actionInteraction?.openUrlIn(url)
|
||||
} else {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.openUrl(url, false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
if let phoneNumber = phoneNumber {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.addContact(phoneNumber)
|
||||
}
|
||||
}))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = cleanUrl
|
||||
}))
|
||||
if canAddToReadingList {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let link = URL(string: url) {
|
||||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||||
}
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .peerMention(peerId, mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
var items: [ActionSheetItem] = []
|
||||
if !mention.isEmpty {
|
||||
items.append(ActionSheetTextItem(title: mention))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.addContact(phoneNumber)
|
||||
strongSelf.actionInteraction?.openPeer(peerId)
|
||||
}
|
||||
}))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = cleanUrl
|
||||
}))
|
||||
if canAddToReadingList {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let link = URL(string: url) {
|
||||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||||
}
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .peerMention(peerId, mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
var items: [ActionSheetItem] = []
|
||||
if !mention.isEmpty {
|
||||
items.append(ActionSheetTextItem(title: mention))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.openPeer(peerId)
|
||||
if !mention.isEmpty {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
}))
|
||||
}
|
||||
}))
|
||||
if !mention.isEmpty {
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .textMention(mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: mention),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.openPeerMention(mention)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .botCommand(command):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: command))
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
UIPasteboard.general.string = command
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .textMention(mention):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: mention),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.openPeerMention(mention)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = mention
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .botCommand(command):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: command))
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = command
|
||||
}))
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .hashtag(peerName, hashtag):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: hashtag),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.openHashtag(peerName, hashtag)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = hashtag
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .hashtag(peerName, hashtag):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: hashtag),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.actionInteraction?.openHashtag(peerName, hashtag)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = hashtag
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
case let .timecode(timecode, text):
|
||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: text),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss(forceAway: false)
|
||||
strongSelf.galleryNode.pager.centralItemNode()?.processAction(.timecode(timecode))
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = text
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -817,8 +857,12 @@ class GalleryController: ViewController {
|
||||
var items: [GalleryItem] = []
|
||||
var centralItemIndex: Int?
|
||||
for entry in self.entries {
|
||||
if let item = galleryItemForEntry(context: context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: self.fromPlayingVideo, landscape: self.landscape, performAction: self.performAction, openActionOptions: self.openActionOptions) {
|
||||
if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == self.centralEntryStableId {
|
||||
var isCentral = false
|
||||
if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == self.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, performAction: self.performAction, openActionOptions: self.openActionOptions) {
|
||||
if isCentral {
|
||||
centralItemIndex = items.count
|
||||
}
|
||||
items.append(item)
|
||||
|
||||
@ -69,6 +69,9 @@ open class GalleryItemNode: ASDisplayNode {
|
||||
open func activateAsInitial() {
|
||||
}
|
||||
|
||||
open func processAction(_ action: GalleryControllerItemNodeAction) {
|
||||
}
|
||||
|
||||
open func visibilityUpdated(isVisible: Bool) {
|
||||
}
|
||||
|
||||
|
||||
@ -27,11 +27,28 @@ private let externalIdentifierDelimiterSet: CharacterSet = {
|
||||
set.remove(".")
|
||||
return set
|
||||
}()
|
||||
private let timecodeDelimiterSet: CharacterSet = {
|
||||
var set = CharacterSet.punctuationCharacters
|
||||
set.formUnion(CharacterSet.whitespacesAndNewlines)
|
||||
set.remove(":")
|
||||
return set
|
||||
}()
|
||||
private let validTimecodeSet: CharacterSet = {
|
||||
var set = CharacterSet(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
|
||||
set.insert(":")
|
||||
return set
|
||||
}()
|
||||
|
||||
struct ApplicationSpecificEntityType {
|
||||
public static let Timecode: Int32 = 1
|
||||
}
|
||||
|
||||
private enum CurrentEntityType {
|
||||
case command
|
||||
case mention
|
||||
case hashtag
|
||||
case phoneNumber
|
||||
case timecode
|
||||
|
||||
var type: EnabledEntityTypes {
|
||||
switch self {
|
||||
@ -41,6 +58,10 @@ private enum CurrentEntityType {
|
||||
return .mention
|
||||
case .hashtag:
|
||||
return .hashtag
|
||||
case .phoneNumber:
|
||||
return .phoneNumber
|
||||
case .timecode:
|
||||
return .timecode
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,12 +78,13 @@ public struct EnabledEntityTypes: OptionSet {
|
||||
public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2)
|
||||
public static let url = EnabledEntityTypes(rawValue: 1 << 3)
|
||||
public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4)
|
||||
public static let external = EnabledEntityTypes(rawValue: 1 << 5)
|
||||
public static let timecode = EnabledEntityTypes(rawValue: 1 << 5)
|
||||
public static let external = EnabledEntityTypes(rawValue: 1 << 6)
|
||||
|
||||
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber]
|
||||
}
|
||||
|
||||
private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity]) {
|
||||
private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) {
|
||||
if !enabledTypes.contains(type.type) {
|
||||
return
|
||||
}
|
||||
@ -76,15 +98,26 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType,
|
||||
}
|
||||
if !overlaps {
|
||||
let entityType: MessageTextEntityType
|
||||
switch type {
|
||||
case .command:
|
||||
entityType = .BotCommand
|
||||
case .mention:
|
||||
entityType = .Mention
|
||||
case .hashtag:
|
||||
entityType = .Hashtag
|
||||
switch type {
|
||||
case .command:
|
||||
entityType = .BotCommand
|
||||
case .mention:
|
||||
entityType = .Mention
|
||||
case .hashtag:
|
||||
entityType = .Hashtag
|
||||
case .phoneNumber:
|
||||
entityType = .PhoneNumber
|
||||
case .timecode:
|
||||
entityType = .Custom(type: ApplicationSpecificEntityType.Timecode)
|
||||
}
|
||||
|
||||
if case .timecode = type, let mediaDuration = mediaDuration, let timecode = parseTimecodeString(String(utf16[range])) {
|
||||
if timecode <= mediaDuration {
|
||||
entities.append(MessageTextEntity(range: indexRange, type: entityType))
|
||||
}
|
||||
} else {
|
||||
entities.append(MessageTextEntity(range: indexRange, type: entityType))
|
||||
}
|
||||
entities.append(MessageTextEntity(range: indexRange, type: entityType))
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,6 +235,8 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
}
|
||||
currentEntity = nil
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -216,23 +251,34 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
return entities
|
||||
}
|
||||
|
||||
func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityTypes, entities: [MessageTextEntity]) -> [MessageTextEntity]? {
|
||||
func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityTypes, entities: [MessageTextEntity], mediaDuration: Double? = nil) -> [MessageTextEntity]? {
|
||||
var resultEntities = entities
|
||||
|
||||
var hasDigits = false
|
||||
if enabledTypes.contains(.phoneNumber) {
|
||||
var hasColons = false
|
||||
|
||||
let detectPhoneNumbers = enabledTypes.contains(.phoneNumber)
|
||||
let detectTimecodes = enabledTypes.contains(.timecode)
|
||||
if detectPhoneNumbers || detectTimecodes {
|
||||
loop: for c in text.utf16 {
|
||||
if let scalar = UnicodeScalar(c) {
|
||||
if scalar >= "0" && scalar <= "9" {
|
||||
hasDigits = true
|
||||
break loop
|
||||
if !detectTimecodes || hasColons {
|
||||
break loop
|
||||
}
|
||||
} else if scalar == ":" {
|
||||
hasColons = true
|
||||
if !detectPhoneNumbers || hasDigits {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasDigits {
|
||||
if let phoneNumberDetector = phoneNumberDetector, enabledTypes.contains(.phoneNumber) {
|
||||
if let phoneNumberDetector = phoneNumberDetector, detectPhoneNumbers {
|
||||
let utf16 = text.utf16
|
||||
phoneNumberDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in
|
||||
if let result = result {
|
||||
@ -240,22 +286,55 @@ func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text)
|
||||
let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text)
|
||||
if let lowerBound = lowerBound, let upperBound = upperBound {
|
||||
let indexRange: Range<Int> = utf16.distance(from: text.startIndex, to: lowerBound) ..< utf16.distance(from: text.startIndex, to: upperBound)
|
||||
var overlaps = false
|
||||
for entity in resultEntities {
|
||||
if entity.range.overlaps(indexRange) {
|
||||
overlaps = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !overlaps {
|
||||
resultEntities.append(MessageTextEntity(range: indexRange, type: .PhoneNumber))
|
||||
}
|
||||
commitEntity(utf16, .phoneNumber, lowerBound ..< upperBound, enabledTypes, &resultEntities)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if hasColons && detectTimecodes {
|
||||
let utf16 = text.utf16
|
||||
let delimiterSet = timecodeDelimiterSet
|
||||
|
||||
var index = utf16.startIndex
|
||||
var currentEntity: (CurrentEntityType, Range<String.UTF16View.Index>)?
|
||||
|
||||
var previousScalar: UnicodeScalar?
|
||||
while index != utf16.endIndex {
|
||||
let c = utf16[index]
|
||||
let scalar = UnicodeScalar(c)
|
||||
var notFound = true
|
||||
if let scalar = scalar {
|
||||
if validTimecodeSet.contains(scalar) {
|
||||
notFound = false
|
||||
if let (type, range) = currentEntity, type == .timecode {
|
||||
currentEntity = (.timecode, range.lowerBound ..< utf16.index(after: index))
|
||||
} else if previousScalar == nil || CharacterSet.whitespacesAndNewlines.contains(previousScalar!) {
|
||||
currentEntity = (.timecode, index ..< index)
|
||||
}
|
||||
}
|
||||
|
||||
if notFound {
|
||||
if let (type, range) = currentEntity {
|
||||
switch type {
|
||||
case .timecode:
|
||||
if delimiterSet.contains(scalar) {
|
||||
commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration)
|
||||
currentEntity = nil
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
index = utf16.index(after: index)
|
||||
previousScalar = scalar
|
||||
}
|
||||
if let (type, range) = currentEntity {
|
||||
commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resultEntities.count != entities.count {
|
||||
@ -264,3 +343,25 @@ func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseTimecodeString(_ string: String?) -> Double? {
|
||||
if let string = string, string.rangeOfCharacter(from: validTimecodeSet.inverted) == nil {
|
||||
let components = string.components(separatedBy: ":")
|
||||
if components.count > 1 && components.count <= 3 {
|
||||
if components.count == 3 {
|
||||
if let hours = Int(components[0]), let minutes = Int(components[1]), let seconds = Int(components[2]) {
|
||||
if hours >= 0 && hours < 48 && minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 {
|
||||
return Double(seconds) + Double(minutes) * 60.0 + Double(hours) * 60.0 * 60.0
|
||||
}
|
||||
}
|
||||
} else if components.count == 2 {
|
||||
if let minutes = Int(components[0]), let seconds = Int(components[1]) {
|
||||
if minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 {
|
||||
return Double(seconds) + Double(minutes) * 60.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ final class InstantPageController: ViewController {
|
||||
}
|
||||
|
||||
private var webpageDisposable: Disposable?
|
||||
private var storedStateDisposable: Disposable?
|
||||
|
||||
private var settings: InstantPagePresentationSettings?
|
||||
private var settingsDisposable: Disposable?
|
||||
@ -48,7 +49,7 @@ final class InstantPageController: ViewController {
|
||||
}
|
||||
})
|
||||
|
||||
self.settingsDisposable = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.instantPagePresentationSettings, ApplicationSpecificSharedDataKeys.presentationThemeSettings])
|
||||
self.settingsDisposable = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.instantPagePresentationSettings, ApplicationSpecificSharedDataKeys.presentationThemeSettings])
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
|
||||
if let strongSelf = self {
|
||||
let settings: InstantPagePresentationSettings
|
||||
@ -79,6 +80,7 @@ final class InstantPageController: ViewController {
|
||||
|
||||
deinit {
|
||||
self.webpageDisposable?.dispose()
|
||||
self.storedStateDisposable?.dispose()
|
||||
self.settingsDisposable?.dispose()
|
||||
}
|
||||
|
||||
@ -109,7 +111,7 @@ final class InstantPageController: ViewController {
|
||||
}
|
||||
})
|
||||
|
||||
let _ = (instantPageStoredState(postbox: self.context.account.postbox, webPage: self.webPage)
|
||||
self.storedStateDisposable = (instantPageStoredState(postbox: self.context.account.postbox, webPage: self.webPage)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updateWebPage(strongSelf.webPage, anchor: strongSelf.anchor, state: state)
|
||||
|
||||
@ -203,6 +203,10 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.updateNavigationBar()
|
||||
|
||||
self.recursivelyEnsureDisplaySynchronously(true)
|
||||
|
||||
if let layout = self.containerLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -359,7 +363,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if didSetScrollOffset {
|
||||
self.previousContentOffset = contentOffset
|
||||
self.updateNavigationBar()
|
||||
self.setupScrollOffsetOnLayout = false
|
||||
if self.currentLayout != nil {
|
||||
self.setupScrollOffsetOnLayout = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldUpdateVisibleItems {
|
||||
|
||||
@ -163,7 +163,7 @@ class InstantPageGalleryController: ViewController {
|
||||
private var innerOpenUrl: (InstantPageUrlItem) -> Void
|
||||
private var openUrlOptions: (InstantPageUrlItem) -> Void
|
||||
|
||||
init(context: AccountContext, webPage: TelegramMediaWebpage, message: Message? = nil, entries: [InstantPageGalleryEntry], centralIndex: Int, fromPlayingVideo: Bool = false, landscape: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?) {
|
||||
init(context: AccountContext, webPage: TelegramMediaWebpage, message: Message? = nil, entries: [InstantPageGalleryEntry], centralIndex: Int, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?) {
|
||||
self.context = context
|
||||
self.webPage = webPage
|
||||
self.message = message
|
||||
|
||||
@ -3,30 +3,37 @@ import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
class PermissionInfoItem: ListViewItem {
|
||||
enum InfoListItemText {
|
||||
case plain(String)
|
||||
case markdown(String)
|
||||
}
|
||||
|
||||
enum InfoListItemLinkAction {
|
||||
case tap(String)
|
||||
}
|
||||
|
||||
class InfoListItem: ListViewItem {
|
||||
let selectable: Bool = false
|
||||
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let subject: DeviceAccessSubject
|
||||
let type: AccessType
|
||||
let title: String
|
||||
let text: InfoListItemText
|
||||
let style: ItemListStyle
|
||||
let suppressed: Bool
|
||||
let close: () -> Void
|
||||
let linkAction: ((InfoListItemLinkAction) -> Void)?
|
||||
let closeAction: (() -> Void)?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, subject: DeviceAccessSubject, type: AccessType, style: ItemListStyle, suppressed: Bool, close: @escaping () -> Void) {
|
||||
init(theme: PresentationTheme, title: String, text: InfoListItemText, style: ItemListStyle, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.subject = subject
|
||||
self.type = type
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.style = style
|
||||
self.suppressed = suppressed
|
||||
self.close = close
|
||||
self.linkAction = linkAction
|
||||
self.closeAction = closeAction
|
||||
}
|
||||
|
||||
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) {
|
||||
async {
|
||||
let node = PermissionInfoItemNode()
|
||||
let node = InfoItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, nil)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
@ -42,7 +49,7 @@ class PermissionInfoItem: ListViewItem {
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? PermissionInfoItemNode {
|
||||
if let nodeValue = node() as? InfoItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
@ -58,17 +65,17 @@ class PermissionInfoItem: ListViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem {
|
||||
class ItemListInfoItem: InfoListItem, ItemListItem {
|
||||
let sectionId: ItemListSectionId
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, subject: DeviceAccessSubject, type: AccessType, style: ItemListStyle, sectionId: ItemListSectionId, suppressed: Bool, close: @escaping () -> Void) {
|
||||
init(theme: PresentationTheme, title: String, text: InfoListItemText, style: ItemListStyle, sectionId: ItemListSectionId, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) {
|
||||
self.sectionId = sectionId
|
||||
super.init(theme: theme, strings: strings, subject: subject, type: type, style: style, suppressed: suppressed, close: close)
|
||||
super.init(theme: theme, title: title, text: text, style: style, linkAction: linkAction, closeAction: closeAction)
|
||||
}
|
||||
|
||||
override 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) {
|
||||
async {
|
||||
let node = PermissionInfoItemNode()
|
||||
let node = InfoItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
@ -84,7 +91,7 @@ class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem {
|
||||
|
||||
override func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? PermissionInfoItemNode {
|
||||
if let nodeValue = node() as? InfoItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
@ -102,20 +109,24 @@ class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem {
|
||||
|
||||
private let titleFont = Font.semibold(17.0)
|
||||
private let textFont = Font.regular(16.0)
|
||||
private let textBoldFont = Font.semibold(16.0)
|
||||
private let badgeFont = Font.regular(15.0)
|
||||
|
||||
class PermissionInfoItemNode: ListViewItemNode {
|
||||
class InfoItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let closeButton: HighlightableButtonNode
|
||||
|
||||
let badgeNode: ASImageNode
|
||||
let labelNode: TextNode
|
||||
let titleNode: TextNode
|
||||
let textNode: TextNode
|
||||
private let badgeNode: ASImageNode
|
||||
private let labelNode: TextNode
|
||||
private let titleNode: TextNode
|
||||
private let textNode: TextNode
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
private var item: PermissionInfoItem?
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: InfoListItem?
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
return false
|
||||
@ -146,6 +157,9 @@ class PermissionInfoItemNode: ListViewItemNode {
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
self.activateArea.accessibilityTraits = UIAccessibilityTraitStaticText
|
||||
|
||||
self.closeButton = HighlightableButtonNode()
|
||||
self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0)
|
||||
self.closeButton.displaysAsynchronously = false
|
||||
@ -156,12 +170,28 @@ class PermissionInfoItemNode: ListViewItemNode {
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
self.addSubnode(self.closeButton)
|
||||
|
||||
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PermissionInfoItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { _ in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
recognizer.highlight = { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateTouchesAtPoint(point)
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: InfoListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
@ -204,34 +234,30 @@ class PermissionInfoItemNode: ListViewItemNode {
|
||||
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
|
||||
}
|
||||
|
||||
let title: String
|
||||
let text: String
|
||||
switch item.subject {
|
||||
case .contacts:
|
||||
title = item.strings.Contacts_PermissionsTitle
|
||||
text = item.strings.Contacts_PermissionsText
|
||||
case .notifications:
|
||||
switch item.type {
|
||||
case .unreachable:
|
||||
title = item.strings.Notifications_PermissionsUnreachableTitle
|
||||
text = item.strings.Notifications_PermissionsUnreachableText
|
||||
default:
|
||||
title = item.strings.Notifications_PermissionsTitle
|
||||
text = item.strings.Notifications_PermissionsText
|
||||
}
|
||||
default:
|
||||
title = ""
|
||||
text = ""
|
||||
let attributedText: NSAttributedString
|
||||
switch item.text {
|
||||
case let .plain(text):
|
||||
attributedText = NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
case let .markdown(text):
|
||||
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}))
|
||||
}
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 36.0)
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.accessibilityLabel = "\(item.title)\n\(attributedText.string)"
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = strongSelf.accessibilityLabel
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
@ -272,14 +298,7 @@ class PermissionInfoItemNode: ListViewItemNode {
|
||||
bottomStripeInset = leftInset
|
||||
}
|
||||
|
||||
if let item = strongSelf.item {
|
||||
switch (item.subject, item.type, item.suppressed) {
|
||||
case (.contacts, _, false), (.notifications, .unreachable, _):
|
||||
strongSelf.closeButton.isHidden = false
|
||||
default:
|
||||
strongSelf.closeButton.isHidden = true
|
||||
}
|
||||
}
|
||||
strongSelf.closeButton.isHidden = item.closeAction == nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
@ -324,7 +343,72 @@ class PermissionInfoItemNode: ListViewItemNode {
|
||||
|
||||
@objc func closeButtonPressed() {
|
||||
if let item = self.item {
|
||||
item.close()
|
||||
item.closeAction?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
let titleFrame = self.textNode.frame
|
||||
if let item = self.item, titleFrame.contains(location) {
|
||||
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
item.linkAction?(.tap(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTouchesAtPoint(_ point: CGPoint?) {
|
||||
if let item = self.item {
|
||||
var rects: [CGRect]?
|
||||
if let point = point {
|
||||
let textNodeFrame = self.textNode.frame
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
let possibleNames: [String] = [
|
||||
TelegramTextAttributes.URL,
|
||||
TelegramTextAttributes.PeerMention,
|
||||
TelegramTextAttributes.PeerTextMention,
|
||||
TelegramTextAttributes.BotCommand,
|
||||
TelegramTextAttributes.Hashtag
|
||||
]
|
||||
for name in possibleNames {
|
||||
if let _ = attributes[NSAttributedStringKey(rawValue: name)] {
|
||||
rects = self.textNode.attributeRects(name: name, at: index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rects = rects {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5))
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
|
||||
}
|
||||
linkHighlightingNode.frame = self.textNode.frame
|
||||
linkHighlightingNode.updateRects(rects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -543,7 +543,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
case .tap, .longTap:
|
||||
if let item = self.item, let url = self.urlAtPoint(location) {
|
||||
if case .longTap = gesture {
|
||||
item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url))
|
||||
item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message.id)
|
||||
} else if url == self.currentPrimaryUrl {
|
||||
if !item.controllerInteraction.openMessage(item.message, .default) {
|
||||
item.controllerInteraction.openUrl(url, false, false)
|
||||
|
||||
@ -407,7 +407,7 @@ public final class MediaManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType) {
|
||||
func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction = .playback(.play)) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
let inputData: Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings)?, NoError>
|
||||
if let (account, playlist) = playlist {
|
||||
@ -444,7 +444,7 @@ public final class MediaManager: NSObject {
|
||||
strongSelf.voiceMediaPlayer = nil
|
||||
}
|
||||
}
|
||||
voiceMediaPlayer.control(.playback(.play))
|
||||
voiceMediaPlayer.control(control)
|
||||
} else {
|
||||
strongSelf.voiceMediaPlayer = nil
|
||||
}
|
||||
@ -460,7 +460,7 @@ public final class MediaManager: NSObject {
|
||||
strongSelf.musicMediaPlayer = nil
|
||||
}
|
||||
}
|
||||
strongSelf.musicMediaPlayer?.control(.playback(.play))
|
||||
strongSelf.musicMediaPlayer?.control(control)
|
||||
} else {
|
||||
strongSelf.musicMediaPlayer = nil
|
||||
}
|
||||
|
||||
@ -55,10 +55,11 @@ enum MediaPlayerPlayOnceWithSoundActionAtEnd {
|
||||
case repeatIfNeeded
|
||||
}
|
||||
|
||||
enum MediaPlayerPlayOnceWithSoundSeek {
|
||||
enum MediaPlayerSeek {
|
||||
case none
|
||||
case start
|
||||
case automatic
|
||||
case timecode(Double)
|
||||
}
|
||||
|
||||
enum MediaPlayerStreaming {
|
||||
@ -483,7 +484,7 @@ private final class MediaPlayerContext {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek = .start) {
|
||||
fileprivate func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if !self.enableSound {
|
||||
@ -506,9 +507,12 @@ private final class MediaPlayerContext {
|
||||
}
|
||||
|
||||
var timestamp: Double
|
||||
if let loadedState = loadedState, seekToStart == .none {
|
||||
if case let .timecode(time) = seek {
|
||||
timestamp = time
|
||||
}
|
||||
else if let loadedState = loadedState, case .none = seek {
|
||||
timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
||||
if let duration = currentDuration() {
|
||||
if let duration = self.currentDuration() {
|
||||
if timestamp > duration - 2.0 {
|
||||
timestamp = 0.0
|
||||
}
|
||||
@ -518,7 +522,10 @@ private final class MediaPlayerContext {
|
||||
}
|
||||
self.seek(timestamp: timestamp, action: .play)
|
||||
} else {
|
||||
if case .playing = self.state {
|
||||
if case let .timecode(time) = seek {
|
||||
self.seek(timestamp: Double(time), action: .play)
|
||||
}
|
||||
else if case .playing = self.state {
|
||||
} else {
|
||||
self.play()
|
||||
}
|
||||
@ -551,7 +558,7 @@ private final class MediaPlayerContext {
|
||||
self.playAndRecord = false
|
||||
|
||||
var timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
||||
if let duration = currentDuration(), timestamp > duration - 2.0 {
|
||||
if let duration = self.currentDuration(), timestamp > duration - 2.0 {
|
||||
timestamp = 0.0
|
||||
}
|
||||
self.seek(timestamp: timestamp, action: .play)
|
||||
@ -644,7 +651,7 @@ private final class MediaPlayerContext {
|
||||
}
|
||||
case .paused:
|
||||
if !self.enableSound {
|
||||
self.playOnceWithSound(playAndRecord: false, seekToStart: .none)
|
||||
self.playOnceWithSound(playAndRecord: false, seek: .none)
|
||||
} else {
|
||||
self.play()
|
||||
}
|
||||
@ -969,10 +976,10 @@ final class MediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek = .start) {
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.playOnceWithSound(playAndRecord: playAndRecord, seekToStart: seekToStart)
|
||||
context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1025,10 +1032,14 @@ final class MediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
func seek(timestamp: Double) {
|
||||
func seek(timestamp: Double, play: Bool? = nil) {
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.seek(timestamp: timestamp)
|
||||
if let play = play {
|
||||
context.seek(timestamp: timestamp, action: play ? .play : .pause)
|
||||
} else {
|
||||
context.seek(timestamp: timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,7 +261,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
self.player.seek(timestamp: timestamp)
|
||||
}
|
||||
|
||||
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
let action = { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
@ -295,7 +295,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
})
|
||||
}
|
||||
|
||||
self.player.playOnceWithSound(playAndRecord: playAndRecord, seekToStart: seekToStart)
|
||||
self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
|
||||
}
|
||||
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||
|
||||
@ -121,7 +121,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
|
||||
case allAccounts(PresentationTheme, String, Bool)
|
||||
case accountsInfo(PresentationTheme, String)
|
||||
|
||||
case permissionInfo(PresentationTheme, PresentationStrings, AccessType)
|
||||
case permissionInfo(PresentationTheme, String, String, Bool)
|
||||
case permissionEnable(PresentationTheme, String)
|
||||
|
||||
case messageHeader(PresentationTheme, String)
|
||||
@ -336,8 +336,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .permissionInfo(lhsTheme, lhsStrings, lhsAccessType):
|
||||
if case let .permissionInfo(rhsTheme, rhsStrings, rhsAccessType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsAccessType == rhsAccessType {
|
||||
case let .permissionInfo(lhsTheme, lhsTitle, lhsText, lhsSuppressed):
|
||||
if case let .permissionInfo(rhsTheme, rhsTitle, rhsText, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSuppressed == rhsSuppressed {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -569,8 +569,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
|
||||
}, tag: self.tag)
|
||||
case let .accountsInfo(theme, text):
|
||||
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
|
||||
case let .permissionInfo(theme, strings, type):
|
||||
return PermissionInfoItemListItem(theme: theme, strings: strings, subject: .notifications, type: type, style: .blocks, sectionId: self.section, suppressed: false, close: {
|
||||
case let .permissionInfo(theme, title, text, suppressed):
|
||||
return ItemListInfoItem(theme: theme, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: suppressed ? nil : {
|
||||
arguments.suppressWarning()
|
||||
})
|
||||
case let .permissionEnable(theme, text):
|
||||
@ -725,15 +725,25 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn
|
||||
}
|
||||
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
let title: String
|
||||
let text: String
|
||||
if case .unreachable = authorizationStatus {
|
||||
title = presentationData.strings.Notifications_PermissionsUnreachableTitle
|
||||
text = presentationData.strings.Notifications_PermissionsUnreachableText
|
||||
} else {
|
||||
title = presentationData.strings.Notifications_PermissionsTitle
|
||||
text = presentationData.strings.Notifications_PermissionsText
|
||||
}
|
||||
|
||||
switch (authorizationStatus, warningSuppressed) {
|
||||
case (.denied, _):
|
||||
entries.append(.permissionInfo(presentationData.theme, presentationData.strings, authorizationStatus))
|
||||
entries.append(.permissionInfo(presentationData.theme, title, text, true))
|
||||
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllowInSettings))
|
||||
case (.unreachable, false):
|
||||
entries.append(.permissionInfo(presentationData.theme, presentationData.strings, authorizationStatus))
|
||||
entries.append(.permissionInfo(presentationData.theme, title, text, false))
|
||||
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsOpenSettings))
|
||||
case (.notDetermined, _):
|
||||
entries.append(.permissionInfo(presentationData.theme, presentationData.strings, authorizationStatus))
|
||||
entries.append(.permissionInfo(presentationData.theme, title, text, true))
|
||||
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllow))
|
||||
default:
|
||||
break
|
||||
|
||||
@ -70,18 +70,22 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
|
||||
}
|
||||
|
||||
var stream = false
|
||||
var fromPlayingVideo = false
|
||||
var autoplayingVideo = false
|
||||
var landscape = false
|
||||
var timecode: Double? = nil
|
||||
|
||||
if case .stream = mode {
|
||||
stream = true
|
||||
}
|
||||
if case .automaticPlayback = mode {
|
||||
fromPlayingVideo = true
|
||||
}
|
||||
if case .landscape = mode {
|
||||
fromPlayingVideo = true
|
||||
landscape = true
|
||||
switch mode {
|
||||
case .stream:
|
||||
stream = true
|
||||
case .automaticPlayback:
|
||||
autoplayingVideo = true
|
||||
case .landscape:
|
||||
autoplayingVideo = true
|
||||
landscape = true
|
||||
case let .timecode(time):
|
||||
timecode = time
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let (webPage, instantPageMedia) = instantPageMedia, let galleryMedia = galleryMedia {
|
||||
@ -93,7 +97,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
|
||||
}
|
||||
}
|
||||
|
||||
let gallery = InstantPageGalleryController(context: context, webPage: webPage, message: message, entries: instantPageMedia, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, landscape: landscape, replaceRootController: { [weak navigationController] controller, ready in
|
||||
let gallery = InstantPageGalleryController(context: context, webPage: webPage, message: message, entries: instantPageMedia, centralIndex: centralIndex, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, replaceRootController: { [weak navigationController] controller, ready in
|
||||
if let navigationController = navigationController {
|
||||
navigationController.replaceTopController(controller, animated: false, ready: ready)
|
||||
}
|
||||
@ -124,7 +128,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
|
||||
}
|
||||
#if DEBUG
|
||||
if ext == "mkv" {
|
||||
let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: fromPlayingVideo, landscape: landscape, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
navigationController?.replaceTopController(controller, animated: false, ready: ready)
|
||||
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
|
||||
return .gallery(gallery)
|
||||
@ -141,10 +145,10 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
|
||||
let gallery = SecretMediaPreviewController(context: context, messageId: message.id)
|
||||
return .secretGallery(gallery)
|
||||
} else {
|
||||
let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: fromPlayingVideo, landscape: landscape, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
navigationController?.replaceTopController(controller, animated: false, ready: ready)
|
||||
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
|
||||
gallery.temporaryDoNotWaitForReady = fromPlayingVideo
|
||||
gallery.temporaryDoNotWaitForReady = autoplayingVideo
|
||||
return .gallery(gallery)
|
||||
}
|
||||
}
|
||||
@ -251,6 +255,10 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
|
||||
case let .audio(file):
|
||||
let location: PeerMessagesPlaylistLocation
|
||||
let playerType: MediaManagerPlayerType
|
||||
var control = SharedMediaPlayerControlAction.playback(.play)
|
||||
if case let .timecode(time) = mode {
|
||||
control = .seek(time)
|
||||
}
|
||||
if (file.isVoice || file.isInstantVideo) && message.tags.contains(.voiceOrInstantVideo) {
|
||||
if standalone {
|
||||
location = .recentActions(message)
|
||||
@ -273,7 +281,7 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
|
||||
}
|
||||
playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music
|
||||
}
|
||||
context.sharedContext.mediaManager.setPlaylist((context.account, PeerMessagesMediaPlaylist(postbox: context.account.postbox, network: context.account.network, location: location)), type: playerType)
|
||||
context.sharedContext.mediaManager.setPlaylist((context.account, PeerMessagesMediaPlaylist(postbox: context.account.postbox, network: context.account.network, location: location)), type: playerType, control: control)
|
||||
return true
|
||||
case let .gallery(gallery):
|
||||
dismissInput()
|
||||
@ -312,45 +320,6 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
|
||||
}
|
||||
let controller = deviceContactInfoController(context: context, subject: .vcard(peer, nil, contactData))
|
||||
navigationController?.pushViewController(controller)
|
||||
|
||||
guard let peer = peer else {
|
||||
return
|
||||
}
|
||||
|
||||
/*let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
let controller = ActionSheetController(presentationTheme: presentationData.theme)
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
if let peerId = contact.peerId {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_SendMessage, action: {
|
||||
dismissAction()
|
||||
|
||||
openPeer(peer, .chat(textInputState: nil, messageId: nil))
|
||||
}))
|
||||
if let isContact = isContact, !isContact {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_AddContact, action: {
|
||||
dismissAction()
|
||||
let _ = addContactPeerInteractively(account: account, peerId: peerId, phone: contact.phoneNumber).start()
|
||||
}))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: {
|
||||
dismissAction()
|
||||
callPeer(peerId)
|
||||
}))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: {
|
||||
dismissAction()
|
||||
account.telegramApplicationcontext.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(contact.phoneNumber).replacingOccurrences(of: " ", with: ""))")
|
||||
}))
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
dismissInput()
|
||||
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))*/
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in
|
||||
}, callPeer: { _ in
|
||||
}, longTap: { _ in
|
||||
}, longTap: { _, _ in
|
||||
}, openCheckoutOrReceipt: { _ in
|
||||
}, openSearch: {
|
||||
}, setupReply: { _ in
|
||||
@ -73,6 +73,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec
|
||||
}, requestSelectMessagePollOption: { _, _ in
|
||||
}, openAppStorePage: {
|
||||
}, displayMessageTooltip: { _, _, _, _ in
|
||||
}, seekToTimecode: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
||||
@ -205,7 +205,7 @@ public class PeerMediaCollectionController: TelegramController {
|
||||
}, navigationController: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in
|
||||
}, longTap: { [weak self] content in
|
||||
}, longTap: { [weak self] content, _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.view.endEditing(true)
|
||||
switch content {
|
||||
@ -254,6 +254,7 @@ public class PeerMediaCollectionController: TelegramController {
|
||||
}, requestSelectMessagePollOption: { _, _ in
|
||||
}, openAppStorePage: {
|
||||
}, displayMessageTooltip: { _, _, _, _ in
|
||||
}, seekToTimecode: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
||||
@ -305,7 +305,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
||||
self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
|
||||
}
|
||||
|
||||
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
}
|
||||
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -43,17 +43,20 @@ private struct SettingsItemArguments {
|
||||
let openPassport: () -> Void
|
||||
let openWatch: () -> Void
|
||||
let openSupport: () -> Void
|
||||
let openFaq: () -> Void
|
||||
let openFaq: (String?) -> Void
|
||||
let openEditing: () -> Void
|
||||
let displayCopyContextMenu: () -> Void
|
||||
let switchToAccount: (AccountRecordId) -> Void
|
||||
let addAccount: () -> Void
|
||||
let setAccountIdWithRevealedOptions: (AccountRecordId?, AccountRecordId?) -> Void
|
||||
let removeAccount: (AccountRecordId) -> Void
|
||||
let keepPhone: () -> Void
|
||||
let openPhoneNumberChange: () -> Void
|
||||
}
|
||||
|
||||
private enum SettingsSection: Int32 {
|
||||
case info
|
||||
case phone
|
||||
case accounts
|
||||
case proxy
|
||||
case media
|
||||
@ -67,6 +70,10 @@ private enum SettingsEntry: ItemListNodeEntry {
|
||||
case setProfilePhoto(PresentationTheme, String)
|
||||
case setUsername(PresentationTheme, String)
|
||||
|
||||
case phoneInfo(PresentationTheme, String, String)
|
||||
case keepPhone(PresentationTheme, String)
|
||||
case changePhone(PresentationTheme, String)
|
||||
|
||||
case account(Int, Account, PresentationTheme, PresentationStrings, Peer, Int32, Bool)
|
||||
case addAccount(PresentationTheme, String)
|
||||
|
||||
@ -91,6 +98,8 @@ private enum SettingsEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case .userInfo, .setProfilePhoto, .setUsername:
|
||||
return SettingsSection.info.rawValue
|
||||
case .phoneInfo, .keepPhone, .changePhone:
|
||||
return SettingsSection.phone.rawValue
|
||||
case .account, .addAccount:
|
||||
return SettingsSection.accounts.rawValue
|
||||
case .proxy:
|
||||
@ -114,8 +123,14 @@ private enum SettingsEntry: ItemListNodeEntry {
|
||||
return 1
|
||||
case .setUsername:
|
||||
return 2
|
||||
case .phoneInfo:
|
||||
return 3
|
||||
case .keepPhone:
|
||||
return 4
|
||||
case .changePhone:
|
||||
return 5
|
||||
case let .account(account):
|
||||
return 3 + Int32(account.0)
|
||||
return 6 + Int32(account.0)
|
||||
case .addAccount:
|
||||
return 1002
|
||||
case .proxy:
|
||||
@ -193,6 +208,24 @@ private enum SettingsEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .phoneInfo(lhsTheme, lhsTitle, lhsText):
|
||||
if case let .phoneInfo(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .keepPhone(lhsTheme, lhsText):
|
||||
if case let .keepPhone(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .changePhone(lhsTheme, lhsText):
|
||||
if case let .changePhone(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .addAccount(lhsTheme, lhsText):
|
||||
if case let .addAccount(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
@ -315,6 +348,20 @@ private enum SettingsEntry: ItemListNodeEntry {
|
||||
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
|
||||
arguments.openUsername()
|
||||
})
|
||||
case let .phoneInfo(theme, title, text):
|
||||
return ItemListInfoItem(theme: theme, title: title, text: .markdown(text), style: .blocks, sectionId: self.section, linkAction: { action in
|
||||
if case .tap = action {
|
||||
arguments.openFaq("q-i-have-a-new-phone-number-what-do-i-do")
|
||||
}
|
||||
}, closeAction: nil)
|
||||
case let .keepPhone(theme, text):
|
||||
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
|
||||
arguments.keepPhone()
|
||||
})
|
||||
case let .changePhone(theme, text):
|
||||
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
|
||||
arguments.openPhoneNumberChange()
|
||||
})
|
||||
case let .account(_, account, theme, strings, peer, badgeCount, revealed):
|
||||
var label: ItemListPeerItemLabel = .none
|
||||
if badgeCount > 0 {
|
||||
@ -393,7 +440,7 @@ private enum SettingsEntry: ItemListNodeEntry {
|
||||
})
|
||||
case let .faq(theme, image, text):
|
||||
return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
|
||||
arguments.openFaq()
|
||||
arguments.openFaq(nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -405,7 +452,7 @@ private struct SettingsState: Equatable {
|
||||
var isSearching: Bool
|
||||
}
|
||||
|
||||
private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, privacySettings: AccountPrivacySettings?, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)], inAppNotificationSettings: InAppNotificationSettings) -> [SettingsEntry] {
|
||||
private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, privacySettings: AccountPrivacySettings?, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)], inAppNotificationSettings: InAppNotificationSettings, displayPhoneNumberConfirmation: Bool) -> [SettingsEntry] {
|
||||
var entries: [SettingsEntry] = []
|
||||
|
||||
if let peer = peerViewMainPeer(view) as? TelegramUser {
|
||||
@ -418,6 +465,13 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
|
||||
entries.append(.setUsername(presentationData.theme, presentationData.strings.Settings_SetUsername))
|
||||
}
|
||||
|
||||
if displayPhoneNumberConfirmation {
|
||||
let phoneNumber = formatPhoneNumber(peer.phone ?? "")
|
||||
entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).0, presentationData.strings.Settings_CheckPhoneNumberText))
|
||||
entries.append(.keepPhone(presentationData.theme, presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0))
|
||||
entries.append(.changePhone(presentationData.theme, presentationData.strings.Settings_ChangePhoneNumber))
|
||||
}
|
||||
|
||||
if !accountsAndPeers.isEmpty {
|
||||
var index = 0
|
||||
for (peerAccount, peer, badgeCount) in accountsAndPeers {
|
||||
@ -532,17 +586,6 @@ private final class SettingsControllerImpl: ItemListController<SettingsEntry>, S
|
||||
}
|
||||
|
||||
func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) {
|
||||
guard let switchController = switchController else {
|
||||
return
|
||||
}
|
||||
/*switch update {
|
||||
case .dismiss:
|
||||
switchController.dismiss()
|
||||
case .present:
|
||||
switchController.finishPresentation()
|
||||
case let .update(progress):
|
||||
switchController.update(progress)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -594,7 +637,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
|
||||
let privacySettings = Promise<AccountPrivacySettings?>(nil)
|
||||
|
||||
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
|
||||
let openFaq: (Promise<ResolvedUrl>, String?) -> Void = { resolvedUrl, customAnchor in
|
||||
let _ = (contextValue.get()
|
||||
|> deliverOnMainQueue
|
||||
|> take(1)).start(next: { context in
|
||||
@ -605,7 +648,11 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in
|
||||
controller?.dismiss()
|
||||
|
||||
|
||||
var resolvedUrl = resolvedUrl
|
||||
if case let .instantView(webPage, _) = resolvedUrl, let customAnchor = customAnchor {
|
||||
resolvedUrl = .instantView(webPage, customAnchor)
|
||||
}
|
||||
openResolvedUrl(resolvedUrl, context: context, navigationController: getNavigationControllerImpl?(), openPeer: { peer, navigation in
|
||||
}, present: { controller, arguments in
|
||||
pushControllerImpl?(controller)
|
||||
@ -622,6 +669,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
|
||||
var switchToAccountImpl: ((AccountRecordId) -> Void)?
|
||||
|
||||
let displayPhoneNumberConfirmation = ValuePromise<Bool>(false)
|
||||
|
||||
let arguments = SettingsItemArguments(accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: {
|
||||
var updating = false
|
||||
updateState {
|
||||
@ -744,7 +793,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: {
|
||||
openFaq(resolvedUrlPromise)
|
||||
openFaq(resolvedUrlPromise, nil)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||
supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in
|
||||
if let peerId = peerId {
|
||||
@ -753,11 +802,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
}))
|
||||
})]), nil)
|
||||
})
|
||||
}, openFaq: {
|
||||
}, openFaq: { anchor in
|
||||
let resolvedUrlPromise = Promise<ResolvedUrl>()
|
||||
resolvedUrlPromise.set(resolvedUrl)
|
||||
|
||||
openFaq(resolvedUrlPromise)
|
||||
openFaq(resolvedUrlPromise, anchor)
|
||||
}, openEditing: {
|
||||
let _ = (contextValue.get()
|
||||
|> deliverOnMainQueue
|
||||
@ -820,6 +868,19 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
])
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
})
|
||||
}, keepPhone: {
|
||||
displayPhoneNumberConfirmation.set(false)
|
||||
}, openPhoneNumberChange: {
|
||||
let _ = (contextValue.get()
|
||||
|> deliverOnMainQueue
|
||||
|> take(1)).start(next: { context in
|
||||
let _ = (context.account.postbox.transaction { transaction -> String in
|
||||
return (transaction.getPeer(context.account.peerId) as? TelegramUser)?.phone ?? ""
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { phoneNumber in
|
||||
pushControllerImpl?(ChangePhoneNumberIntroController(context: context, phoneNumber: formatPhoneNumber(phoneNumber)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
changeProfilePhotoImpl = {
|
||||
@ -1050,7 +1111,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
return context.account.viewTracker.featuredStickerPacks()
|
||||
}
|
||||
|
||||
let signal = combineLatest(queue: Queue.mainQueue(), contextValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), preferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), privacySettings.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasPassport.get(), hasWatchApp.get()), accountsAndPeers.get())
|
||||
let signal = combineLatest(queue: Queue.mainQueue(), contextValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), preferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), privacySettings.get(), displayPhoneNumberConfirmation.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasPassport.get(), hasWatchApp.get()), accountsAndPeers.get())
|
||||
|> map { context, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasPassportAndWatch, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState<SettingsEntry>, SettingsEntry.ItemGenerationArguments)) in
|
||||
let proxySettings: ProxySettings = preferencesAndExceptions.0.entries[SharedDataKeys.proxySettings] as? ProxySettings ?? ProxySettings.defaultSettings
|
||||
let inAppNotificationSettings: InAppNotificationSettings = preferencesAndExceptions.0.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings
|
||||
@ -1080,15 +1141,15 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
if value {
|
||||
setDisplayNavigationBarImpl?(false)
|
||||
}
|
||||
}, presentController: { v, a in
|
||||
}, presentController: { c, a in
|
||||
dismissInputImpl?()
|
||||
presentControllerImpl?(v, a)
|
||||
}, pushController: { v in
|
||||
pushControllerImpl?(v)
|
||||
presentControllerImpl?(c, a)
|
||||
}, pushController: { c in
|
||||
pushControllerImpl?(c)
|
||||
}, getNavigationController: getNavigationControllerImpl, exceptionsList: notifyExceptions.get(), archivedStickerPacks: archivedPacks.get(), privacySettings: privacySettings.get())
|
||||
|
||||
let (hasPassport, hasWatchApp) = hasPassportAndWatch
|
||||
let listState = ItemListNodeState(entries: settingsEntries(account: context.account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, privacySettings: preferencesAndExceptions.4, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers.1, inAppNotificationSettings: inAppNotificationSettings), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up))
|
||||
let listState = ItemListNodeState(entries: settingsEntries(account: context.account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, privacySettings: preferencesAndExceptions.4, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers.1, inAppNotificationSettings: inAppNotificationSettings, displayPhoneNumberConfirmation: preferencesAndExceptions.5), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up))
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
@ -672,13 +672,21 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode {
|
||||
if let strongSelf = self {
|
||||
switch mode {
|
||||
case .push:
|
||||
strongSelf.pushController(controller)
|
||||
if let controller = controller {
|
||||
strongSelf.pushController(controller)
|
||||
}
|
||||
case .modal:
|
||||
strongSelf.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in
|
||||
self?.cancel()
|
||||
}))
|
||||
if let controller = controller {
|
||||
strongSelf.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in
|
||||
self?.cancel()
|
||||
}))
|
||||
}
|
||||
case .immediate:
|
||||
strongSelf.presentController(controller, nil)
|
||||
if let controller = controller {
|
||||
strongSelf.presentController(controller, nil)
|
||||
}
|
||||
case .dismiss:
|
||||
strongSelf.cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -138,6 +138,7 @@ enum SettingsSearchableItemPresentation {
|
||||
case push
|
||||
case modal
|
||||
case immediate
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct SettingsSearchableItem {
|
||||
@ -146,7 +147,7 @@ struct SettingsSearchableItem {
|
||||
let alternate: [String]
|
||||
let icon: SettingsSearchableItemIcon
|
||||
let breadcrumbs: [String]
|
||||
let present: (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void
|
||||
let present: (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void
|
||||
}
|
||||
|
||||
private func synonyms(_ string: String?) -> [String] {
|
||||
@ -161,7 +162,7 @@ private func profileSearchableItems(context: AccountContext, canAddAccount: Bool
|
||||
let icon: SettingsSearchableItemIcon = .profile
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentProfileSettings: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController) -> Void, EditSettingsEntryTag?) -> Void = { context, present, itemTag in
|
||||
let presentProfileSettings: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, EditSettingsEntryTag?) -> Void = { context, present, itemTag in
|
||||
let _ = openEditSettings(context: context, accountsAndPeers: activeAccountsAndPeers(context: context), focusOnItemTag: itemTag, presentController: { controller, _ in
|
||||
present(.immediate, controller)
|
||||
}, pushController: { controller in
|
||||
@ -211,7 +212,7 @@ private func callSearchableItems(context: AccountContext) -> [SettingsSearchable
|
||||
let icon: SettingsSearchableItemIcon = .calls
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentCallSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, present in
|
||||
let presentCallSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in
|
||||
present(.push, CallListController(context: context, mode: .navigation))
|
||||
}
|
||||
|
||||
@ -229,7 +230,7 @@ private func stickerSearchableItems(context: AccountContext, archivedStickerPack
|
||||
let icon: SettingsSearchableItemIcon = .stickers
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentStickerSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, InstalledStickerPacksEntryTag?) -> Void = { context, present, itemTag in
|
||||
let presentStickerSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, InstalledStickerPacksEntryTag?) -> Void = { context, present, itemTag in
|
||||
present(.push, installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedStickerPacks, updatedPacks: { _ in }, focusOnItemTag: itemTag))
|
||||
}
|
||||
|
||||
@ -259,7 +260,7 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
|
||||
let icon: SettingsSearchableItemIcon = .notifications
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentNotificationSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, NotificationsAndSoundsEntryTag?) -> Void = { context, present, itemTag in
|
||||
let presentNotificationSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, NotificationsAndSoundsEntryTag?) -> Void = { context, present, itemTag in
|
||||
present(.push, notificationsAndSoundsController(context: context, exceptionsList: exceptionsList, focusOnItemTag: itemTag))
|
||||
}
|
||||
|
||||
@ -269,40 +270,39 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
|
||||
var channels:[PeerId : NotificationExceptionWrapper] = [:]
|
||||
if let list = exceptionsList {
|
||||
for (key, value) in list.settings {
|
||||
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
|
||||
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
|
||||
switch value.muteState {
|
||||
case .default:
|
||||
switch value.messageSound {
|
||||
case .default:
|
||||
break
|
||||
switch value.messageSound {
|
||||
case .default:
|
||||
break
|
||||
default:
|
||||
switch key.namespace {
|
||||
case Namespaces.Peer.CloudUser:
|
||||
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
default:
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
} else {
|
||||
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
switch key.namespace {
|
||||
case Namespaces.Peer.CloudUser:
|
||||
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
default:
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
} else {
|
||||
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
}
|
||||
case Namespaces.Peer.CloudUser:
|
||||
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
default:
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
} else {
|
||||
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
switch key.namespace {
|
||||
case Namespaces.Peer.CloudUser:
|
||||
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
default:
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
} else {
|
||||
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (.users(users), .groups(groups), .channels(channels))
|
||||
}
|
||||
|
||||
@ -413,11 +413,11 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
|
||||
let icon: SettingsSearchableItemIcon = .privacy
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, PrivacyAndSecurityEntryTag?) -> Void = { context, present, itemTag in
|
||||
let presentPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, PrivacyAndSecurityEntryTag?) -> Void = { context, present, itemTag in
|
||||
present(.push, privacyAndSecurityController(context: context, focusOnItemTag: itemTag))
|
||||
}
|
||||
|
||||
let presentSelectivePrivacySettings: (AccountContext, SelectivePrivacySettingsKind, @escaping (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, kind, present in
|
||||
let presentSelectivePrivacySettings: (AccountContext, SelectivePrivacySettingsKind, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, kind, present in
|
||||
let privacySignal: Signal<AccountPrivacySettings, NoError>
|
||||
if let privacySettings = privacySettings {
|
||||
privacySignal = .single(privacySettings)
|
||||
@ -463,7 +463,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
|
||||
})
|
||||
}
|
||||
|
||||
let presentDataPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, present in
|
||||
let presentDataPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in
|
||||
present(.push, dataPrivacyController(context: context))
|
||||
}
|
||||
|
||||
@ -557,7 +557,7 @@ private func dataSearchableItems(context: AccountContext) -> [SettingsSearchable
|
||||
let icon: SettingsSearchableItemIcon = .data
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentDataSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, DataAndStorageEntryTag?) -> Void = { context, present, itemTag in
|
||||
let presentDataSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, DataAndStorageEntryTag?) -> Void = { context, present, itemTag in
|
||||
present(.push, dataAndStorageController(context: context, focusOnItemTag: itemTag))
|
||||
}
|
||||
|
||||
@ -611,7 +611,7 @@ private func proxySearchableItems(context: AccountContext, servers: [ProxyServer
|
||||
let icon: SettingsSearchableItemIcon = .proxy
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentProxySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, present in
|
||||
let presentProxySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in
|
||||
present(.push, proxySettingsController(context: context))
|
||||
}
|
||||
|
||||
@ -642,7 +642,7 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear
|
||||
let icon: SettingsSearchableItemIcon = .appearance
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let presentAppearanceSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, ThemeSettingsEntryTag?) -> Void = { context, present, itemTag in
|
||||
let presentAppearanceSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, ThemeSettingsEntryTag?) -> Void = { context, present, itemTag in
|
||||
present(.push, themeSettingsController(context: context, focusOnItemTag: itemTag))
|
||||
}
|
||||
|
||||
@ -676,6 +676,36 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear
|
||||
]
|
||||
}
|
||||
|
||||
private func languageSearchableItems(context: AccountContext, localizations: [LocalizationInfo]) -> [SettingsSearchableItem] {
|
||||
let icon: SettingsSearchableItemIcon = .language
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let applyLocalization: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, String) -> Void = { context, present, languageCode in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: nil))
|
||||
present(.immediate, controller)
|
||||
|
||||
let _ = (downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, languageCode: languageCode)
|
||||
|> deliverOnMainQueue).start(completed: { [weak controller] in
|
||||
controller?.dismiss()
|
||||
present(.dismiss, nil)
|
||||
})
|
||||
}
|
||||
|
||||
var items: [SettingsSearchableItem] = []
|
||||
items.append(SettingsSearchableItem(id: .language(0), title: strings.Settings_AppLanguage, alternate: synonyms(strings.SettingsSearch_Synonyms_AppLanguage), icon: icon, breadcrumbs: [], present: { context, _, present in
|
||||
present(.push, LocalizationListController(context: context))
|
||||
}))
|
||||
var index: Int32 = 1
|
||||
for localization in localizations {
|
||||
items.append(SettingsSearchableItem(id: .language(index), title: localization.localizedTitle, alternate: [localization.title], icon: icon, breadcrumbs: [strings.Settings_AppLanguage], present: { context, _, present in
|
||||
applyLocalization(context, present, localization.languageCode)
|
||||
}))
|
||||
index += 1
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>) -> Signal<[SettingsSearchableItem], NoError> {
|
||||
let watchAppInstalled = (context.watchManager?.watchAppInstalled ?? .single(false))
|
||||
|> take(1)
|
||||
@ -716,8 +746,40 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
|
||||
return settings.servers
|
||||
}
|
||||
|
||||
return combineLatest(watchAppInstalled, canAddAccount, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings)
|
||||
|> map { watchAppInstalled, canAddAccount, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings in
|
||||
let localizationPreferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState]))
|
||||
let localizations = context.account.postbox.combinedView(keys: [localizationPreferencesKey])
|
||||
|> map { view -> [LocalizationInfo] in
|
||||
if let localizationListState = (view.views[localizationPreferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState] as? LocalizationListState, !localizationListState.availableOfficialLocalizations.isEmpty {
|
||||
|
||||
var existingIds = Set<String>()
|
||||
let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) })
|
||||
|
||||
var localizationItems: [LocalizationInfo] = []
|
||||
if !availableSavedLocalizations.isEmpty {
|
||||
for info in availableSavedLocalizations {
|
||||
if existingIds.contains(info.languageCode) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(info.languageCode)
|
||||
localizationItems.append(info)
|
||||
}
|
||||
}
|
||||
for info in localizationListState.availableOfficialLocalizations {
|
||||
if existingIds.contains(info.languageCode) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(info.languageCode)
|
||||
localizationItems.append(info)
|
||||
}
|
||||
|
||||
return localizationItems
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings)
|
||||
|> map { watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings in
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
var allItems: [SettingsSearchableItem] = []
|
||||
@ -751,10 +813,8 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
|
||||
let appearanceItems = appearanceSearchableItems(context: context)
|
||||
allItems.append(contentsOf: appearanceItems)
|
||||
|
||||
let language = SettingsSearchableItem(id: .language(0), title: strings.Settings_AppLanguage, alternate: synonyms(strings.SettingsSearch_Synonyms_AppLanguage), icon: .language, breadcrumbs: [], present: { context, _, present in
|
||||
present(.push, LocalizationListController(context: context))
|
||||
})
|
||||
allItems.append(language)
|
||||
let languageItems = languageSearchableItems(context: context, localizations: localizations)
|
||||
allItems.append(contentsOf: languageItems)
|
||||
|
||||
if watchAppInstalled {
|
||||
let watch = SettingsSearchableItem(id: .watch(0), title: strings.Settings_AppleWatch, alternate: synonyms(strings.SettingsSearch_Synonyms_Watch), icon: .watch, breadcrumbs: [], present: { context, _, present in
|
||||
|
||||
@ -402,6 +402,7 @@ final class SharedMediaPlayer {
|
||||
private var playbackItem: SharedMediaPlaybackItem?
|
||||
private var currentPlayedToEnd = false
|
||||
private var scheduledPlaybackAction: SharedMediaPlayerPlaybackControlAction?
|
||||
private var scheduledStartTime: Double?
|
||||
|
||||
private let markItemAsPlayedDisposable = MetaDisposable()
|
||||
|
||||
@ -507,11 +508,19 @@ final class SharedMediaPlayer {
|
||||
|
||||
if let scheduledPlaybackAction = strongSelf.scheduledPlaybackAction {
|
||||
strongSelf.scheduledPlaybackAction = nil
|
||||
let scheduledStartTime = strongSelf.scheduledStartTime
|
||||
strongSelf.scheduledStartTime = nil
|
||||
|
||||
switch scheduledPlaybackAction {
|
||||
case .play:
|
||||
switch playbackItem {
|
||||
case let .audio(player):
|
||||
player.play()
|
||||
if let scheduledStartTime = scheduledStartTime {
|
||||
player.seek(timestamp: scheduledStartTime)
|
||||
player.play()
|
||||
} else {
|
||||
player.play()
|
||||
}
|
||||
case let .instantVideo(node):
|
||||
node.playOnceWithSound(playAndRecord: controlPlaybackWithProximity)
|
||||
}
|
||||
@ -654,6 +663,9 @@ final class SharedMediaPlayer {
|
||||
case let .seek(timestamp):
|
||||
if let playbackItem = self.playbackItem {
|
||||
playbackItem.seek(timestamp)
|
||||
} else {
|
||||
self.scheduledPlaybackAction = .play
|
||||
self.scheduledStartTime = timestamp
|
||||
}
|
||||
case let .setOrder(order):
|
||||
self.playlist.setOrder(order)
|
||||
|
||||
@ -164,6 +164,20 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba
|
||||
string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range)
|
||||
case .Code, .Pre:
|
||||
string.addAttribute(NSAttributedStringKey.font, value: fixedFont, range: range)
|
||||
case let .Custom(type):
|
||||
if type == ApplicationSpecificEntityType.Timecode {
|
||||
string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range)
|
||||
if underlineLinks && underlineAllLinks {
|
||||
string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range)
|
||||
}
|
||||
if nsString == nil {
|
||||
nsString = text as NSString
|
||||
}
|
||||
let text = nsString!.substring(with: range)
|
||||
if let time = parseTimecodeString(text) {
|
||||
string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode), value: TelegramTimecode(time: time, text: text), range: range)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -227,7 +227,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
//self.playerView.seek(toPosition: timestamp)
|
||||
}
|
||||
|
||||
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
}
|
||||
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||
|
||||
@ -23,10 +23,21 @@ final class TelegramPeerMention {
|
||||
}
|
||||
}
|
||||
|
||||
final class TelegramTimecode {
|
||||
let time: Double
|
||||
let text: String
|
||||
|
||||
init(time: Double, text: String) {
|
||||
self.time = time
|
||||
self.text = text
|
||||
}
|
||||
}
|
||||
|
||||
struct TelegramTextAttributes {
|
||||
static let URL = "UrlAttributeT"
|
||||
static let PeerMention = "TelegramPeerMention"
|
||||
static let PeerTextMention = "TelegramPeerTextMention"
|
||||
static let BotCommand = "TelegramBotCommand"
|
||||
static let Hashtag = "TelegramHashtag"
|
||||
static let Timecode = "TelegramTimecode"
|
||||
}
|
||||
|
||||
@ -23,11 +23,12 @@ class UniversalVideoGalleryItem: GalleryItem {
|
||||
let hideControls: Bool
|
||||
let fromPlayingVideo: Bool
|
||||
let landscape: Bool
|
||||
let timecode: Double?
|
||||
let playbackCompleted: () -> Void
|
||||
let performAction: (GalleryControllerInteractionTapAction) -> Void
|
||||
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) {
|
||||
init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.content = content
|
||||
@ -39,6 +40,7 @@ class UniversalVideoGalleryItem: GalleryItem {
|
||||
self.hideControls = hideControls
|
||||
self.fromPlayingVideo = fromPlayingVideo
|
||||
self.landscape = landscape
|
||||
self.timecode = timecode
|
||||
self.playbackCompleted = playbackCompleted
|
||||
self.performAction = performAction
|
||||
self.openActionOptions = openActionOptions
|
||||
@ -195,8 +197,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrubberView.seek = { [weak self] timestamp in
|
||||
self?.videoNode?.seek(timestamp)
|
||||
self.scrubberView.seek = { [weak self] timecode in
|
||||
self?.videoNode?.seek(timecode)
|
||||
}
|
||||
|
||||
self.statusButtonNode.addSubnode(self.statusNode)
|
||||
@ -612,19 +614,34 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func processAction(_ action: GalleryControllerItemNodeAction) {
|
||||
guard let videoNode = self.videoNode else {
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case let .timecode(timecode):
|
||||
videoNode.seek(timecode)
|
||||
}
|
||||
}
|
||||
|
||||
override func activateAsInitial() {
|
||||
if let videoNode = self.videoNode, self.isCentral {
|
||||
self.initiallyActivated = true
|
||||
|
||||
var isAnimated = false
|
||||
var seek = MediaPlayerSeek.start
|
||||
if let item = self.item, let content = item.content as? NativeVideoContent {
|
||||
isAnimated = content.fileReference.media.isAnimated
|
||||
if let time = item.timecode {
|
||||
seek = .timecode(time)
|
||||
}
|
||||
}
|
||||
if isAnimated {
|
||||
videoNode.seek(0.0)
|
||||
videoNode.play()
|
||||
} else {
|
||||
videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seek: seek, actionAtEnd: .stop)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -979,18 +996,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
if let fetchStatus = self.fetchStatus {
|
||||
switch fetchStatus {
|
||||
case .Local:
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop)
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
|
||||
case .Remote:
|
||||
if self.requiresDownload {
|
||||
self.fetchControls?.fetch()
|
||||
} else {
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop)
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
|
||||
}
|
||||
case .Fetching:
|
||||
self.fetchControls?.cancel()
|
||||
}
|
||||
} else {
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop)
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ protocol UniversalVideoContentNode: class {
|
||||
func togglePlayPause()
|
||||
func setSoundEnabled(_ value: Bool)
|
||||
func seek(_ timestamp: Double)
|
||||
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
|
||||
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
|
||||
@ -271,10 +271,10 @@ final class UniversalVideoNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
|
||||
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek = .start, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) {
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) {
|
||||
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||
if let contentNode = contentNode {
|
||||
contentNode.playOnceWithSound(playAndRecord: playAndRecord, seekToStart: seekToStart, actionAtEnd: actionAtEnd)
|
||||
contentNode.playOnceWithSound(playAndRecord: playAndRecord, seek: seek, actionAtEnd: actionAtEnd)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte
|
||||
self.playerNode.seek(timestamp: timestamp)
|
||||
}
|
||||
|
||||
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
}
|
||||
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user