Emoji Keywords

This commit is contained in:
Ilya Laktyushin 2019-03-18 16:21:33 +03:00
parent e2de09dc70
commit 27b25d20b5
5 changed files with 372 additions and 3 deletions

View File

@ -16,6 +16,7 @@
0962E66F21B6147600245FD9 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0962E66E21B6147600245FD9 /* AppConfiguration.swift */; };
0962E67521B6437600245FD9 /* SplitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0962E67421B6437600245FD9 /* SplitTest.swift */; };
0962E68121BAA20E00245FD9 /* SearchBotsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0962E68021BAA20E00245FD9 /* SearchBotsConfiguration.swift */; };
09E4A80F223F1FBF0038140F /* EmojiKeywords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09E4A80E223F1FBF0038140F /* EmojiKeywords.swift */; };
09EDAD382213120C0012A50B /* AutodownloadSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDAD372213120C0012A50B /* AutodownloadSettings.swift */; };
09EDAD3A22131D010012A50B /* ManagedAutodownloadSettingsUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDAD3922131D010012A50B /* ManagedAutodownloadSettingsUpdates.swift */; };
9F06831021A40DEC001D8EDB /* NotificationExceptionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830F21A40DEC001D8EDB /* NotificationExceptionsList.swift */; };
@ -816,6 +817,7 @@
0962E66E21B6147600245FD9 /* AppConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = "<group>"; };
0962E67421B6437600245FD9 /* SplitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitTest.swift; sourceTree = "<group>"; };
0962E68021BAA20E00245FD9 /* SearchBotsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBotsConfiguration.swift; sourceTree = "<group>"; };
09E4A80E223F1FBF0038140F /* EmojiKeywords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiKeywords.swift; sourceTree = "<group>"; };
09EDAD372213120C0012A50B /* AutodownloadSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadSettings.swift; sourceTree = "<group>"; };
09EDAD3922131D010012A50B /* ManagedAutodownloadSettingsUpdates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAutodownloadSettingsUpdates.swift; sourceTree = "<group>"; };
9F06830F21A40DEC001D8EDB /* NotificationExceptionsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExceptionsList.swift; sourceTree = "<group>"; };
@ -1764,6 +1766,7 @@
D08CAA831ED8164B0000FDA8 /* Localization.swift */,
D08CAA861ED81DD40000FDA8 /* LocalizationInfo.swift */,
D05D8B362192F8AF0064586F /* LocalizationListState.swift */,
09E4A80E223F1FBF0038140F /* EmojiKeywords.swift */,
);
name = Localization;
sourceTree = "<group>";
@ -2320,6 +2323,7 @@
D00D34421E6EDD2E0057B307 /* ManagedSynchronizeConsumeMessageContentsOperations.swift in Sources */,
D08984FB2118816A00918162 /* Reachability.m in Sources */,
D0DA1D321F7043D50034E892 /* ManagedPendingPeerNotificationSettings.swift in Sources */,
09E4A80F223F1FBF0038140F /* EmojiKeywords.swift in Sources */,
D099D7491EEF418D00A3128C /* HistoryViewChannelStateValidation.swift in Sources */,
C23BC3871E9BE3CA00D79F92 /* ImportContact.swift in Sources */,
D00422D321677F4500719B67 /* ManagedAccountPresence.swift in Sources */,

View File

@ -132,6 +132,7 @@ private var declaredEncodables: Void = {
declareEncodable(TelegramMediaPoll.self, f: { TelegramMediaPoll(decoder: $0) })
declareEncodable(TelegramMediaUnsupported.self, f: { TelegramMediaUnsupported(decoder: $0) })
declareEncodable(ContactsSettings.self, f: { ContactsSettings(decoder: $0) })
declareEncodable(EmojiKeywordsMap.self, f: { EmojiKeywordsMap(decoder: $0) })
return
}()

View File

