mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge branch 'master' into 125layer
This commit is contained in:
commit
796c77b11d
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Set active Xcode path
|
||||
run: sudo xcode-select -s /Applications/Xcode_12.3.app/Contents/Developer
|
||||
run: sudo xcode-select -s /Applications/Xcode_12.4.app/Contents/Developer
|
||||
|
||||
- name: Create canonical source directory
|
||||
run: |
|
||||
@ -40,8 +40,8 @@ jobs:
|
||||
# download bazel
|
||||
mkdir -p $HOME/bazel-dist
|
||||
pushd $HOME/bazel-dist
|
||||
curl -O -L https://github.com/bazelbuild/bazel/releases/download/3.7.0/bazel-3.7.0-darwin-x86_64
|
||||
mv bazel-3.7.0* bazel
|
||||
curl -O -L https://github.com/bazelbuild/bazel/releases/download/4.0.0/bazel-4.0.0-darwin-x86_64
|
||||
mv bazel-4.0.0* bazel
|
||||
chmod +x bazel
|
||||
./bazel --version
|
||||
popd
|
||||
|
@ -6153,3 +6153,8 @@ Sorry for the inconvenience.";
|
||||
"Conversation.UsersTooMuchError" = "Sorry, this group is full.";
|
||||
|
||||
"Conversation.UploadFileTooLarge" = "File could not be sent, because it is larger than 2 GB.\n\nYou can send as many files as you like, but each must be smaller than 2 GB.";
|
||||
|
||||
"Channel.AddUserLeftError" = "Sorry, if a person is no longer part of a channel, you need to be in their Telegram contacts in order to add them back.\n\nNote that they can still join via the channel's invite link as long as they are not in the Removed Users list.";
|
||||
|
||||
"Message.ScamAccount" = "Scam";
|
||||
"Message.FakeAccount" = "Fake";
|
||||
|
@ -258,7 +258,7 @@ private final class ImportManager {
|
||||
if !pathExtension.isEmpty, let value = TGMimeTypeMap.mimeType(forExtension: pathExtension) {
|
||||
mimeType = value
|
||||
}
|
||||
return ChatHistoryImport.uploadMedia(account: account, session: session, file: tempFile, fileName: entry.0.path, mimeType: mimeType, type: entry.2)
|
||||
return ChatHistoryImport.uploadMedia(account: account, session: session, file: tempFile, disposeFileAfterDone: true, fileName: entry.0.path, mimeType: mimeType, type: entry.2)
|
||||
|> mapError { error -> ImportError in
|
||||
switch error {
|
||||
case .chatAdminRequired:
|
||||
|
@ -332,7 +332,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
||||
if case .search = source {
|
||||
if let _ = peer as? TelegramChannel {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_JoinChannel, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
var createSignal = context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peerId)
|
||||
var createSignal = context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peerId, hash: nil)
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
@ -1307,10 +1307,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _):
|
||||
if let peer = messages.last?.author {
|
||||
if peer.isScam {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isFake {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isVerified {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
|
||||
@ -1322,10 +1322,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
|
||||
if peer.isScam {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isFake {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isVerified {
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
|
||||
|
@ -95,10 +95,17 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
||||
if let navigationController = (contactsController?.navigationController as? NavigationController) {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), peekData: nil))
|
||||
}
|
||||
}, error: { _ in
|
||||
}, error: { error in
|
||||
if let contactsController = contactsController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
contactsController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = presentationData.strings.TwoStepAuth_FloodError
|
||||
default:
|
||||
text = presentationData.strings.Login_UnknownError
|
||||
}
|
||||
contactsController.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
@ -121,10 +128,17 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
||||
}
|
||||
|
||||
if canCall {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_Call, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let contactsController = contactsController {
|
||||
context.requestCall(peerId: peerId, isVideo: false, completion: {})
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_Call, icon: { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
context.requestCall(peerId: peerId, isVideo: false, completion: {})
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
if canVideoCall {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_VideoCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VideoCall"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
context.requestCall(peerId: peerId, isVideo: true, completion: {})
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
@ -663,12 +663,17 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
transition.updateAlpha(node: self.textNode, alpha: displayCaption ? 1.0 : 0.0)
|
||||
|
||||
self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
self.editButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - 50.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
|
||||
let deleteFrame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
var editFrame = CGRect(origin: CGPoint(x: width - 44.0 - 50.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
if self.deleteButton.isHidden {
|
||||
editFrame = deleteFrame
|
||||
}
|
||||
self.deleteButton.frame = deleteFrame
|
||||
self.editButton.frame = editFrame
|
||||
|
||||
if let image = self.backwardButton.image(for: .normal) {
|
||||
self.backwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) - 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
|
||||
|
||||
}
|
||||
if let image = self.forwardButton.image(for: .normal) {
|
||||
self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
|
||||
@ -911,15 +916,20 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
if let strongSelf = self, !messages.isEmpty {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
var generalMessageContentKind: MessageContentKind?
|
||||
var beganContentKindScanning = false
|
||||
var messageContentKinds = Set<MessageContentKindKey>()
|
||||
|
||||
for message in messages {
|
||||
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId)
|
||||
if generalMessageContentKind == nil || generalMessageContentKind == currentKind {
|
||||
generalMessageContentKind = currentKind
|
||||
} else {
|
||||
if beganContentKindScanning && currentKind != generalMessageContentKind {
|
||||
generalMessageContentKind = nil
|
||||
break
|
||||
} else if !beganContentKindScanning || currentKind == generalMessageContentKind {
|
||||
beganContentKindScanning = true
|
||||
generalMessageContentKind = currentKind
|
||||
}
|
||||
messageContentKinds.insert(currentKind.key)
|
||||
}
|
||||
|
||||
var preferredAction = ShareControllerPreferredAction.default
|
||||
if let generalMessageContentKind = generalMessageContentKind {
|
||||
switch generalMessageContentKind {
|
||||
@ -928,6 +938,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if messageContentKinds.count == 2 && messageContentKinds.contains(.image) && messageContentKinds.contains(.video) {
|
||||
preferredAction = .saveToCameraRoll
|
||||
}
|
||||
|
||||
if messages.count == 1 {
|
||||
|
@ -164,7 +164,11 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate
|
||||
self.centerScrollViewContents(transition: transition)
|
||||
self.ignoreZoom = false
|
||||
|
||||
let updatedZoomScale = self.scrollNode.view.zoomScale != self.scrollNode.view.minimumZoomScale
|
||||
self.scrollNode.view.zoomScale = self.scrollNode.view.minimumZoomScale
|
||||
if !updatedZoomScale {
|
||||
self.scrollViewDidZoom(self.scrollNode.view)
|
||||
}
|
||||
self.ignoreZoomTransition = nil
|
||||
}
|
||||
|
||||
|
@ -309,7 +309,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
@objc func joinPressed() {
|
||||
if let peer = self.peer, case .notJoined = self.joinState {
|
||||
self.updateJoinState(.inProgress)
|
||||
self.joinDisposable.set((joinChannel(account: self.context.account, peerId: peer.id) |> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
self.joinDisposable.set((joinChannel(account: self.context.account, peerId: peer.id, hash: nil) |> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if case .inProgress = strongSelf.joinState {
|
||||
strongSelf.updateJoinState(.notJoined)
|
||||
|
@ -30,11 +30,11 @@ private final class InviteLinkListControllerArguments {
|
||||
let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
|
||||
let createLink: () -> Void
|
||||
let openLink: (ExportedInvitation) -> Void
|
||||
let linkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
|
||||
let linkContextAction: (ExportedInvitation?, Bool, ASDisplayNode, ContextGesture?) -> Void
|
||||
let openAdmin: (ExportedInvitationCreator) -> Void
|
||||
let deleteAllRevokedLinks: () -> Void
|
||||
|
||||
init(context: AccountContext, shareMainLink: @escaping (ExportedInvitation) -> Void, openMainLink: @escaping (ExportedInvitation) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedInvitation?) -> Void, linkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, openAdmin: @escaping (ExportedInvitationCreator) -> Void, deleteAllRevokedLinks: @escaping () -> Void) {
|
||||
init(context: AccountContext, shareMainLink: @escaping (ExportedInvitation) -> Void, openMainLink: @escaping (ExportedInvitation) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedInvitation?) -> Void, linkContextAction: @escaping (ExportedInvitation?, Bool, ASDisplayNode, ContextGesture?) -> Void, openAdmin: @escaping (ExportedInvitationCreator) -> Void, deleteAllRevokedLinks: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.shareMainLink = shareMainLink
|
||||
self.openMainLink = openMainLink
|
||||
@ -65,7 +65,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
|
||||
case linksHeader(PresentationTheme, String)
|
||||
case linksCreate(PresentationTheme, String)
|
||||
case link(Int32, PresentationTheme, ExportedInvitation?, Int32?)
|
||||
case link(Int32, PresentationTheme, ExportedInvitation?, Bool, Int32?)
|
||||
case linksInfo(PresentationTheme, String)
|
||||
|
||||
case revokedLinksHeader(PresentationTheme, String)
|
||||
@ -104,7 +104,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
return 4
|
||||
case .linksCreate:
|
||||
return 5
|
||||
case let .link(index, _, _, _):
|
||||
case let .link(index, _, _, _, _):
|
||||
return 6 + index
|
||||
case .linksInfo:
|
||||
return 10000
|
||||
@ -159,8 +159,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .link(lhsIndex, lhsTheme, lhsLink, lhsTick):
|
||||
if case let .link(rhsIndex, rhsTheme, rhsLink, rhsTick) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLink == rhsLink, lhsTick == rhsTick {
|
||||
case let .link(lhsIndex, lhsTheme, lhsLink, lhsCanEdit, lhsTick):
|
||||
if case let .link(rhsIndex, rhsTheme, rhsLink, rhsCanEdit, rhsTick) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLink == rhsLink, lhsCanEdit == rhsCanEdit, lhsTick == rhsTick {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -239,11 +239,11 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: {
|
||||
arguments.createLink()
|
||||
})
|
||||
case let .link(_, _, invite, _):
|
||||
case let .link(_, _, invite, canEdit, _):
|
||||
return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in
|
||||
arguments.openLink(invite)
|
||||
} contextAction: { invite, node, gesture in
|
||||
arguments.linkContextAction(invite, node, gesture)
|
||||
arguments.linkContextAction(invite, canEdit, node, gesture)
|
||||
}
|
||||
case let .linksInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
@ -257,12 +257,12 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in
|
||||
arguments.openLink(invite)
|
||||
} contextAction: { invite, node, gesture in
|
||||
arguments.linkContextAction(invite, node, gesture)
|
||||
arguments.linkContextAction(invite, false, node, gesture)
|
||||
}
|
||||
case let .adminsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .admin(_, _, creator):
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: arguments.context, peer: creator.peer.peer!, height: .peerList, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .none, label: .disclosure("\(creator.count)"), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: arguments.context, peer: creator.peer.peer!, height: .peerList, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .none, label: creator.count > 1 ? .disclosure("\(creator.count)") : .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
|
||||
arguments.openAdmin(creator)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
|
||||
}
|
||||
@ -333,16 +333,21 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData,
|
||||
entries.append(.linksCreate(presentationData.theme, presentationData.strings.InviteLink_Create))
|
||||
}
|
||||
|
||||
var canEditLinks = true
|
||||
if let peer = admin?.peer.peer as? TelegramUser, peer.botInfo != nil {
|
||||
canEditLinks = false
|
||||
}
|
||||
|
||||
if let additionalInvites = additionalInvites {
|
||||
var index: Int32 = 0
|
||||
for invite in additionalInvites {
|
||||
entries.append(.link(index, presentationData.theme, invite, invite.expireDate != nil ? tick : nil))
|
||||
entries.append(.link(index, presentationData.theme, invite, canEditLinks, invite.expireDate != nil ? tick : nil))
|
||||
index += 1
|
||||
}
|
||||
} else if let admin = admin, admin.count > 1 {
|
||||
var index: Int32 = 0
|
||||
for _ in 0 ..< admin.count - 1 {
|
||||
entries.append(.link(index, presentationData.theme, nil, nil))
|
||||
entries.append(.link(index, presentationData.theme, nil, false, nil))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
@ -558,7 +563,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
|
||||
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: invitesContext, revokedInvitationsContext: revokedInvitesContext, importersContext: nil)
|
||||
pushControllerImpl?(controller)
|
||||
}
|
||||
}, linkContextAction: { invite, node, gesture in
|
||||
}, linkContextAction: { invite, canEdit, node, gesture in
|
||||
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?(), let invite = invite else {
|
||||
return
|
||||
}
|
||||
@ -615,7 +620,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
|
||||
})))
|
||||
}
|
||||
|
||||
if !invite.isPermanent {
|
||||
if !invite.isPermanent && canEdit {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextEdit, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
@ -687,8 +692,10 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
|
||||
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
|
||||
dismissAction()
|
||||
|
||||
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in
|
||||
if case let .replace(_, newInvite) = result {
|
||||
invitesContext.add(newInvite)
|
||||
}
|
||||
}))
|
||||
|
||||
invitesContext.remove(invite)
|
||||
|
@ -537,8 +537,10 @@ public final class InviteLinkViewController: ViewController {
|
||||
dismissAction()
|
||||
self?.controller?.dismiss()
|
||||
|
||||
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in
|
||||
if case let .replace(_, newInvite) = result {
|
||||
self?.controller?.invitationsContext?.add(newInvite)
|
||||
}
|
||||
})
|
||||
|
||||
self?.controller?.invitationsContext?.remove(invite)
|
||||
|
@ -374,10 +374,10 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo
|
||||
var credibilityIconOffset: CGFloat = 4.0
|
||||
if let peer = item.peer {
|
||||
if peer.isScam {
|
||||
credibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
|
||||
credibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||
credibilityIconOffset = 6.0
|
||||
} else if peer.isFake {
|
||||
credibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
|
||||
credibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||
credibilityIconOffset = 2.0
|
||||
} else if peer.isVerified {
|
||||
credibilityIconImage = PresentationResourcesItemList.verifiedPeerIcon(item.presentationData.theme)
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
#define GPUImageRotationSwapsWidthAndHeight(rotation) ((rotation) == kGPUImageRotateLeft || (rotation) == kGPUImageRotateRight || (rotation) == kGPUImageRotateRightFlipVertical || (rotation) == kGPUImageRotateRightFlipHorizontal)
|
||||
|
||||
typedef enum { kGPUImageNoRotation, kGPUImageRotateLeft, kGPUImageRotateRight, kGPUImageFlipVertical, kGPUImageFlipHorizonal, kGPUImageRotateRightFlipVertical, kGPUImageRotateRightFlipHorizontal, kGPUImageRotate180 } GPUImageRotationMode;
|
||||
typedef enum { kGPUImageNoRotation, kGPUImageRotateLeft, kGPUImageRotateRight, kGPUImageFlipVertical, kGPUImageFlipHorizonal, kGPUImageRotateRightFlipVertical, kGPUImageRotateRightFlipHorizontal, kGPUImageRotate180, kGPUImageRotate180FlipHorizontal } GPUImageRotationMode;
|
||||
|
||||
@interface GPUImageContext : NSObject
|
||||
|
||||
|
@ -283,6 +283,13 @@ NSString *const kGPUImagePassthroughFragmentShaderString = SHADER_STRING
|
||||
1.0f, 0.0f,
|
||||
0.0f, 0.0f,
|
||||
};
|
||||
|
||||
static const GLfloat rotate180HorizontalFlipTextureCoordinates[] = {
|
||||
0.0f, 1.0f,
|
||||
1.0f, 1.0f,
|
||||
0.0f, 0.0f,
|
||||
1.0f, 0.0f,
|
||||
};
|
||||
|
||||
switch(rotationMode)
|
||||
{
|
||||
@ -294,6 +301,7 @@ NSString *const kGPUImagePassthroughFragmentShaderString = SHADER_STRING
|
||||
case kGPUImageRotateRightFlipVertical: return rotateRightVerticalFlipTextureCoordinates;
|
||||
case kGPUImageRotateRightFlipHorizontal: return rotateRightHorizontalFlipTextureCoordinates;
|
||||
case kGPUImageRotate180: return rotate180TextureCoordinates;
|
||||
case kGPUImageRotate180FlipHorizontal: return rotate180HorizontalFlipTextureCoordinates;
|
||||
}
|
||||
}
|
||||
|
||||
@ -642,6 +650,11 @@ NSString *const kGPUImagePassthroughFragmentShaderString = SHADER_STRING
|
||||
rotatedPoint.x = 1.0f - pointToRotate.x;
|
||||
rotatedPoint.y = 1.0f - pointToRotate.y;
|
||||
}; break;
|
||||
case kGPUImageRotate180FlipHorizontal:
|
||||
{
|
||||
rotatedPoint.x = pointToRotate.x;
|
||||
rotatedPoint.y = 1.0f - pointToRotate.y;
|
||||
}; break;
|
||||
}
|
||||
|
||||
return rotatedPoint;
|
||||
|
@ -214,7 +214,7 @@
|
||||
_rotationMode = cropMirrored ? kGPUImageRotateRightFlipHorizontal : kGPUImageRotateRight;
|
||||
break;
|
||||
case UIImageOrientationDown:
|
||||
_rotationMode = kGPUImageRotate180;
|
||||
_rotationMode = cropMirrored ? kGPUImageRotate180FlipHorizontal : kGPUImageRotate180;
|
||||
break;
|
||||
case UIImageOrientationUp:
|
||||
if (cropMirrored)
|
||||
|
@ -285,6 +285,13 @@
|
||||
0.0f, 1.0f,
|
||||
};
|
||||
|
||||
static const GLfloat rotate180HorizontalFlipTextureCoordinates[] = {
|
||||
0.0f, 1.0f,
|
||||
1.0f, 1.0f,
|
||||
0.0f, 0.0f,
|
||||
1.0f, 0.0f,
|
||||
};
|
||||
|
||||
switch(rotationMode)
|
||||
{
|
||||
case kGPUImageNoRotation: return noRotationTextureCoordinates;
|
||||
@ -295,6 +302,7 @@
|
||||
case kGPUImageRotateRightFlipVertical: return rotateRightVerticalFlipTextureCoordinates;
|
||||
case kGPUImageRotateRightFlipHorizontal: return rotateRightHorizontalFlipTextureCoordinates;
|
||||
case kGPUImageRotate180: return rotate180TextureCoordinates;
|
||||
case kGPUImageRotate180FlipHorizontal: return rotate180HorizontalFlipTextureCoordinates;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,16 +118,20 @@ NSString *const kYUVVideoRangeConversionForLAFragmentShaderString = SHADER_STRIN
|
||||
|
||||
- (GPUImageRotationMode)rotationForTrack:(AVAsset *)asset {
|
||||
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
|
||||
CGAffineTransform trackTransform = [videoTrack preferredTransform];
|
||||
CGAffineTransform t = [videoTrack preferredTransform];
|
||||
|
||||
if (trackTransform.a == -1 && trackTransform.d == -1) {
|
||||
if (t.a == -1 && t.d == -1) {
|
||||
return kGPUImageRotate180;
|
||||
} else if (trackTransform.a == 1 && trackTransform.d == 1) {
|
||||
} else if (t.a == 1 && t.d == 1) {
|
||||
return kGPUImageNoRotation;
|
||||
} else if (trackTransform.b == -1 && trackTransform.c == 1) {
|
||||
} else if (t.b == -1 && t.c == 1) {
|
||||
return kGPUImageRotateLeft;
|
||||
} else if (t.a == -1 && t.d == 1) {
|
||||
return kGPUImageFlipHorizonal;
|
||||
} else if (t.a == 1 && t.d == -1) {
|
||||
return kGPUImageRotate180FlipHorizontal;
|
||||
} else {
|
||||
if (trackTransform.c == 1) {
|
||||
if (t.c == 1) {
|
||||
return kGPUImageRotateRightFlipVertical;
|
||||
} else {
|
||||
return kGPUImageRotateRight;
|
||||
|
@ -593,28 +593,30 @@ UIImageOrientation TGVideoOrientationForAsset(AVAsset *asset, bool *mirrored)
|
||||
{
|
||||
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
|
||||
CGAffineTransform t = videoTrack.preferredTransform;
|
||||
double videoRotation = atan2((float)t.b, (float)t.a);
|
||||
|
||||
if (mirrored != NULL)
|
||||
{
|
||||
CGFloat scaleX = sqrt(t.a * t.a + t.c * t.c);
|
||||
CGFloat scaleY = sqrt(t.b * t.b + t.d * t.d);
|
||||
CGSize scale = CGSizeMake(scaleX, scaleY);
|
||||
|
||||
*mirrored = (scale.width < 0);
|
||||
}
|
||||
|
||||
if (fabs(videoRotation - M_PI) < FLT_EPSILON) {
|
||||
if (t.a == -1 && t.d == -1) {
|
||||
return UIImageOrientationLeft;
|
||||
} else if (fabs(videoRotation - M_PI_2) < FLT_EPSILON) {
|
||||
if (t.c == 1 && mirrored != NULL) {
|
||||
} else if (t.a == 1 && t.d == 1) {
|
||||
return UIImageOrientationRight;
|
||||
} else if (t.b == -1 && t.c == 1) {
|
||||
return UIImageOrientationDown;
|
||||
} else if (t.a == -1 && t.d == 1) {
|
||||
if (mirrored != NULL) {
|
||||
*mirrored = true;
|
||||
}
|
||||
return UIImageOrientationLeft;
|
||||
} else if (t.a == 1 && t.d == -1) {
|
||||
if (mirrored != NULL) {
|
||||
*mirrored = true;
|
||||
}
|
||||
return UIImageOrientationUp;
|
||||
} else if (fabs(videoRotation + M_PI_2) < FLT_EPSILON) {
|
||||
return UIImageOrientationDown;
|
||||
} else {
|
||||
return UIImageOrientationRight;
|
||||
} else {
|
||||
if (t.c == 1) {
|
||||
if (mirrored != NULL) {
|
||||
*mirrored = true;
|
||||
}
|
||||
}
|
||||
return UIImageOrientationUp;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -696,8 +696,8 @@ static void addRoundedRectToPath(CGContextRef context, CGRect rect, CGFloat oval
|
||||
(id)UIColorRGB(0x80c864).CGColor, //green
|
||||
(id)UIColorRGB(0xfcde65).CGColor, //yellow
|
||||
(id)UIColorRGB(0xfc964d).CGColor, //orange
|
||||
(id)[UIColor blackColor].CGColor, //black
|
||||
(id)[UIColor whiteColor].CGColor //white
|
||||
(id)UIColorRGB(0x000000).CGColor, //black
|
||||
(id)UIColorRGB(0xffffff).CGColor //white
|
||||
];
|
||||
});
|
||||
return colors;
|
||||
|
@ -61,7 +61,7 @@ const CGFloat TGPhotoPaintSettingsPadPickerWidth = 360.0f;
|
||||
_eyedropperButton.exclusiveTouch = true;
|
||||
[_eyedropperButton setImage:TGTintedImage([UIImage imageNamed:@"Editor/Eyedropper"], [UIColor whiteColor]) forState:UIControlStateNormal];
|
||||
[_eyedropperButton addTarget:self action:@selector(eyedropperButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:_eyedropperButton];
|
||||
// [self addSubview:_eyedropperButton];
|
||||
|
||||
_settingsButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 44.0f, 44.0f)];
|
||||
_settingsButton.exclusiveTouch = true;
|
||||
@ -211,7 +211,8 @@ const CGFloat TGPhotoPaintSettingsPadPickerWidth = 360.0f;
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
CGFloat inset = 66.0f;
|
||||
CGFloat leftInset = 23.0f;
|
||||
CGFloat rightInset = 66.0f;
|
||||
CGFloat colorPickerHeight = 10.0f;
|
||||
if (self.frame.size.width > self.frame.size.height)
|
||||
{
|
||||
@ -222,14 +223,14 @@ const CGFloat TGPhotoPaintSettingsPadPickerWidth = 360.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
_colorPicker.frame = CGRectMake(inset, ceil((self.frame.size.height - colorPickerHeight) / 2.0f), self.frame.size.width - inset - inset, colorPickerHeight);
|
||||
_colorPicker.frame = CGRectMake(leftInset, ceil((self.frame.size.height - colorPickerHeight) / 2.0f), self.frame.size.width - leftInset - rightInset, colorPickerHeight);
|
||||
_eyedropperButton.frame = CGRectMake(10.0f, floor((self.frame.size.height - _eyedropperButton.frame.size.height) / 2.0f) + 1.0f, _eyedropperButton.frame.size.width, _eyedropperButton.frame.size.height);
|
||||
_settingsButton.frame = CGRectMake(self.frame.size.width - _settingsButton.frame.size.width - 10.0f, floor((self.frame.size.height - _settingsButton.frame.size.height) / 2.0f) + 1.0f, _settingsButton.frame.size.width, _settingsButton.frame.size.height);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_colorPicker.frame = CGRectMake(ceil((self.frame.size.width - colorPickerHeight) / 2.0f), inset, colorPickerHeight, self.frame.size.height - inset - inset);
|
||||
_colorPicker.frame = CGRectMake(ceil((self.frame.size.width - colorPickerHeight) / 2.0f), rightInset, colorPickerHeight, self.frame.size.height - leftInset - rightInset);
|
||||
_eyedropperButton.frame = CGRectMake(floor((self.frame.size.width - _eyedropperButton.frame.size.width) / 2.0f), self.frame.size.height - _eyedropperButton.frame.size.height - 10.0, _eyedropperButton.frame.size.width, _eyedropperButton.frame.size.height);
|
||||
_settingsButton.frame = CGRectMake(floor((self.frame.size.width - _settingsButton.frame.size.width) / 2.0f), 10.0f, _settingsButton.frame.size.width, _settingsButton.frame.size.height);
|
||||
}
|
||||
|
@ -187,7 +187,7 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f;
|
||||
_style = style;
|
||||
switch (_style) {
|
||||
case TGPhotoPaintTextEntityStyleRegular:
|
||||
_textView.layer.shadowColor = [[UIColor blackColor] CGColor];
|
||||
_textView.layer.shadowColor = [UIColorRGB(0x000000) CGColor];
|
||||
_textView.layer.shadowOffset = CGSizeMake(0.0f, 4.0f);
|
||||
_textView.layer.shadowOpacity = 0.4f;
|
||||
_textView.layer.shadowRadius = 4.0f;
|
||||
@ -218,7 +218,7 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f;
|
||||
|
||||
case TGPhotoPaintTextEntityStyleOutlined:
|
||||
{
|
||||
_textView.textColor = [UIColor whiteColor];
|
||||
_textView.textColor = UIColorRGB(0xffffff);
|
||||
_textView.strokeColor = _swatch.color;
|
||||
_textView.frameColor = nil;
|
||||
}
|
||||
@ -238,9 +238,9 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f;
|
||||
}
|
||||
|
||||
if (lightness > 0.87) {
|
||||
_textView.textColor = [UIColor blackColor];
|
||||
_textView.textColor = UIColorRGB(0x000000);
|
||||
} else {
|
||||
_textView.textColor = [UIColor whiteColor];
|
||||
_textView.textColor = UIColorRGB(0xffffff);
|
||||
}
|
||||
_textView.strokeColor = nil;
|
||||
_textView.frameColor = _swatch.color;
|
||||
@ -484,6 +484,7 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f;
|
||||
@implementation TGPhotoTextView
|
||||
{
|
||||
UIFont *_font;
|
||||
UIColor *_forcedTextColor;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
@ -564,6 +565,11 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f;
|
||||
self.layoutManager.textContainers.firstObject.lineFragmentPadding = floor(font.pointSize * 0.3);
|
||||
}
|
||||
|
||||
- (void)setTextColor:(UIColor *)textColor {
|
||||
_forcedTextColor = textColor;
|
||||
[super setTextColor:textColor];
|
||||
}
|
||||
|
||||
- (void)insertText:(NSString *)text {
|
||||
[self fixTypingAttributes];
|
||||
[super insertText:text];
|
||||
@ -577,9 +583,14 @@ const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f;
|
||||
}
|
||||
|
||||
- (void)fixTypingAttributes {
|
||||
NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
|
||||
if (_font != nil) {
|
||||
self.typingAttributes = @{NSFontAttributeName: _font};
|
||||
attributes[NSFontAttributeName] = _font;
|
||||
}
|
||||
if (_forcedTextColor != nil) {
|
||||
attributes[NSForegroundColorAttributeName] = _forcedTextColor;
|
||||
}
|
||||
self.typingAttributes = attributes;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -530,7 +530,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
|
||||
}
|
||||
}
|
||||
}
|
||||
if estimatedSize > 5 * 1024 * 1024 {
|
||||
if estimatedSize > 10 * 1024 * 1024 {
|
||||
fileAttributes.append(.hintFileIsLarge)
|
||||
}
|
||||
|
||||
|
@ -11,11 +11,29 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
|
||||
|
||||
private var delayedFrames: [MediaTrackFrame] = []
|
||||
|
||||
init(codecContext: FFMpegAVCodecContext) {
|
||||
init(codecContext: FFMpegAVCodecContext, sampleRate: Int = 44100, channelCount: Int = 2) {
|
||||
self.codecContext = codecContext
|
||||
self.audioFrame = FFMpegAVFrame()
|
||||
|
||||
self.swrContext = FFMpegSWResample(sourceChannelCount: Int(codecContext.channels()), sourceSampleRate: Int(codecContext.sampleRate()), sourceSampleFormat: codecContext.sampleFormat(), destinationChannelCount: 2, destinationSampleRate: 44100, destinationSampleFormat: FFMPEG_AV_SAMPLE_FMT_S16)
|
||||
self.swrContext = FFMpegSWResample(sourceChannelCount: Int(codecContext.channels()), sourceSampleRate: Int(codecContext.sampleRate()), sourceSampleFormat: codecContext.sampleFormat(), destinationChannelCount: channelCount, destinationSampleRate: sampleRate, destinationSampleFormat: FFMPEG_AV_SAMPLE_FMT_S16)
|
||||
}
|
||||
|
||||
func decodeRaw(frame: MediaTrackDecodableFrame) -> Data? {
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
if status == 0 {
|
||||
let result = self.codecContext.receive(into: self.audioFrame)
|
||||
if case .success = result {
|
||||
guard let data = self.swrContext.resample(self.audioFrame) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return data
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? {
|
||||
|
@ -23,4 +23,8 @@ public final class MediaTrackDecodableFrame {
|
||||
|
||||
self.packet = packet
|
||||
}
|
||||
|
||||
public func copyPacketData() -> Data {
|
||||
return Data(bytes: self.packet.data, count: Int(self.packet.size))
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +179,7 @@ public final class SoftwareVideoSource {
|
||||
if endOfStream {
|
||||
break
|
||||
} else {
|
||||
if let avFormatContext = self.avFormatContext, let videoStream = self.videoStream {
|
||||
if let _ = self.avFormatContext, let _ = self.videoStream {
|
||||
endOfStream = true
|
||||
break
|
||||
} else {
|
||||
@ -254,3 +254,197 @@ public final class SoftwareVideoSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class SoftwareAudioStream {
|
||||
let index: Int
|
||||
let fps: CMTime
|
||||
let timebase: CMTime
|
||||
let duration: CMTime
|
||||
let decoder: FFMpegAudioFrameDecoder
|
||||
|
||||
init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegAudioFrameDecoder) {
|
||||
self.index = index
|
||||
self.fps = fps
|
||||
self.timebase = timebase
|
||||
self.duration = duration
|
||||
self.decoder = decoder
|
||||
}
|
||||
}
|
||||
|
||||
public final class SoftwareAudioSource {
|
||||
private var readingError = false
|
||||
private var audioStream: SoftwareAudioStream?
|
||||
private var avIoContext: FFMpegAVIOContext?
|
||||
private var avFormatContext: FFMpegAVFormatContext?
|
||||
private let path: String
|
||||
fileprivate let fd: Int32?
|
||||
fileprivate let size: Int32
|
||||
|
||||
private var hasReadToEnd: Bool = false
|
||||
|
||||
public init(path: String) {
|
||||
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
|
||||
|
||||
var s = stat()
|
||||
stat(path, &s)
|
||||
self.size = Int32(s.st_size)
|
||||
|
||||
let fd = open(path, O_RDONLY, S_IRUSR)
|
||||
if fd >= 0 {
|
||||
self.fd = fd
|
||||
} else {
|
||||
self.fd = nil
|
||||
}
|
||||
|
||||
self.path = path
|
||||
|
||||
let avFormatContext = FFMpegAVFormatContext()
|
||||
|
||||
let ioBufferSize = 64 * 1024
|
||||
|
||||
let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback)
|
||||
self.avIoContext = avIoContext
|
||||
|
||||
avFormatContext.setIO(self.avIoContext!)
|
||||
|
||||
if !avFormatContext.openInput() {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
|
||||
if !avFormatContext.findStreamInfo() {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
|
||||
self.avFormatContext = avFormatContext
|
||||
|
||||
var audioStream: SoftwareAudioStream?
|
||||
|
||||
for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeAudio) {
|
||||
let streamIndex = streamIndexNumber.int32Value
|
||||
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
|
||||
continue
|
||||
}
|
||||
|
||||
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
|
||||
|
||||
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
|
||||
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
|
||||
|
||||
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
|
||||
|
||||
let codec = FFMpegAVCodec.find(forId: codecId)
|
||||
|
||||
if let codec = codec {
|
||||
let codecContext = FFMpegAVCodecContext(codec: codec)
|
||||
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
|
||||
if codecContext.open() {
|
||||
audioStream = SoftwareAudioStream(index: Int(streamIndex), fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext, sampleRate: 48000, channelCount: 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.audioStream = audioStream
|
||||
|
||||
if let audioStream = self.audioStream {
|
||||
avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: false)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let fd = self.fd {
|
||||
close(fd)
|
||||
}
|
||||
}
|
||||
|
||||
private func readPacketInternal() -> FFMpegPacket? {
|
||||
guard let avFormatContext = self.avFormatContext else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let packet = FFMpegPacket()
|
||||
if avFormatContext.readFrame(into: packet) {
|
||||
return packet
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func readDecodableFrame() -> (MediaTrackDecodableFrame?, Bool) {
|
||||
var frames: [MediaTrackDecodableFrame] = []
|
||||
var endOfStream = false
|
||||
|
||||
while !self.readingError && frames.isEmpty {
|
||||
if let packet = self.readPacketInternal() {
|
||||
if let audioStream = audioStream, Int(packet.streamIndex) == audioStream.index {
|
||||
let packetPts = packet.pts
|
||||
|
||||
let pts = CMTimeMake(value: packetPts, timescale: audioStream.timebase.timescale)
|
||||
let dts = CMTimeMake(value: packet.dts, timescale: audioStream.timebase.timescale)
|
||||
|
||||
let duration: CMTime
|
||||
|
||||
let frameDuration = packet.duration
|
||||
if frameDuration != 0 {
|
||||
duration = CMTimeMake(value: frameDuration * audioStream.timebase.value, timescale: audioStream.timebase.timescale)
|
||||
} else {
|
||||
duration = audioStream.fps
|
||||
}
|
||||
|
||||
let frame = MediaTrackDecodableFrame(type: .audio, packet: packet, pts: pts, dts: dts, duration: duration)
|
||||
frames.append(frame)
|
||||
}
|
||||
} else {
|
||||
if endOfStream {
|
||||
break
|
||||
} else {
|
||||
if let _ = self.avFormatContext, let _ = self.audioStream {
|
||||
endOfStream = true
|
||||
break
|
||||
} else {
|
||||
endOfStream = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (frames.first, endOfStream)
|
||||
}
|
||||
|
||||
public func readFrame() -> Data? {
|
||||
guard let audioStream = self.audioStream, let _ = self.avFormatContext else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let (decodableFrame, _) = self.readDecodableFrame()
|
||||
if let decodableFrame = decodableFrame {
|
||||
return audioStream.decoder.decodeRaw(frame: decodableFrame)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func readEncodedFrame() -> (Data, Int)? {
|
||||
guard let _ = self.audioStream, let _ = self.avFormatContext else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let (decodableFrame, _) = self.readDecodableFrame()
|
||||
if let decodableFrame = decodableFrame {
|
||||
return (decodableFrame.copyPacketData(), Int(decodableFrame.packet.duration - max(0, -decodableFrame.packet.pts)))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func seek(timestamp: Double) {
|
||||
if let stream = self.audioStream, let avFormatContext = self.avFormatContext {
|
||||
let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale)
|
||||
avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,21 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OggOpusFrame : NSObject
|
||||
|
||||
@property (nonatomic, readonly) int numSamples;
|
||||
@property (nonatomic, strong, readonly) NSData *data;
|
||||
|
||||
@end
|
||||
|
||||
@interface OggOpusReader : NSObject
|
||||
|
||||
- (instancetype _Nullable)initWithPath:(NSString *)path;
|
||||
|
||||
- (int32_t)read:(void *)pcmData bufSize:(int)bufSize;
|
||||
|
||||
+ (NSArray<OggOpusFrame *> * _Nullable)extractFrames:(NSData *)data;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
@ -2,6 +2,37 @@
|
||||
|
||||
#import "opusfile/opusfile.h"
|
||||
|
||||
static int is_opus(ogg_page *og) {
|
||||
ogg_stream_state os;
|
||||
ogg_packet op;
|
||||
|
||||
ogg_stream_init(&os, ogg_page_serialno(og));
|
||||
ogg_stream_pagein(&os, og);
|
||||
if (ogg_stream_packetout(&os, &op) == 1)
|
||||
{
|
||||
if (op.bytes >= 19 && !memcmp(op.packet, "OpusHead", 8))
|
||||
{
|
||||
ogg_stream_clear(&os);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
ogg_stream_clear(&os);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@implementation OggOpusFrame
|
||||
|
||||
- (instancetype)initWithNumSamples:(int)numSamples data:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_numSamples = numSamples;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface OggOpusReader () {
|
||||
OggOpusFile *_opusFile;
|
||||
}
|
||||
@ -32,4 +63,132 @@
|
||||
return op_read(_opusFile, pcmData, bufSize, NULL);
|
||||
}
|
||||
|
||||
+ (NSArray<OggOpusFrame *> * _Nullable)extractFrames:(NSData *)data {
|
||||
NSMutableArray *result = [[NSMutableArray alloc] init];
|
||||
|
||||
ogg_page opage;
|
||||
ogg_packet opacket;
|
||||
ogg_sync_state ostate;
|
||||
ogg_stream_state ostream;
|
||||
int sampleRate = 48000;
|
||||
|
||||
if (ogg_sync_init(&ostate) < 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
char *obuffer;
|
||||
long obufferSize = (long)data.length;
|
||||
|
||||
obuffer = ogg_sync_buffer(&ostate, obufferSize);
|
||||
if (!obuffer) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
memcpy(obuffer, data.bytes, data.length);
|
||||
// ogg_sync_wrote function is used to tell the ogg_sync_state struct how many bytes we wrote into the buffer.
|
||||
if (ogg_sync_wrote(&ostate, obufferSize) < 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int pages = 0;
|
||||
int packetsout = 0;
|
||||
int invalid = 0;
|
||||
int eos = 0;
|
||||
|
||||
int headers = 0;
|
||||
int serialno = 0;
|
||||
|
||||
/* LOOP START */
|
||||
while (ogg_sync_pageout(&ostate, &opage) == 1) {
|
||||
pages++;
|
||||
|
||||
if (headers == 0) {
|
||||
if (is_opus(&opage)) {
|
||||
/* this is the start of an Opus stream */
|
||||
serialno = ogg_page_serialno(&opage);
|
||||
if (ogg_stream_init(&ostream, ogg_page_serialno(&opage)) < 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
headers++;
|
||||
} else if (!ogg_page_bos(&opage)) {
|
||||
// We're past the header and haven't found an Opus stream.
|
||||
// Time to give up.
|
||||
break;
|
||||
} else {
|
||||
/* try again */
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
eos = ogg_page_eos(&opage);
|
||||
|
||||
/* submit the page for packetization */
|
||||
if (ogg_stream_pagein(&ostream, &opage) < 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
/* read and process available packets */
|
||||
while (ogg_stream_packetout(&ostream, &opacket) == 1) {
|
||||
|
||||
packetsout++;
|
||||
|
||||
int samples;
|
||||
/* skip header packets */
|
||||
if (headers == 1 && opacket.bytes >= 19 && !memcmp(opacket.packet, "OpusHead", 8)) {
|
||||
headers++;
|
||||
continue;
|
||||
}
|
||||
if (headers == 2 && opacket.bytes >= 16 && !memcmp(opacket.packet, "OpusTags", 8)) {
|
||||
headers++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* get packet duration */
|
||||
samples = opus_packet_get_nb_samples(opacket.packet, opacket.bytes, sampleRate);
|
||||
if (samples <= 0) {
|
||||
invalid++;
|
||||
continue; // skipping invalid packet
|
||||
}
|
||||
|
||||
[result addObject:[[OggOpusFrame alloc] initWithNumSamples:samples data:[NSData dataWithBytes:opacket.packet length:opacket.bytes]]];
|
||||
|
||||
/* update the rtp header and send */
|
||||
/*this->rtp.header_size = 12 + 4 * this->rtp.cc;
|
||||
this->rtp.seq++;
|
||||
this->rtp.time += samples;
|
||||
this->rtp.payload_size = opacket.bytes;
|
||||
|
||||
// Create RTP Packet
|
||||
unsigned char *packet;
|
||||
size_t packetSize = this->rtp.header_size + this->rtp.payload_size;
|
||||
packet = (unsigned char *)malloc(packetSize);
|
||||
if (!packet)
|
||||
throw Napi::Error::New(info.Env(), "Couldn't allocate packet buffer.");
|
||||
|
||||
// Serialize header and copy to packet. Then copy payload to packet.
|
||||
serialize_rtp_header(packet, this->rtp.header_size, &this->rtp);
|
||||
memcpy(packet + this->rtp.header_size, opacket.packet, opacket.bytes);
|
||||
|
||||
Napi::Buffer<unsigned char> output = Napi::Buffer<unsigned char>::Copy(env, reinterpret_cast<unsigned char *>(packet), packetSize);
|
||||
|
||||
push.Call(thisObj, {output});*/
|
||||
}
|
||||
|
||||
if (eos > 0) {
|
||||
// End of the logical bitstream, clear headers to reset.
|
||||
headers = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* CLEAN UP */
|
||||
if (eos > 0)
|
||||
{
|
||||
ogg_stream_clear(&ostream);
|
||||
ogg_sync_clear(&ostate);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -1049,11 +1049,20 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
case .tooMuchJoined:
|
||||
text = presentationData.strings.Group_ErrorSupergroupConversionNotPossible
|
||||
case .restricted:
|
||||
if let peer = adminView.peers[adminView.peerId] {
|
||||
text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0
|
||||
if let admin = adminView.peers[adminView.peerId] {
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(admin.compactDisplayTitle, admin.compactDisplayTitle).0
|
||||
case .group:
|
||||
text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0
|
||||
}
|
||||
}
|
||||
case .notMutualContact:
|
||||
text = presentationData.strings.GroupInfo_AddUserLeftError
|
||||
if case .broadcast = channel.info {
|
||||
text = presentationData.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = presentationData.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -1119,17 +1128,28 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
return current.withUpdatedUpdating(true)
|
||||
}
|
||||
updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { error in
|
||||
if case let .addMemberError(error) = error, let admin = adminView.peers[adminView.peerId] {
|
||||
if case .restricted = error {
|
||||
var text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(admin.compactDisplayTitle, admin.compactDisplayTitle).0
|
||||
if case .group = channel.info {
|
||||
text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
} else if case .tooMuchJoined = error {
|
||||
let text = presentationData.strings.Invite_ChannelsTooMuch
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
if case let .addMemberError(addMemberError) = error, let admin = adminView.peers[adminView.peerId] {
|
||||
var text = presentationData.strings.Login_UnknownError
|
||||
switch addMemberError {
|
||||
case .tooMuchJoined:
|
||||
text = presentationData.strings.Group_ErrorSupergroupConversionNotPossible
|
||||
case .restricted:
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(admin.compactDisplayTitle, admin.compactDisplayTitle).0
|
||||
case .group:
|
||||
text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0
|
||||
}
|
||||
case .notMutualContact:
|
||||
if case .broadcast = channel.info {
|
||||
text = presentationData.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = presentationData.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
} else if case .adminsTooMuch = error {
|
||||
let text: String
|
||||
if case .broadcast = channel.info {
|
||||
@ -1146,7 +1166,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if let group = channelView.peers[channelView.peerId] as? TelegramGroup {
|
||||
} else if let _ = channelView.peers[channelView.peerId] as? TelegramGroup {
|
||||
var updateFlags: TelegramChatAdminRightsFlags?
|
||||
var updateRank: String?
|
||||
updateState { current in
|
||||
@ -1163,12 +1183,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
}
|
||||
|
||||
let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific
|
||||
let defaultFlags: TelegramChatAdminRightsFlags
|
||||
if case .creator = group.role {
|
||||
defaultFlags = maskRightsFlags.subtracting(.canBeAnonymous)
|
||||
} else {
|
||||
defaultFlags = maskRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous)
|
||||
}
|
||||
let defaultFlags = maskRightsFlags.subtracting([.canBeAnonymous, .canAddAdmins])
|
||||
|
||||
if updateFlags == nil {
|
||||
updateFlags = defaultFlags
|
||||
|
@ -370,27 +370,30 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
||||
}
|
||||
}).start(error: { [weak contactsController] error in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = presentationData.strings.Channel_ErrorAddTooMuch
|
||||
case .tooMuchJoined:
|
||||
text = presentationData.strings.Invite_ChannelsTooMuch
|
||||
case .generic:
|
||||
text = presentationData.strings.Login_UnknownError
|
||||
case .restricted:
|
||||
text = presentationData.strings.Channel_ErrorAddBlocked
|
||||
case .notMutualContact:
|
||||
text = presentationData.strings.GroupInfo_AddUserLeftError
|
||||
case let .bot(memberId):
|
||||
let _ = (context.account.postbox.transaction { transaction in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
let _ = (context.account.postbox.transaction { transaction in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = presentationData.strings.Channel_ErrorAddTooMuch
|
||||
case .tooMuchJoined:
|
||||
text = presentationData.strings.Invite_ChannelsTooMuch
|
||||
case .generic:
|
||||
text = presentationData.strings.Login_UnknownError
|
||||
case .restricted:
|
||||
text = presentationData.strings.Channel_ErrorAddBlocked
|
||||
case .notMutualContact:
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
text = presentationData.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = presentationData.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
case let .bot(memberId):
|
||||
guard let peer = peer as? TelegramChannel else {
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
contactsController?.dismiss()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -407,15 +410,15 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
||||
}
|
||||
|
||||
contactsController?.dismiss()
|
||||
})
|
||||
return
|
||||
case .botDoesntSupportGroups:
|
||||
text = presentationData.strings.Channel_BotDoesntSupportGroups
|
||||
case .tooMuchBots:
|
||||
text = presentationData.strings.Channel_TooMuchBots
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
contactsController?.dismiss()
|
||||
return
|
||||
case .botDoesntSupportGroups:
|
||||
text = presentationData.strings.Channel_BotDoesntSupportGroups
|
||||
case .tooMuchBots:
|
||||
text = presentationData.strings.Channel_TooMuchBots
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
contactsController?.dismiss()
|
||||
})
|
||||
}))
|
||||
|
||||
presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
|
@ -1059,6 +1059,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let previousHadNamesToRevoke = Atomic<Bool?>(value: nil)
|
||||
let previousInvitation = Atomic<ExportedInvitation?>(value: nil)
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue)
|
||||
|> deliverOnMainQueue
|
||||
@ -1253,6 +1254,15 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
|
||||
}
|
||||
|
||||
var crossfade: Bool = false
|
||||
if let cachedData = view.cachedData as? CachedChannelData {
|
||||
let invitation = cachedData.exportedInvitation
|
||||
let previousInvitation = previousInvitation.swap(invitation)
|
||||
|
||||
if invitation != previousInvitation {
|
||||
crossfade = true
|
||||
}
|
||||
}
|
||||
|
||||
let hasNamesToRevoke = publicChannelsToRevoke != nil && !publicChannelsToRevoke!.isEmpty
|
||||
let hadNamesToRevoke = previousHadNamesToRevoke.swap(hasNamesToRevoke)
|
||||
if let peer = view.peers[view.peerId] as? TelegramChannel {
|
||||
@ -1273,7 +1283,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
|
||||
}
|
||||
}
|
||||
|
||||
if selectedType == .publicChannel, let hadNamesToRevoke = hadNamesToRevoke {
|
||||
if selectedType == .publicChannel, let hadNamesToRevoke = hadNamesToRevoke, !crossfade {
|
||||
crossfade = hadNamesToRevoke != hasNamesToRevoke
|
||||
}
|
||||
}
|
||||
|
@ -1420,10 +1420,17 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
||||
if let navigationController = (controller?.navigationController as? NavigationController) {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId)))
|
||||
}
|
||||
}, error: { [weak controller] _ in
|
||||
}, error: { [weak controller] error in
|
||||
if let controller = controller {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
controller.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = presentationData.strings.TwoStepAuth_FloodError
|
||||
default:
|
||||
text = presentationData.strings.Login_UnknownError
|
||||
}
|
||||
controller.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
})]), in: .window(.root))
|
||||
|
@ -100,6 +100,10 @@ public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaRe
|
||||
switch results[i].0 {
|
||||
case .image:
|
||||
if let data = results[i].1, data.count != 0 {
|
||||
if Int(fullRepresentationSize.width) > 100 && i <= 1 && !isLastSize {
|
||||
continue
|
||||
}
|
||||
|
||||
subscriber.putNext(Tuple4(nil, data, .full, isLastSize))
|
||||
foundData = true
|
||||
if isLastSize {
|
||||
@ -623,6 +627,11 @@ public func chatMessagePhotoInternal(photoData: Signal<Tuple4<Data?, Data?, Chat
|
||||
if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
|
||||
thumbnailImage = image
|
||||
}
|
||||
|
||||
if quality == .blurred && fullSizeImage != nil {
|
||||
thumbnailImage = fullSizeImage
|
||||
fullSizeImage = nil
|
||||
}
|
||||
|
||||
var blurredThumbnailImage: UIImage?
|
||||
if let thumbnailImage = thumbnailImage {
|
||||
|
@ -73,7 +73,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case optimizeDatabase(PresentationTheme)
|
||||
case photoPreview(PresentationTheme, Bool)
|
||||
case knockoutWallpaper(PresentationTheme, Bool)
|
||||
case alternativeFolderTabs(Bool)
|
||||
case demoAudioStream(Bool)
|
||||
case snapPinListToTop(Bool)
|
||||
case playerEmbedding(Bool)
|
||||
case playlistPlayback(Bool)
|
||||
@ -94,7 +94,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.logging.rawValue
|
||||
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .snapPinListToTop, .playerEmbedding, .playlistPlayback, .voiceConference:
|
||||
case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .demoAudioStream, .snapPinListToTop, .playerEmbedding, .playlistPlayback, .voiceConference:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .preferredVideoCodec:
|
||||
return DebugControllerSection.videoExperiments.rawValue
|
||||
@ -155,7 +155,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 22
|
||||
case .knockoutWallpaper:
|
||||
return 23
|
||||
case .alternativeFolderTabs:
|
||||
case .demoAudioStream:
|
||||
return 24
|
||||
case .snapPinListToTop:
|
||||
return 25
|
||||
@ -696,7 +696,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .knockoutWallpaper(theme, value):
|
||||
case let .knockoutWallpaper(_, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Knockout Wallpaper", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
@ -706,12 +706,12 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .alternativeFolderTabs(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Alternative Tabs", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
case let .demoAudioStream(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Demo Audio Stream", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings
|
||||
settings.foldersTabAtBottom = value
|
||||
settings.demoAudioStream = value
|
||||
return settings
|
||||
})
|
||||
}).start()
|
||||
@ -826,7 +826,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS
|
||||
entries.append(.optimizeDatabase(presentationData.theme))
|
||||
//entries.append(.photoPreview(presentationData.theme, experimentalSettings.chatListPhotos))
|
||||
entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper))
|
||||
entries.append(.alternativeFolderTabs(experimentalSettings.foldersTabAtBottom))
|
||||
entries.append(.demoAudioStream(experimentalSettings.demoAudioStream))
|
||||
entries.append(.snapPinListToTop(experimentalSettings.snapPinListToTop))
|
||||
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
|
||||
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
||||
|
@ -469,7 +469,7 @@ public final class ShareController: ViewController {
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares, fromForeignApp: self.fromForeignApp, forcedTheme: self.forcedTheme)
|
||||
self.controllerNode.completed = completed
|
||||
self.controllerNode.completed = self.completed
|
||||
self.controllerNode.dismiss = { [weak self] shared in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
self?.dismissed?(shared)
|
||||
@ -721,13 +721,18 @@ public final class ShareController: ViewController {
|
||||
})
|
||||
activities = [shareToInstagram]
|
||||
}
|
||||
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: activities)
|
||||
|
||||
if let window = strongSelf.view.window, let rootViewController = window.rootViewController {
|
||||
activityController.popoverPresentationController?.sourceView = window
|
||||
activityController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
rootViewController.present(activityController, animated: true, completion: nil)
|
||||
}
|
||||
let _ = (strongSelf.didAppearPromise.get()
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: activities)
|
||||
if let strongSelf = self, let window = strongSelf.view.window, let rootViewController = window.rootViewController {
|
||||
activityController.popoverPresentationController?.sourceView = window
|
||||
activityController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
rootViewController.present(activityController, animated: true, completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
return .done
|
||||
}
|
||||
@ -785,12 +790,16 @@ public final class ShareController: ViewController {
|
||||
super.loadView()
|
||||
}
|
||||
|
||||
let didAppearPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.animatedIn {
|
||||
self.animatedIn = true
|
||||
self.controllerNode.animateIn()
|
||||
self.didAppearPromise.set(true)
|
||||
if !self.immediateExternalShare {
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,10 +21,6 @@ enum ShareExternalState {
|
||||
case done
|
||||
}
|
||||
|
||||
func openExternalShare(state: () -> Signal<ShareExternalState, NoError>) {
|
||||
|
||||
}
|
||||
|
||||
final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||
private let sharedContext: SharedAccountContext
|
||||
private var context: AccountContext?
|
||||
@ -175,6 +171,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.isHidden = true
|
||||
|
||||
self.controllerInteraction = ShareControllerInteraction(togglePeer: { [weak self] peer, search in
|
||||
if let strongSelf = self {
|
||||
@ -621,6 +619,11 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if let completion = self.outCompletion {
|
||||
self.outCompletion = nil
|
||||
completion()
|
||||
return
|
||||
}
|
||||
if self.contentNode != nil {
|
||||
self.isHidden = false
|
||||
|
||||
@ -635,6 +638,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
var outCompletion: (() -> Void)?
|
||||
func animateOut(shared: Bool, completion: @escaping () -> Void) {
|
||||
if self.contentNode != nil {
|
||||
var dimCompleted = false
|
||||
@ -664,7 +668,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
internalCompletion()
|
||||
})
|
||||
} else {
|
||||
completion()
|
||||
self.outCompletion = completion
|
||||
Queue.mainQueue().after(0.2) {
|
||||
if let completion = self.outCompletion {
|
||||
self.outCompletion = nil
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,7 +128,7 @@ private func preparedShareItem(account: Account, to peerId: PeerId, value: [Stri
|
||||
let estimatedSize = TGMediaVideoConverter.estimatedSize(for: preset, duration: finalDuration, hasAudio: true)
|
||||
|
||||
let resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: asset.url.path, adjustments: resourceAdjustments)
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: Int(finalDuration), size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags)], hintFileIsLarge: estimatedSize > 5 * 1024 * 1024)
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: Int(finalDuration), size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024)
|
||||
|> mapError { _ -> Void in
|
||||
return Void()
|
||||
}
|
||||
@ -192,7 +192,7 @@ private func preparedShareItem(account: Account, to peerId: PeerId, value: [Stri
|
||||
mimeType = "animation/gif"
|
||||
attributes = [.ImageSize(size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height))), .Animated, .FileName(fileName: fileName ?? "animation.gif")]
|
||||
}
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(data), mimeType: mimeType, attributes: attributes, hintFileIsLarge: data.count > 5 * 1024 * 1024)
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(data), mimeType: mimeType, attributes: attributes, hintFileIsLarge: data.count > 10 * 1024 * 1024)
|
||||
|> mapError { _ -> Void in return Void() }
|
||||
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
|
||||
switch event {
|
||||
@ -223,7 +223,7 @@ private func preparedShareItem(account: Account, to peerId: PeerId, value: [Stri
|
||||
thumbnailData = jpegData
|
||||
}
|
||||
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(data), thumbnailData: thumbnailData, mimeType: mimeType, attributes: [.FileName(fileName: fileName ?? "file")], hintFileIsLarge: data.count > 5 * 1024 * 1024)
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(data), thumbnailData: thumbnailData, mimeType: mimeType, attributes: [.FileName(fileName: fileName ?? "file")], hintFileIsLarge: data.count > 10 * 1024 * 1024)
|
||||
|> mapError { _ -> Void in return Void() }
|
||||
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
|
||||
switch event {
|
||||
@ -241,13 +241,14 @@ private func preparedShareItem(account: Account, to peerId: PeerId, value: [Stri
|
||||
let isVoice = ((value["isVoice"] as? NSNumber)?.boolValue ?? false)
|
||||
let title = value["title"] as? String
|
||||
let artist = value["artist"] as? String
|
||||
let mimeType = value["mimeType"] as? String ?? "audio/ogg"
|
||||
|
||||
var waveform: MemoryBuffer?
|
||||
if let waveformData = TGItemProviderSignals.audioWaveform(url) {
|
||||
waveform = MemoryBuffer(data: waveformData)
|
||||
}
|
||||
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(audioData), mimeType: "audio/ogg", attributes: [.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: artist, waveform: waveform), .FileName(fileName: fileName)], hintFileIsLarge: audioData.count > 5 * 1024 * 1024)
|
||||
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(audioData), mimeType: mimeType, attributes: [.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: artist, waveform: waveform), .FileName(fileName: fileName)], hintFileIsLarge: audioData.count > 10 * 1024 * 1024)
|
||||
|> mapError { _ -> Void in return Void() }
|
||||
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
|
||||
switch event {
|
||||
|
@ -140,33 +140,31 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
|
||||
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty {
|
||||
if let stickerItem = stickerItem {
|
||||
if let _ = stickerItem.file.dimensions {
|
||||
if stickerItem.file.isAnimatedSticker {
|
||||
let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
|
||||
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
self?.removePlaceholder(animated: false)
|
||||
}
|
||||
if stickerItem.file.isAnimatedSticker {
|
||||
let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
|
||||
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
self?.removePlaceholder(animated: false)
|
||||
}
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||
} else {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = false
|
||||
self.animationNode = nil
|
||||
animationNode.removeFromSupernode()
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
|
||||
}
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||
} else {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = false
|
||||
self.animationNode = nil
|
||||
animationNode.removeFromSupernode()
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
|
||||
}
|
||||
} else {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
|
@ -845,7 +845,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
strongSelf.maybeRequestParticipants(ssrcs: ssrcs)
|
||||
}
|
||||
})
|
||||
}, demoAudioStream: self.accountContext.sharedContext.immediateExperimentalUISettings.demoAudioStream)
|
||||
self.incomingVideoSourcePromise.set(callContext.videoSources
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] sources -> [PeerId: UInt32] in
|
||||
|
@ -887,7 +887,11 @@ public final class VoiceChatController: ViewController {
|
||||
case .restricted:
|
||||
text = presentationData.strings.Channel_ErrorAddBlocked
|
||||
case .notMutualContact:
|
||||
text = presentationData.strings.GroupInfo_AddUserLeftError
|
||||
if case .broadcast = groupPeer.info {
|
||||
text = presentationData.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = presentationData.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
case .botDoesntSupportGroups:
|
||||
text = presentationData.strings.Channel_BotDoesntSupportGroups
|
||||
case .tooMuchBots:
|
||||
@ -952,15 +956,9 @@ public final class VoiceChatController: ViewController {
|
||||
self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
case .notMutualContact:
|
||||
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peer.id)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .tooManyChannels:
|
||||
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peer.id)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .groupFull, .generic:
|
||||
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ public enum ChatHistoryImport {
|
||||
case chatAdminRequired
|
||||
}
|
||||
|
||||
public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, mimeType: String, type: MediaType) -> Signal<Float, UploadMediaError> {
|
||||
public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, disposeFileAfterDone: Bool, fileName: String, mimeType: String, type: MediaType) -> Signal<Float, UploadMediaError> {
|
||||
var forceNoBigParts = true
|
||||
guard let size = fileSize(file.path), size != 0 else {
|
||||
return .single(1.0)
|
||||
@ -160,6 +160,11 @@ public enum ChatHistoryImport {
|
||||
|> mapToSignal { result -> Signal<Float, UploadMediaError> in
|
||||
return .single(1.0)
|
||||
}
|
||||
|> afterDisposed {
|
||||
if disposeFileAfterDone {
|
||||
TempBox.shared.dispose(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,7 +197,7 @@ public enum ChatHistoryImport {
|
||||
case invalidChatType
|
||||
case userBlocked
|
||||
case limitExceeded
|
||||
case userIsNotMutualContact
|
||||
case notMutualContact
|
||||
}
|
||||
|
||||
public static func checkPeerImport(account: Account, peerId: PeerId) -> Signal<CheckPeerImportResult, CheckPeerImportError> {
|
||||
@ -217,7 +222,7 @@ public enum ChatHistoryImport {
|
||||
} else if error.errorDescription == "USER_IS_BLOCKED" {
|
||||
return .userBlocked
|
||||
} else if error.errorDescription == "USER_NOT_MUTUAL_CONTACT" {
|
||||
return .userBlocked
|
||||
return .notMutualContact
|
||||
} else if error.errorDescription == "FLOOD_WAIT" {
|
||||
return .limitExceeded
|
||||
} else {
|
||||
|
@ -6,6 +6,7 @@ import MtProtoKit
|
||||
|
||||
public enum CreateSecretChatError {
|
||||
case generic
|
||||
case limitExceeded
|
||||
}
|
||||
|
||||
public func createSecretChat(account: Account, peerId: PeerId) -> Signal<PeerId, CreateSecretChatError> {
|
||||
@ -29,9 +30,13 @@ public func createSecretChat(account: Account, peerId: PeerId) -> Signal<PeerId,
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
return account.network.request(Api.functions.messages.requestEncryption(userId: inputUser, randomId: Int32(bitPattern: arc4random()), gA: Buffer(data: ga)))
|
||||
|> mapError { _ -> CreateSecretChatError in
|
||||
return .generic
|
||||
return account.network.request(Api.functions.messages.requestEncryption(userId: inputUser, randomId: Int32(bitPattern: arc4random()), gA: Buffer(data: ga)), automaticFloodWait: false)
|
||||
|> mapError { error -> CreateSecretChatError in
|
||||
if error.errorDescription.hasPrefix("FLOOD_WAIT_") {
|
||||
return .limitExceeded
|
||||
} else {
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
|> mapToSignal { result -> Signal<PeerId, CreateSecretChatError> in
|
||||
return account.postbox.transaction { transaction -> PeerId in
|
||||
|
@ -529,7 +529,7 @@ private final class PeerExportedInvitationsContextImpl {
|
||||
}
|
||||
|
||||
private func updateCache() {
|
||||
guard self.hasLoadedOnce && !self.isLoadingMore else {
|
||||
guard self.isMainList && self.hasLoadedOnce && !self.isLoadingMore else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import SwiftSignalKit
|
||||
import MtProtoKit
|
||||
|
||||
import SyncCore
|
||||
|
||||
@ -11,13 +12,19 @@ public enum JoinChannelError {
|
||||
case tooMuchUsers
|
||||
}
|
||||
|
||||
public func joinChannel(account: Account, peerId: PeerId) -> Signal<RenderedChannelParticipant?, JoinChannelError> {
|
||||
public func joinChannel(account: Account, peerId: PeerId, hash: String?) -> Signal<RenderedChannelParticipant?, JoinChannelError> {
|
||||
return account.postbox.loadedPeerWithId(peerId)
|
||||
|> take(1)
|
||||
|> castError(JoinChannelError.self)
|
||||
|> mapToSignal { peer -> Signal<RenderedChannelParticipant?, JoinChannelError> in
|
||||
if let inputChannel = apiInputChannel(peer) {
|
||||
return account.network.request(Api.functions.channels.joinChannel(channel: inputChannel))
|
||||
let request: Signal<Api.Updates, MTRpcError>
|
||||
if let hash = hash {
|
||||
request = account.network.request(Api.functions.messages.importChatInvite(hash: hash))
|
||||
} else {
|
||||
request = account.network.request(Api.functions.channels.joinChannel(channel: inputChannel))
|
||||
}
|
||||
return request
|
||||
|> mapError { error -> JoinChannelError in
|
||||
switch error.errorDescription {
|
||||
case "CHANNELS_TOO_MUCH":
|
||||
|
@ -202,6 +202,8 @@ public func updateChannelAdminRights(account: Account, peerId: PeerId, adminId:
|
||||
}
|
||||
|> map { [$0] }
|
||||
)
|
||||
} else if error.errorDescription == "USER_NOT_MUTUAL_CONTACT" {
|
||||
return .fail(.addMemberError(.notMutualContact))
|
||||
} else if error.errorDescription == "USER_PRIVACY_RESTRICTED" {
|
||||
return .fail(.addMemberError(.restricted))
|
||||
} else if error.errorDescription == "USER_CHANNELS_TOO_MUCH" {
|
||||
|
@ -623,7 +623,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
||||
hintFileIsLarge = true
|
||||
break loop
|
||||
default:
|
||||
break loop
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -212,7 +212,7 @@ public struct PresentationResourcesChatList {
|
||||
})
|
||||
}
|
||||
|
||||
public static func scamIcon(_ theme: PresentationTheme, type: ScamIconType) -> UIImage? {
|
||||
public static func scamIcon(_ theme: PresentationTheme, strings: PresentationStrings, type: ScamIconType) -> UIImage? {
|
||||
let key: PresentationResourceKey
|
||||
let color: UIColor
|
||||
switch type {
|
||||
@ -227,7 +227,10 @@ public struct PresentationResourcesChatList {
|
||||
color = theme.chat.serviceMessage.components.withDefaultWallpaper.scam
|
||||
}
|
||||
return theme.image(key.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 37.0, height: 16.0), contextGenerator: { size, context in
|
||||
let titleString = NSAttributedString(string: strings.Message_ScamAccount.uppercased(), font: Font.bold(10.0), textColor: color, paragraphAlignment: .center)
|
||||
let stringRect = titleString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
|
||||
return generateImage(CGSize(width: floor(stringRect.width) + 11.0, height: 16.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
@ -240,7 +243,6 @@ public struct PresentationResourcesChatList {
|
||||
|
||||
let titlePath = CGMutablePath()
|
||||
titlePath.addRect(bounds.offsetBy(dx: 0.0, dy: -2.0 + UIScreenPixel))
|
||||
let titleString = NSAttributedString(string: "SCAM", font: Font.bold(10.0), textColor: color, paragraphAlignment: .center)
|
||||
let titleFramesetter = CTFramesetterCreateWithAttributedString(titleString as CFAttributedString)
|
||||
let titleFrame = CTFramesetterCreateFrame(titleFramesetter, CFRangeMake(0, titleString.length), titlePath, nil)
|
||||
CTFrameDraw(titleFrame, context)
|
||||
@ -248,7 +250,7 @@ public struct PresentationResourcesChatList {
|
||||
})
|
||||
}
|
||||
|
||||
public static func fakeIcon(_ theme: PresentationTheme, type: ScamIconType) -> UIImage? {
|
||||
public static func fakeIcon(_ theme: PresentationTheme, strings: PresentationStrings, type: ScamIconType) -> UIImage? {
|
||||
let key: PresentationResourceKey
|
||||
let color: UIColor
|
||||
switch type {
|
||||
@ -263,7 +265,10 @@ public struct PresentationResourcesChatList {
|
||||
color = theme.chat.serviceMessage.components.withDefaultWallpaper.scam
|
||||
}
|
||||
return theme.image(key.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 37.0, height: 16.0), contextGenerator: { size, context in
|
||||
let titleString = NSAttributedString(string: strings.Message_FakeAccount.uppercased(), font: Font.bold(10.0), textColor: color, paragraphAlignment: .center)
|
||||
let stringRect = titleString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
|
||||
return generateImage(CGSize(width: floor(stringRect.width) + 11.0, height: 16.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
@ -276,7 +281,6 @@ public struct PresentationResourcesChatList {
|
||||
|
||||
let titlePath = CGMutablePath()
|
||||
titlePath.addRect(bounds.offsetBy(dx: 0.0, dy: -2.0 + UIScreenPixel))
|
||||
let titleString = NSAttributedString(string: "FAKE", font: Font.bold(10.0), textColor: color, paragraphAlignment: .center)
|
||||
let titleFramesetter = CTFramesetterCreateWithAttributedString(titleString as CFAttributedString)
|
||||
let titleFrame = CTFramesetterCreateFrame(titleFramesetter, CFRangeMake(0, titleString.length), titlePath, nil)
|
||||
CTFrameDraw(titleFrame, context)
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/VideoCall.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/VideoCall.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_menuvideocall.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/VideoCall.imageset/ic_menuvideocall.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/VideoCall.imageset/ic_menuvideocall.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0000.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0000.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0001.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0001.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0002.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0002.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0003.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0003.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0004.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0004.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0005.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0005.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0006.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0006.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0007.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0007.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0008.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0008.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0009.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0009.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0010.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0010.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0011.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0011.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0012.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0012.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0013.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0013.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0014.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0014.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0015.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0015.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0016.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0016.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0017.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0017.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0018.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0018.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0019.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0019.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0020.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0020.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0021.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0021.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0022.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0022.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0023.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0023.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0024.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0024.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0025.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0025.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0026.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0026.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0027.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0027.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0028.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0028.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0029.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0029.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0030.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0030.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0031.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0031.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0032.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0032.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0033.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0033.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0034.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0034.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0035.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0035.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0036.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0036.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0037.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0037.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0038.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0038.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0039.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0039.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0040.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0040.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0041.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0041.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0042.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0042.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0043.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0043.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0044.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0044.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0045.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0045.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0046.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0046.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0047.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0047.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0048.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0048.ogg
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/streaming_example/0049.ogg
Normal file
BIN
submodules/TelegramUI/Resources/streaming_example/0049.ogg
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user