@ -0,0 +1,357 @@
import Foundation
#if os(macOS)
import PostboxMac
import SwiftSignalKitMac
#else
import Postbox
import SwiftSignalKit
#endif
public enum EmojiKeyword: Equatable {
case keyword(String, [String])
case keywordSubtrahend(String, [String])
var name: String {
switch self {
case let .keyword(name, _), let .keywordSubtrahend(name, _):
return name
}
}
func union(_ emojiKeyword: EmojiKeyword) -> EmojiKeyword {
if case let .keyword(name, lhsEmoticons) = self, name == emojiKeyword.name {
switch emojiKeyword {
case let .keyword(_, rhsEmoticons):
var existingEmoticons = Set(lhsEmoticons)
var updatedEmoticons = lhsEmoticons
for emoticon in rhsEmoticons {
if !existingEmoticons.contains(emoticon) {
existingEmoticons.insert(emoticon)
updatedEmoticons.append(emoticon)
}
}
return .keyword(name, updatedEmoticons)
case let .keywordSubtrahend(_, rhsEmoticons):
let substractedEmoticons = Set(rhsEmoticons)
let updatedEmoticons = lhsEmoticons.filter { !substractedEmoticons.contains($0) }
return .keyword(name, updatedEmoticons)
}
} else {
return self
}
}
}
private func writeString(_ buffer: WriteBuffer, _ string: String) {
if let data = string.data(using: .utf8) {
var length: Int32 = Int32(data.count)
buffer.write(&length, offset: 0, length: 4)
buffer.write(data)
} else {
var length: Int32 = 0
buffer.write(&length, offset: 0, length: 4)
}
}
private func writeStringArray(_ buffer: WriteBuffer, _ array: [String]) {
var length = Int32(array.count)
buffer.write(&length, offset: 0, length: 4)
for string in array {
writeString(buffer, string)
}
}
public final class EmojiKeywords: PostboxCoding, Equatable {
public let languageCode: String
public let inputLanguageCode: String
public let version: Int32
public let timestamp: Int32
public let entries: [String: EmojiKeyword]
public init(languageCode: String, inputLanguageCode: String, version: Int32, timestamp: Int32, entries: [String: EmojiKeyword]) {
self.languageCode = languageCode
self.inputLanguageCode = inputLanguageCode
self.version = version
self.timestamp = timestamp
self.entries = entries
}
public init(decoder: PostboxDecoder) {
self.languageCode = decoder.decodeStringForKey("l", orElse: "")
self.inputLanguageCode = decoder.decodeStringForKey("i", orElse: "")
self.version = decoder.decodeInt32ForKey("v", orElse: 0)
self.timestamp = decoder.decodeInt32ForKey("t", orElse: 0)
let count = decoder.decodeInt32ForKey("c", orElse: 0)
var entries: [String: EmojiKeyword] = [:]
if let data = decoder.decodeBytesForKey("d") {
for _ in 0 ..< count {
var length: Int32 = 0
data.read(&length, offset: 0, length: 4)
let nameData = Data(bytes: data.memory.advanced(by: data.offset), count: Int(length))
let name = String(data: nameData, encoding: .utf8)
data.skip(Int(length))
var emoticonsCount: Int32 = 0
data.read(&emoticonsCount, offset: 0, length: 4)
var emoticons: [String] = []
for _ in 0 ..< emoticonsCount {
var length: Int32 = 0
data.read(&length, offset: 0, length: 4)
let emoticonData = Data(bytes: data.memory.advanced(by: data.offset), count: Int(length))
let emoticon = String(data: emoticonData, encoding: .utf8)
data.skip(Int(length))
if let emoticon = emoticon {
emoticons.append(emoticon)
}
}
if let name = name {
entries[name] = .keyword(name, emoticons)
}
}
}
self.entries = entries
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.languageCode, forKey: "l")
encoder.encodeString(self.inputLanguageCode, forKey: "i")
encoder.encodeInt32(self.version, forKey: "v")
encoder.encodeInt32(self.timestamp, forKey: "t")
encoder.encodeInt32(Int32(self.entries.count), forKey: "c")
let buffer = WriteBuffer()
for case let .keyword(name, emoticons) in self.entries.values {
writeString(buffer, name)
writeStringArray(buffer, emoticons)
}
encoder.encodeBytes(buffer, forKey: "d")
}
public static func ==(lhs: EmojiKeywords, rhs: EmojiKeywords) -> Bool {
if lhs === rhs {
return true
}
if lhs.languageCode == rhs.languageCode && lhs.inputLanguageCode == rhs.inputLanguageCode && lhs.entries == rhs.entries {
return true
}
return false
}
}
extension EmojiKeyword {
init(apiEmojiKeyword: Api.EmojiKeyword) {
switch apiEmojiKeyword {
case let .emojiKeyword(keyword, emoticons):
self = .keyword(keyword, emoticons)
case let .emojiKeywordDeleted(keyword, emoticons):
self = .keywordSubtrahend(keyword, emoticons)
}
}
}
public final class EmojiKeywordsMap: PreferencesEntry, Equatable {
public let entries: [String: EmojiKeywords]
public static var defaultValue: EmojiKeywordsMap {
return EmojiKeywordsMap(entries: [:])
}
public init(entries: [String: EmojiKeywords]) {
self.entries = entries
}
public init(decoder: PostboxDecoder) {
self.entries = decoder.decodeObjectDictionaryForKey("entries", keyDecoder: { decoder in
return decoder.decodeStringForKey("k", orElse: "")
}, valueDecoder: { decoder in
return EmojiKeywords(decoder: decoder)
})
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectDictionary(self.entries, forKey: "entries", keyEncoder: { key, encoder in
encoder.encodeString(key, forKey: "k")
})
}
public func isEqual(to: PreferencesEntry) -> Bool {
if let to = to as? EmojiKeywordsMap {
return self == to
} else {
return false
}
}
public static func ==(lhs: EmojiKeywordsMap, rhs: EmojiKeywordsMap) -> Bool {
return lhs.entries == rhs.entries
}
}
private func updateEmojiKeywordsList(accountManager: AccountManager, _ f: @escaping (EmojiKeywordsMap) -> EmojiKeywordsMap) -> Void {
let _ = accountManager.transaction({ transaction -> Void in
transaction.updateSharedData(SharedDataKeys.emojiKeywords, { entry in
let current: EmojiKeywordsMap
if let entry = entry as? EmojiKeywordsMap {
current = entry
} else {
current = .defaultValue
}
return f(current)
})
}).start()
}
private let refreshTimeout: Int32 = 60 * 60
public enum DownloadEmojiKeywordsError {
case generic
case invalidLanguageCode
}
private func downloadEmojiKeywords(network: Network, inputLanguageCode: String) -> Signal<EmojiKeywords, DownloadEmojiKeywordsError> {
return network.request(Api.functions.messages.getEmojiKeywords(langCode: inputLanguageCode))
|> mapError { _ -> DownloadEmojiKeywordsError in
return .generic
}
|> map { result -> EmojiKeywords in
switch result {
case let .emojiKeywordsDifference(langCode, _, version, keywords):
var entries: [String: EmojiKeyword] = [:]
for apiEmojiKeyword in keywords {
let emojiKeyword = EmojiKeyword(apiEmojiKeyword: apiEmojiKeyword)
entries[emojiKeyword.name] = emojiKeyword
}
return EmojiKeywords(languageCode: langCode, inputLanguageCode: inputLanguageCode, version: version, timestamp: Int32(CFAbsoluteTimeGetCurrent()), entries: entries)
}
}
}
private func downloadEmojiKeywordsDifference(network: Network, languageCode: String, inputLanguageCode: String, fromVersion: Int32) -> Signal<EmojiKeywords, DownloadEmojiKeywordsError> {
return network.request(Api.functions.messages.getEmojiKeywordsDifference(langCode: languageCode, fromVersion: fromVersion))
|> mapError { _ -> DownloadEmojiKeywordsError in
return .generic
}
|> mapToSignal { result -> Signal<EmojiKeywords, DownloadEmojiKeywordsError> in
switch result {
case let .emojiKeywordsDifference(langCode, _, version, keywords):
if langCode == languageCode {
var entries: [String: EmojiKeyword] = [:]
for apiEmojiKeyword in keywords {
let emojiKeyword = EmojiKeyword(apiEmojiKeyword: apiEmojiKeyword)
entries[emojiKeyword.name] = emojiKeyword
}
return .single(EmojiKeywords(languageCode: langCode, inputLanguageCode: inputLanguageCode, version: version, timestamp: Int32(CFAbsoluteTimeGetCurrent()), entries: entries))
} else {
return .fail(.invalidLanguageCode)
}
}
}
}
public func emojiKeywords(accountManager: AccountManager, network: Network, inputLanguageCode: String) -> Signal<EmojiKeywords?, NoError> {
return accountManager.sharedData(keys: [SharedDataKeys.emojiKeywords])
|> take(1)
|> map { sharedData in
return sharedData.entries[SharedDataKeys.emojiKeywords] as? EmojiKeywordsMap ?? .defaultValue
}
|> mapToSignal { keywordsMap -> Signal<EmojiKeywords?, NoError> in
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
let downloadEmojiKeywordsSignal: Signal<EmojiKeywords?, NoError> = downloadEmojiKeywords(network: network, inputLanguageCode: inputLanguageCode)
|> map(Optional.init)
|> `catch` { _ -> Signal<EmojiKeywords?, NoError> in
return .single(nil)
}
|> mapToSignal { keywords -> Signal<EmojiKeywords?, NoError> in
if let keywords = keywords {
updateEmojiKeywordsList(accountManager: accountManager, { keywordsMap -> EmojiKeywordsMap in
var entries = keywordsMap.entries
entries[inputLanguageCode] = keywords
return EmojiKeywordsMap(entries: entries)
})
}
return .complete()
}
if let emojiKeywords = keywordsMap.entries[inputLanguageCode] {
if emojiKeywords.timestamp + refreshTimeout > timestamp {
return .single(emojiKeywords)
} else {
return downloadEmojiKeywordsDifference(network: network, languageCode: emojiKeywords.languageCode, inputLanguageCode: inputLanguageCode, fromVersion: emojiKeywords.version)
|> map(Optional.init)
|> `catch` { _ -> Signal<EmojiKeywords?, NoError> in
return .single(nil)
}
|> mapToSignal { differenceKeywords -> Signal<EmojiKeywords?, NoError> in
if let differenceKeywords = differenceKeywords {
var updatedKeywords = emojiKeywords
var updatedKeywordEntries: [String: EmojiKeyword] = emojiKeywords.entries
for differenceKeywordEntry in differenceKeywords.entries.values {
let name = differenceKeywordEntry.name
if let existingKeyword = updatedKeywordEntries[name] {
updatedKeywordEntries[name] = existingKeyword.union(differenceKeywordEntry)
} else if case .keyword = differenceKeywordEntry {
updatedKeywordEntries[name] = differenceKeywordEntry
}
}
updatedKeywords = EmojiKeywords(languageCode: differenceKeywords.languageCode, inputLanguageCode: inputLanguageCode, version: differenceKeywords.version, timestamp: Int32(CFAbsoluteTimeGetCurrent()), entries: updatedKeywordEntries)
updateEmojiKeywordsList(accountManager: accountManager, { keywordsMap -> EmojiKeywordsMap in
var entries = keywordsMap.entries
entries[inputLanguageCode] = updatedKeywords
return EmojiKeywordsMap(entries: entries)
})
return .complete()
} else {
return downloadEmojiKeywordsSignal
}
}
}
} else {
return downloadEmojiKeywordsSignal
}
}
}
public func searchEmojiKeywords(keywords: EmojiKeywords, query: String, completeMatch: Bool) -> Signal<[String], NoError> {
return Signal { subscriber in
let query = query.lowercased()
var existing = Set<String>()
var matched: [String] = []
if completeMatch {
if let keyword = keywords.entries[query], case let .keyword(_, emoticons) = keyword {
for emoticon in emoticons {
if !existing.contains(emoticon) {
existing.insert(emoticon)
matched.append(emoticon)
}
}
}
} else {
for case let .keyword(name, emoticons) in keywords.entries.values {
if name.hasPrefix(query) {
for emoticon in emoticons {
if !existing.contains(emoticon) {
existing.insert(emoticon)
matched.append(emoticon)
}
}
}
}
}
subscriber.putNext(matched)
subscriber.putCompletion()
return EmptyDisposable
} |> runOn(Queue.concurrentDefaultQueue())
}

View File

@ -452,9 +452,9 @@ extension JSON {
var dictionary = dictionary
switch value {
case let .jsonObjectValue(key, value):
if let value = JSON(apiJson: value, root: false) {
dictionary[key] = value
}
if let value = JSON(apiJson: value, root: false) {
dictionary[key] = value
}
}
return dictionary
})

View File

@ -252,6 +252,7 @@ private enum SharedDataKeyValues: Int32 {
case localizationSettings = 3
case proxySettings = 4
case autodownloadSettings = 5
case emojiKeywords = 6
}
public struct SharedDataKeys {
@ -284,6 +285,12 @@ public struct SharedDataKeys {
key.setInt32(0, value: SharedDataKeyValues.autodownloadSettings.rawValue)
return key
}()
public static let emojiKeywords: ValueBoxKey = {
let key = ValueBoxKey(length: 4)
key.setInt32(0, value: SharedDataKeyValues.emojiKeywords.rawValue)
return key
}()
}
public func applicationSpecificItemCacheCollectionId(_ value: Int8) -> Int8 {