From b23e7e738e1c4d35b9e2c40cc7ed0b8a39ce17d6 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 9 Aug 2018 18:39:31 +0300 Subject: [PATCH 01/32] [NY-2421] Blinking group avatar (#1052) * [NY-2421] Update sqlite foreign key constrains declaration * [NY-2421] Implemented migration for RoomTable, ContactTable, RoomMemberTable, LinkTable. --- Nynja/DB/Models/DBMessage.swift | 6 ------ Nynja/DB/Tables/ContactTable.swift | 6 +++--- Nynja/DB/Tables/LinkTable.swift | 6 +++--- Nynja/DB/Tables/RoomMemberTable.swift | 8 ++++---- Nynja/DB/Tables/RoomTable.swift | 4 ++-- Nynja/MessageDAO.swift | 10 +++------- Nynja/MigrationManager.swift | 21 +++++++++++++++++++++ 7 files changed, 36 insertions(+), 25 deletions(-) diff --git a/Nynja/DB/Models/DBMessage.swift b/Nynja/DB/Models/DBMessage.swift index d345acc4c..ca7f2db49 100644 --- a/Nynja/DB/Models/DBMessage.swift +++ b/Nynja/DB/Models/DBMessage.swift @@ -305,12 +305,6 @@ class DBMessage: Record, DBModelProtocol { } self.id = id - let rooms = try DBRoom.filter(Column(RoomTable.Column.messageId.title) == id).fetchAll(db) - try rooms.forEach { $0.messageId = nil; try $0.saveAggregate(db) } - - let contacts = try DBContact.filter(Column(ContactTable.Column.messageId.title) == id).fetchAll(db) - try contacts.forEach { $0.messageId = nil; try $0.saveAggregate(db) } - try DBDesc.deleteAll(db, targetId: String(id), targetType: .message) return try delete(db) diff --git a/Nynja/DB/Tables/ContactTable.swift b/Nynja/DB/Tables/ContactTable.swift index d69ed6d91..9285b90ee 100644 --- a/Nynja/DB/Tables/ContactTable.swift +++ b/Nynja/DB/Tables/ContactTable.swift @@ -15,7 +15,7 @@ class ContactTable: Table { } static func create(in db: Database) throws { - try db.create(table: ContactTable.name, body: { (t) in + try db.create(table: ContactTable.name) { t in t.column(Column.phoneId, .text).notNull().primaryKey() t.column(Column.avatar, .text) t.column(Column.names, .text) @@ -28,9 +28,9 @@ class ContactTable: Table { t.column(Column.presence, .text) t.column(Column.status, .text) - t.column(Column.messageId, .integer).references(MessageTable.name) + t.column(Column.messageId, .integer).references(MessageTable.name, onDelete: .setNull) t.column(Column.rosterId, .integer).references(RosterTable.name, onDelete: .cascade) - }) + } } } diff --git a/Nynja/DB/Tables/LinkTable.swift b/Nynja/DB/Tables/LinkTable.swift index 5b2b743b0..36884f8bd 100644 --- a/Nynja/DB/Tables/LinkTable.swift +++ b/Nynja/DB/Tables/LinkTable.swift @@ -15,13 +15,13 @@ final class LinkTable: Table { } static func create(in db: Database) throws { - try db.create(table: LinkTable.name, body: { (table) in + try db.create(table: LinkTable.name) { table in table.column(Column.id, .text).notNull().primaryKey() table.column(Column.name, .text).notNull() - table.column(Column.roomId, .text).notNull().references(RoomTable.name) + table.column(Column.roomId, .text).notNull().references(RoomTable.name, onDelete: .cascade) table.column(Column.created, .integer).notNull() table.column(Column.status, .text) - }) + } } } diff --git a/Nynja/DB/Tables/RoomMemberTable.swift b/Nynja/DB/Tables/RoomMemberTable.swift index dafffaaf6..9c27d5533 100644 --- a/Nynja/DB/Tables/RoomMemberTable.swift +++ b/Nynja/DB/Tables/RoomMemberTable.swift @@ -15,16 +15,16 @@ class RoomMemberTable: Table { } static func create(in db: Database) throws { - try db.create(table: RoomMemberTable.name, body: { (t) in + try db.create(table: RoomMemberTable.name) { t in t.column(Column.roomId, .text) t.column(Column.memberId, .integer) t.column(Column.isAdmin, .boolean).defaults(to: false) t.uniqueKey([Column.roomId.title, Column.memberId.title, Column.isAdmin.title], onConflict: .replace) - t.foreignKey([Column.roomId.title], references: RoomTable.name, columns: nil, onDelete: .cascade, onUpdate: nil, deferred: false) - t.foreignKey([Column.memberId.title], references: MemberTable.name, columns: nil, onDelete: .cascade, onUpdate: nil, deferred: false) - }) + t.foreignKey([Column.roomId.title], references: RoomTable.name, onDelete: .cascade) + t.foreignKey([Column.memberId.title], references: MemberTable.name, onDelete: .cascade) + } } } diff --git a/Nynja/DB/Tables/RoomTable.swift b/Nynja/DB/Tables/RoomTable.swift index 035c4b718..8cea93bff 100644 --- a/Nynja/DB/Tables/RoomTable.swift +++ b/Nynja/DB/Tables/RoomTable.swift @@ -15,7 +15,7 @@ class RoomTable: Table { } static func create(in db: Database) throws { - try db.create(table: RoomTable.name) { (t) in + try db.create(table: RoomTable.name) { t in t.column(Column.id, .text).primaryKey() t.column(Column.name, .text).notNull() t.column(Column.description, .text) @@ -31,7 +31,7 @@ class RoomTable: Table { t.column(Column.created, .integer).notNull().defaults(to: 0) t.column(Column.status, .text) - t.column(Column.messageId, .integer).references(MessageTable.name) + t.column(Column.messageId, .integer).references(MessageTable.name, onDelete: .setNull) t.column(Column.rosterId, .integer).references(RosterTable.name) } } diff --git a/Nynja/MessageDAO.swift b/Nynja/MessageDAO.swift index 7ffbb77fd..8e0e19371 100644 --- a/Nynja/MessageDAO.swift +++ b/Nynja/MessageDAO.swift @@ -129,11 +129,9 @@ class MessageDAO: MessageDAOProtocol { let msg = Message() msg.id = message.link msg.feed_id = message.feed_id - if message.feed_id as? p2p != nil { + if message.feed_id is p2p { if let ids = message.seenby as? [Int64] { - let res = ids.filter { (value) -> Bool in - return value == -1 - }.count + let res = ids.count { $0 == -1 } if res > 0 { removeMessage(msg: msg) notify?(msg) @@ -151,9 +149,7 @@ class MessageDAO: MessageDAOProtocol { } if let id = (message.feed_id as? muc)?.name { if let ids = message.seenby as? [Int64] { - let res = ids.filter { (value) -> Bool in - return value == -1 - }.count + let res = ids.count { $0 == -1 } if res > 0 { removeMessage(msg: msg) notify?(msg) diff --git a/Nynja/MigrationManager.swift b/Nynja/MigrationManager.swift index 5e32d88dc..333819072 100644 --- a/Nynja/MigrationManager.swift +++ b/Nynja/MigrationManager.swift @@ -19,6 +19,7 @@ enum Migration: Int, Describable { case renameLinksToMessageLink case createLinkForRoom case renameChannelFeatureKeys + case updateMessageIdForeignKeyOnContactRoomTables static var allTitles: [String] = { var i = 0 @@ -202,6 +203,26 @@ class MigrationManager { try self.replaceFeatureKey("SubscribersCount", newKey: .subscribersCount, db: db) try self.replaceFeatureKey("AdminsCount", newKey: .adminsCount, db: db) } + + migrator.registerMigration(Migration.updateMessageIdForeignKeyOnContactRoomTables.title) { db in + let rooms = try DBRoom.fetchAll(db) + let contacts = try DBContact.fetchAll(db) + let roomMembers = try DBRoomMember.fetchAll(db) + let links = try DBLink.fetchAll(db) + + try db.drop(table: LinkTable.name) + try db.drop(table: RoomTable.name) + try db.drop(table: ContactTable.name) + + try RoomTable.create(in: db) + try ContactTable.create(in: db) + try LinkTable.create(in: db) + + try rooms.forEach { try $0.save(db) } + try contacts.forEach { try $0.save(db) } + try links.forEach { try $0.save(db) } + try roomMembers.forEach { try $0.save(db) } + } } private func recreateDescTable(_ db: Database, closure: ((DBDesc) -> Void)? = nil) throws { -- GitLab From 59e0bb2d67fe6d6af204b03e22ea210dfe677a4b Mon Sep 17 00:00:00 2001 From: Roman Chopovenko Date: Tue, 24 Jul 2018 17:19:52 +0300 Subject: [PATCH 02/32] initial commit --- Nynja/AppDelegate.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index e000e94ca..ade8b9e23 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -94,9 +94,8 @@ private extension AppDelegate { self.window?.rootViewController = navigation self.window?.makeKeyAndVisible() } - - - func configureDependencies() { + + private func configureDependencies() { setupTestFairy() setupCrashlytics() setupGoogleMaps() -- GitLab From f0532241e32880c57c622e617862b01c4d491518 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Wed, 1 Aug 2018 15:44:23 +0300 Subject: [PATCH 03/32] Setup marketplace menu flow --- Nynja.xcodeproj/project.pbxproj | 100 +++++++ Nynja/CircleMenuControl/Core/CircleMenu.swift | 270 ++++++++++++++++++ .../Core/CircleMenuDelegate.swift | 14 + .../Core/Sector/SectionView.swift | 136 +++++++++ .../Core/Sector/Sector.swift | 25 ++ .../Extension/UIView+Mask.swift | 50 ++++ .../Model/CircleMenuFactory.swift | 85 ++++++ .../Model/CircleMenuItem.swift | 18 ++ .../Model/CircleMenuSet.swift | 14 + Nynja/Modules/Main/MainProtocols.swift | 1 + .../Main/WireFrame/MainWireframe.swift | 6 + .../Marketplace/MarketplaceInteractor.swift | 25 ++ .../Marketplace/MarketplacePresenter.swift | 38 +++ .../Marketplace/MarketplaceProtocols.swift | 59 ++++ .../MarketplaceViewController.swift | 92 ++++++ .../Marketplace/MarketplaceWireFrame.swift | 40 +++ .../Message/Presenter/MessagePresenter.swift | 4 + .../Message/Protocols/MessageProtocols.swift | 2 + .../View/MessageVC+ContextMenuDelegate.swift | 2 +- .../Message/WireFrame/MessageWireframe.swift | 5 + .../Assets.xcassets/Marketplace/Contents.json | 6 + .../Contents.json | 12 + .../marketplace_swap_button.pdf | Bin 0 -> 12016 bytes .../Marketplace/menu/Contents.json | 6 + .../Marketplace/menu/access/Contents.json | 6 + .../marketplace_apps.imageset/Contents.json | 12 + .../marketplace_apps.pdf | Bin 0 -> 4491 bytes .../marketplace_bots.imageset/Contents.json | 12 + .../marketplace_bots.pdf | Bin 0 -> 5831 bytes .../Contents.json | 12 + .../marketplace_groups_channels.pdf | Bin 0 -> 8701 bytes .../Marketplace/menu/freelance/Contents.json | 6 + .../marketplace_design.imageset/Contents.json | 12 + .../marketplace_design.pdf | Bin 0 -> 5367 bytes .../Contents.json | 12 + .../marketplace_interpretation.pdf | Bin 0 -> 6447 bytes .../Contents.json | 12 + .../marketplace_support.pdf | Bin 0 -> 4922 bytes .../Marketplace/menu/main/Contents.json | 6 + .../marketplace_access.imageset/Contents.json | 12 + .../marketplace_access.pdf | Bin 0 -> 4346 bytes .../Contents.json | 12 + .../marketplace_freelance.pdf | Bin 0 -> 5134 bytes .../Contents.json | 12 + .../marketplace_virtual_goods.pdf | Bin 0 -> 6264 bytes .../menu/virtual goods/Contents.json | 6 + .../Contents.json | 12 + .../marketplace_media_content.pdf | Bin 0 -> 4325 bytes .../Contents.json | 12 + .../marketplace_sticker.pdf | Bin 0 -> 4653 bytes Nynja/Resources/Constants.swift | 12 + Nynja/Resources/en.lproj/Localizable.strings | 17 ++ Nynja/Resources/ru.lproj/Localizable.strings | 19 +- 53 files changed, 1212 insertions(+), 2 deletions(-) create mode 100644 Nynja/CircleMenuControl/Core/CircleMenu.swift create mode 100644 Nynja/CircleMenuControl/Core/CircleMenuDelegate.swift create mode 100644 Nynja/CircleMenuControl/Core/Sector/SectionView.swift create mode 100644 Nynja/CircleMenuControl/Core/Sector/Sector.swift create mode 100644 Nynja/CircleMenuControl/Extension/UIView+Mask.swift create mode 100644 Nynja/CircleMenuControl/Model/CircleMenuFactory.swift create mode 100644 Nynja/CircleMenuControl/Model/CircleMenuItem.swift create mode 100644 Nynja/CircleMenuControl/Model/CircleMenuSet.swift create mode 100644 Nynja/Modules/Marketplace/MarketplaceInteractor.swift create mode 100644 Nynja/Modules/Marketplace/MarketplacePresenter.swift create mode 100644 Nynja/Modules/Marketplace/MarketplaceProtocols.swift create mode 100644 Nynja/Modules/Marketplace/MarketplaceViewController.swift create mode 100644 Nynja/Modules/Marketplace/MarketplaceWireFrame.swift create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/marketplace_swap_button.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/marketplace_swap_button.imageset/marketplace_swap_button.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/access/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_apps.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_apps.imageset/marketplace_apps.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_bots.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_bots.imageset/marketplace_bots.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_groups_channels.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_groups_channels.imageset/marketplace_groups_channels.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_design.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_design.imageset/marketplace_design.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/marketplace_interpretation.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_support.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_support.imageset/marketplace_support.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/main/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/main/marketplace_access.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/main/marketplace_access.imageset/marketplace_access.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/main/marketplace_freelance.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/main/marketplace_freelance.imageset/marketplace_freelance.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/main/marketplace_virtual_goods.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/main/marketplace_virtual_goods.imageset/marketplace_virtual_goods.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/virtual goods/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/virtual goods/marketplace_media_content.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/virtual goods/marketplace_media_content.imageset/marketplace_media_content.pdf create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/virtual goods/marketplace_sticker.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Marketplace/menu/virtual goods/marketplace_sticker.imageset/marketplace_sticker.pdf diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 3aa8a8d1b..74b61ab42 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1579,11 +1579,24 @@ B723C632204D9E5100884FFD /* DataAndStorageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C631204D9E5100884FFD /* DataAndStorageOption.swift */; }; B723C634204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C633204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift */; }; B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C635204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift */; }; + B74BAFFB21076AFA0049CD27 /* CircleMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */; }; + B74BAFFC21076AFA0049CD27 /* SectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF121076AFA0049CD27 /* SectionView.swift */; }; + B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF221076AFA0049CD27 /* Sector.swift */; }; + B74BAFFF21076AFA0049CD27 /* CircleMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF421076AFA0049CD27 /* CircleMenuDelegate.swift */; }; + B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF621076AFA0049CD27 /* UIView+Mask.swift */; }; + B74BB00121076AFA0049CD27 /* CircleMenuSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF821076AFA0049CD27 /* CircleMenuSet.swift */; }; + B74BB00221076AFA0049CD27 /* CircleMenuFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF921076AFA0049CD27 /* CircleMenuFactory.swift */; }; + B74BB00321076AFA0049CD27 /* CircleMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFFA21076AFA0049CD27 /* CircleMenuItem.swift */; }; B750EF022046B24D00A99F9C /* TransferInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B750EF012046B24D00A99F9C /* TransferInfo.swift */; }; B750EF042046D69C00A99F9C /* SpeedMesurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B750EF032046D69C00A99F9C /* SpeedMesurement.swift */; }; B750EF062046D7C700A99F9C /* SpeedStringRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B750EF052046D7C700A99F9C /* SpeedStringRepresentable.swift */; }; B763DD9320AA1C3400A30B63 /* ContactCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B763DD9220AA1C3400A30B63 /* ContactCellLayout.swift */; }; B79B996E20CA88D100BEF5DE /* InviteFriendsMessageComposeDelegateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79B996D20CA88D100BEF5DE /* InviteFriendsMessageComposeDelegateHandler.swift */; }; + B79FA02B2107731400F286BF /* MarketplacePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0262107731400F286BF /* MarketplacePresenter.swift */; }; + B79FA02C2107731400F286BF /* MarketplaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0272107731400F286BF /* MarketplaceViewController.swift */; }; + B79FA02D2107731400F286BF /* MarketplaceProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0282107731400F286BF /* MarketplaceProtocols.swift */; }; + B79FA02E2107731400F286BF /* MarketplaceInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0292107731400F286BF /* MarketplaceInteractor.swift */; }; + B79FA02F2107731400F286BF /* MarketplaceWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA02A2107731400F286BF /* MarketplaceWireFrame.swift */; }; B7F505192061158800C28FA1 /* SettingsArrowCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */; }; B7F5051B20611A0900C28FA1 /* DownloadSettingsArrowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */; }; B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */; }; @@ -3400,12 +3413,25 @@ B723C631204D9E5100884FFD /* DataAndStorageOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageOption.swift; sourceTree = ""; }; B723C633204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataAndStorageTableDataSource.swift; sourceTree = ""; }; B723C635204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataAndStorageTableDelegate.swift; sourceTree = ""; }; + B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMenu.swift; sourceTree = ""; }; + B74BAFF121076AFA0049CD27 /* SectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionView.swift; sourceTree = ""; }; + B74BAFF221076AFA0049CD27 /* Sector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sector.swift; sourceTree = ""; }; + B74BAFF421076AFA0049CD27 /* CircleMenuDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMenuDelegate.swift; sourceTree = ""; }; + B74BAFF621076AFA0049CD27 /* UIView+Mask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Mask.swift"; sourceTree = ""; }; + B74BAFF821076AFA0049CD27 /* CircleMenuSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMenuSet.swift; sourceTree = ""; }; + B74BAFF921076AFA0049CD27 /* CircleMenuFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMenuFactory.swift; sourceTree = ""; }; + B74BAFFA21076AFA0049CD27 /* CircleMenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMenuItem.swift; sourceTree = ""; }; B750EF012046B24D00A99F9C /* TransferInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferInfo.swift; sourceTree = ""; }; B750EF032046D69C00A99F9C /* SpeedMesurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedMesurement.swift; sourceTree = ""; }; B750EF052046D7C700A99F9C /* SpeedStringRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedStringRepresentable.swift; sourceTree = ""; }; B763DD9220AA1C3400A30B63 /* ContactCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCellLayout.swift; sourceTree = ""; }; B79B996D20CA88D100BEF5DE /* InviteFriendsMessageComposeDelegateHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteFriendsMessageComposeDelegateHandler.swift; sourceTree = ""; }; B79D8BCE2020C35300184D5D /* UIEdgeInsets+Adjust.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Adjust.swift"; sourceTree = ""; }; + B79FA0262107731400F286BF /* MarketplacePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplacePresenter.swift; sourceTree = ""; }; + B79FA0272107731400F286BF /* MarketplaceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceViewController.swift; sourceTree = ""; }; + B79FA0282107731400F286BF /* MarketplaceProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceProtocols.swift; sourceTree = ""; }; + B79FA0292107731400F286BF /* MarketplaceInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceInteractor.swift; sourceTree = ""; }; + B79FA02A2107731400F286BF /* MarketplaceWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceWireFrame.swift; sourceTree = ""; }; B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsArrowCellViewModel.swift; sourceTree = ""; }; B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadSettingsArrowModel.swift; sourceTree = ""; }; B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageItemsFactory.swift; sourceTree = ""; }; @@ -5378,6 +5404,7 @@ 3A768DE41ECB3E7600108F7C /* Library */ = { isa = PBXGroup; children = ( + B74BAFED21076ADB0049CD27 /* CircleMenuControl */, A46679EF20F10B2B00DBC6B4 /* RequestModelFactory */, A458FAC620ECDAD90075D55E /* Base */, F11786D520A9AAB1007A9A1B /* Interfaces */, @@ -5845,6 +5872,7 @@ 975DB2471671357A9EEBF65B /* TimeZoneSelector */, E1BF3560A2E8EE8B02A9A9FB /* DateTimePicker */, 859B86352048224B003272B2 /* Settings */, + B79FA025210772CF00F286BF /* Marketplace */, ); path = Modules; sourceTree = ""; @@ -9939,6 +9967,53 @@ path = TableView; sourceTree = ""; }; + B74BAFED21076ADB0049CD27 /* CircleMenuControl */ = { + isa = PBXGroup; + children = ( + B74BAFEE21076AFA0049CD27 /* Core */, + B74BAFF521076AFA0049CD27 /* Extension */, + B74BAFF721076AFA0049CD27 /* Model */, + ); + path = CircleMenuControl; + sourceTree = ""; + }; + B74BAFEE21076AFA0049CD27 /* Core */ = { + isa = PBXGroup; + children = ( + B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */, + B74BAFF021076AFA0049CD27 /* Sector */, + B74BAFF421076AFA0049CD27 /* CircleMenuDelegate.swift */, + ); + path = Core; + sourceTree = ""; + }; + B74BAFF021076AFA0049CD27 /* Sector */ = { + isa = PBXGroup; + children = ( + B74BAFF121076AFA0049CD27 /* SectionView.swift */, + B74BAFF221076AFA0049CD27 /* Sector.swift */, + ); + path = Sector; + sourceTree = ""; + }; + B74BAFF521076AFA0049CD27 /* Extension */ = { + isa = PBXGroup; + children = ( + B74BAFF621076AFA0049CD27 /* UIView+Mask.swift */, + ); + path = Extension; + sourceTree = ""; + }; + B74BAFF721076AFA0049CD27 /* Model */ = { + isa = PBXGroup; + children = ( + B74BAFF821076AFA0049CD27 /* CircleMenuSet.swift */, + B74BAFF921076AFA0049CD27 /* CircleMenuFactory.swift */, + B74BAFFA21076AFA0049CD27 /* CircleMenuItem.swift */, + ); + path = Model; + sourceTree = ""; + }; B79B996F20CA89BA00BEF5DE /* MessageComposeHandler */ = { isa = PBXGroup; children = ( @@ -9947,6 +10022,18 @@ path = MessageComposeHandler; sourceTree = ""; }; + B79FA025210772CF00F286BF /* Marketplace */ = { + isa = PBXGroup; + children = ( + B79FA0282107731400F286BF /* MarketplaceProtocols.swift */, + B79FA0272107731400F286BF /* MarketplaceViewController.swift */, + B79FA0262107731400F286BF /* MarketplacePresenter.swift */, + B79FA0292107731400F286BF /* MarketplaceInteractor.swift */, + B79FA02A2107731400F286BF /* MarketplaceWireFrame.swift */, + ); + path = Marketplace; + sourceTree = ""; + }; B7B3AAAF204D3F1400756B77 /* TableHeaderFooter */ = { isa = PBXGroup; children = ( @@ -13042,6 +13129,7 @@ E7EED2341F740BF3005DAE20 /* ChatsItem.swift in Sources */, 260313A620A0A4BA009AC66D /* SwitchableActionCellViewModel.swift in Sources */, A42D51AE206A361400EEB952 /* Message.swift in Sources */, + B79FA02C2107731400F286BF /* MarketplaceViewController.swift in Sources */, C9405159204C91E000D72B04 /* DataAndStorageTableDataSource.swift in Sources */, A45F112920B4218D00F45004 /* MessageContentAppearance.swift in Sources */, C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */, @@ -13223,6 +13311,7 @@ 26C1A3F02031D9E60009F7F0 /* OtherUserTableViewDS.swift in Sources */, A45F110D20B4218D00F45004 /* SendingStatus.swift in Sources */, A45F114720B421AB00F45004 /* MessageExtension.swift in Sources */, + B79FA02E2107731400F286BF /* MarketplaceInteractor.swift in Sources */, F10B0E1B20B4412100528E7A /* GalleryViewController.swift in Sources */, E78EFB891FC867B200C44975 /* DBMuc.swift in Sources */, 8EE53A12200543F40079CCA8 /* GroupCollectionConstants.swift in Sources */, @@ -13278,11 +13367,13 @@ 0062D9472062EC4100B915AC /* InviteFriendsViewController.swift in Sources */, 4B2D063C202E1A1500010A0C /* ContactsExpandedItemsFactory.swift in Sources */, 85D66A2120BD970400FBD803 /* BBCodeEntity.swift in Sources */, + B74BB00221076AFA0049CD27 /* CircleMenuFactory.swift in Sources */, 4B8996EC204EF35200DCB183 /* MessageActionDAO.swift in Sources */, E791178A1F97874D00462D68 /* GradientView.swift in Sources */, E79117901F97A3BC00462D68 /* ProfileDetailsView.swift in Sources */, 265F5D24209B6987008ACCC8 /* Place.swift in Sources */, E734831A1F9F39400090A4DB /* CellModel.swift in Sources */, + B74BB00321076AFA0049CD27 /* CircleMenuItem.swift in Sources */, 6D485DDF1F0ACA4700E12FB1 /* UIImageView+Rounded.swift in Sources */, 26CD3FDB2104D19D00597E62 /* TranscribeShortAudioOperation.swift in Sources */, 40C2631343E285717633ADFA /* AuthPresenter.swift in Sources */, @@ -13291,6 +13382,7 @@ C940514A204C7FAF00D72B04 /* DataAndStorageViewController.swift in Sources */, 26C1A3E32031A95D0009F7F0 /* OtherUserProtocols.swift in Sources */, 8503B528205046A6006F0593 /* NotificationSettingsInteractor.swift in Sources */, + B79FA02F2107731400F286BF /* MarketplaceWireFrame.swift in Sources */, A4C9300520B323B700D6FB0F /* Room+BaseChatModel.swift in Sources */, E73483211F9F78DC0090A4DB /* ProfileSectionFooterView.swift in Sources */, 3AE0A84B1F20321A008A04F3 /* Wheel.swift in Sources */, @@ -13370,6 +13462,7 @@ 4B4266C3204D923400194BC1 /* Array+UIView.swift in Sources */, A4CB15232103735200C3B68B /* JDFileBasedMechanism.swift in Sources */, 3A771CAA1F191B38008D968A /* ProfileHandler.swift in Sources */, + B74BAFFC21076AFA0049CD27 /* SectionView.swift in Sources */, E78EFB871FC867A900C44975 /* DBP2p.swift in Sources */, 8ED0F3CF1FBC5CF2004916AB /* GroupsListPresenter.swift in Sources */, 850A2BB22035AE5E00D68FDF /* ForwardCellViewModel.swift in Sources */, @@ -13728,6 +13821,7 @@ 263D66271FE829CC00A509F8 /* RoomExtension+BERT.swift in Sources */, A45F112320B4218D00F45004 /* MessageVoiceView.swift in Sources */, A44B4D5520CE9BDF00CA700A /* SwitchCellViewModel.swift in Sources */, + B74BAFFB21076AFA0049CD27 /* CircleMenu.swift in Sources */, F117872520ACF2DB007A9A1B /* QualityName.swift in Sources */, A45F111B20B4218D00F45004 /* MessagePlaceView.swift in Sources */, A42D52C4206A53AA00EEB952 /* error2_Spec.swift in Sources */, @@ -13752,6 +13846,7 @@ 00E9825E205FDB1A008BF03D /* AuthExtension.swift in Sources */, A45F116020B422AF00F45004 /* Message+DB.swift in Sources */, 8E23E086200614AB00A59B8C /* GroupVideosCell.swift in Sources */, + B74BB00121076AFA0049CD27 /* CircleMenuSet.swift in Sources */, A4679B8E20B2DA610021FE9C /* SelectorAvatarCell.swift in Sources */, 2648C41B2069B52100863614 /* ChangeNumberView.swift in Sources */, 2648C40E2069B52100863614 /* ChangeNumberStep1Interactor.swift in Sources */, @@ -13841,6 +13936,7 @@ 26610F5B2015476C00609F77 /* LocationFullWheelItemModel.swift in Sources */, F117872920ACF2DB007A9A1B /* CameraSetting.swift in Sources */, E77D58971F98B91600FBE926 /* ProfileTableViewDelegate.swift in Sources */, + B79FA02D2107731400F286BF /* MarketplaceProtocols.swift in Sources */, A45F111420B4218D00F45004 /* SystemCell.swift in Sources */, 26C1A3E92031AAA30009F7F0 /* OtherUserViewController.swift in Sources */, 2606F3BC20BFE20500CF7F15 /* MessageInteractor+Translation.swift in Sources */, @@ -13862,6 +13958,7 @@ F10AFEB420F7B1B000C7CE83 /* WheelPreviewProtocol.swift in Sources */, A45F111220B4218D00F45004 /* MessageVC.swift in Sources */, 26DCB256206924B3001EF0AB /* FeatureFactory.swift in Sources */, + B74BAFFF21076AFA0049CD27 /* CircleMenuDelegate.swift in Sources */, A432CF1420B4347D00993AFB /* InputInfo.swift in Sources */, 85D66A0720BD963C00FBD803 /* MessagePayloadRenderer.swift in Sources */, 8EE9BC181FFBE0F800ECBBC7 /* Array+Message.swift in Sources */, @@ -13986,6 +14083,7 @@ 4B8996CD204ED33400DCB183 /* StarDAOProtocol.swift in Sources */, 85D669EC20BD962800FBD803 /* MessageVCLayout.swift in Sources */, 8EDDB08A200529C6000B7EC2 /* GroupStorageCollectionVC.swift in Sources */, + B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */, A42D51CD206A361400EEB952 /* operation.swift in Sources */, 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, @@ -14199,6 +14297,7 @@ E09CECE79892CABEF8793389 /* ImagePreviewInteractor.swift in Sources */, 2603139820A0A4B9009AC66D /* LangCellViewModel.swift in Sources */, A4BCEC6C20DBF2A40078B076 /* Link+DB.swift in Sources */, + B79FA02B2107731400F286BF /* MarketplacePresenter.swift in Sources */, 0008E9282036F480003E316E /* ScheduledMessage.swift in Sources */, 851872C120CD45B3007CD6CA /* StickersProvider.swift in Sources */, 26E7D04C1FCB8A72001C69B7 /* UIImageView+SetImage.swift in Sources */, @@ -14589,6 +14688,7 @@ 8580BAED20BD9A7100239D9D /* LinkLongPressGestureRecognizer.swift in Sources */, C6B308C6734EFB77892832A0 /* SecurityPresenter.swift in Sources */, A42D52B4206A53AA00EEB952 /* ok_Spec.swift in Sources */, + B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */, 8E54E93EA25B11D417A6100E /* SecurityInteractor.swift in Sources */, A43B259420AB1DFA00FF8107 /* InputBar.swift in Sources */, F11DF06820BD996200F3E005 /* NavigationProtocol.swift in Sources */, diff --git a/Nynja/CircleMenuControl/Core/CircleMenu.swift b/Nynja/CircleMenuControl/Core/CircleMenu.swift new file mode 100644 index 000000000..3f99f28eb --- /dev/null +++ b/Nynja/CircleMenuControl/Core/CircleMenu.swift @@ -0,0 +1,270 @@ +// +// CircleMenu.swift +// CircleMenuControl +// +// Created by Roman Chopovenko on 7/23/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import UIKit + +class CircleMenu: UIControl { + + weak var delegate: CircleMenuDelegate? + + private var container: UIView! + private var sectors = [Sector]() + private(set) var currentSectorIndex: Int? + private let separatorPadding: CGFloat = 30 + + private var menuNavigationStack = [CircleMenuSet]() { + didSet { + if let data = self.dataSource { + self.delegate?.didChangeCurrentSet(self, title: data.title) + } + self.sectors = [Sector]() + self.currentSectorIndex = nil + self.container.removeFromSuperview() + self.container = nil + self.setNeedsDisplay() + } + } + private var dataSource: CircleMenuSet? { + return self.menuNavigationStack.last + } + + private var containerCenter: CGPoint { + return CGPoint(x: self.container.bounds.width/2, y: self.container.bounds.height/2) + } + + private var segmentAngle: CGFloat { + guard let items = self.dataSource?.items else { return 0 } + return (2 * .pi) / CGFloat(items.count) + } + + + //MARK: Public methods + + public func updateMenu(with newMenuSet: CircleMenuSet) { + self.menuNavigationStack.append(newMenuSet) + } + + public func navigateBackInMenuStack() -> Bool { + if self.menuNavigationStack.count > 1 { + let lastIndex = self.menuNavigationStack.count - 1 + self.menuNavigationStack.remove(at: lastIndex) + return true + } + return false + } + + + //MARK: - Init + + required init(rect: CGRect, delegate: CircleMenuDelegate, menuSet: CircleMenuSet) { + super.init(frame: rect) + self.delegate = delegate + self.menuNavigationStack.append(menuSet) + self.backgroundColor = .clear + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: - Override + + override func draw(_ rect: CGRect) { + guard let items = self.dataSource?.items else { return } + self.container = UIView(frame: rect) + self.container.backgroundColor = .clear + + for index in 0.. Bool { + let touchPoint: CGPoint = touch.location(in: self) + + if let sector = self.detectSelectedSegment(from: touchPoint), let data = self.dataSource { + if data.items[sector.index].isEnabled { + self.selectSegment(at: sector.index) + } + } + return self.ignoreTaps(for: touchPoint) + } + + override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + if let selectedIndex = self.currentSectorIndex, let section = self.sectors[selectedIndex].section { + self.delegate?.didSelectItem(self, type: section.model.type) + } + self.deSelectSegment(at: self.currentSectorIndex) + } + + + //MARK: - Construct Menu UI + + private func createSector(at index: Int, itemsCount: Int, angle: CGFloat) -> Sector { + var endAngle = self.segmentAngle * CGFloat(index) + angle/2 + + if itemsCount == 2 { + endAngle = angle * CGFloat(index) + } + + let startAngle = endAngle - angle + let midAngle = startAngle + angle/2 + + let sector = Sector(index: index, + min: startAngle, + mid: midAngle, + max: endAngle) + return sector + } + + private func createSeparator(padding: CGFloat, height: CGFloat, angle: CGFloat, center: CGPoint, color: UIColor) -> UIView { + let separator = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 1, height: height))) + separator.backgroundColor = color + separator.layer.anchorPoint = CGPoint(x: 0.5, y: 1) + + let separatorStartPoint = self.getPointCoordinate(withCenterIn: center, + radius: padding, + angle: angle - .pi/2) + separator.layer.position = separatorStartPoint + separator.transform = CGAffineTransform(rotationAngle: angle) + return separator + } + + private func createSection(in view: UIView, rotationAngle: CGFloat, model: CircleMenuItem) -> SectionView { + let sectionRect = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: view.bounds.height/2)) + let section = SectionView(frame: sectionRect, model: model, angle: 2 * .pi - rotationAngle) + + section.backgroundColor = .clear + section.isHighlighted = false + + section.layer.anchorPoint = CGPoint(x: 0.5, y: 1) + section.layer.position = view.center + section.transform = CGAffineTransform(rotationAngle: rotationAngle) + return section + } + + //MARK: Private methods + + private func selectSegment(at index: Int) { + if let section = self.sectors[index].section { + self.currentSectorIndex = index + section.isHighlighted = true + } + } + + private func deSelectSegment(at index: Int?) { + if let thisIndex = index, let section = self.sectors[thisIndex].section { + self.currentSectorIndex = nil + section.isHighlighted = false + } + } + + private func detectSelectedSegment(from touchPoint: CGPoint) -> Sector? { + // Get inverted touch angle value + let dx = container.center.x - touchPoint.x + let dy = container.center.y - touchPoint.y + let angle = -atan2(dx, dy) + + for sector in self.sectors { + let newAngle = angle.toPositiveRadians + + let min = sector.minValue.toPositiveRadians + let max = sector.maxValue.toPositiveRadians + + let regularCondition = newAngle > min && newAngle < max + let firstElementCondition = min > max && (newAngle > min && newAngle <= 2 * .pi || newAngle < max) + + if regularCondition || firstElementCondition { + return sector + } + } + return nil + } + + private func calculateDistance(fromCenter point: CGPoint) -> CGFloat { + let center = CGPoint(x: bounds.size.width/2, y: bounds.size.height/2) + let dx = CGFloat(point.x - center.x) + let dy = CGFloat(point.y - center.y) + + return sqrt(pow(dx, 2) + pow(dy, 2)) + } + + private func ignoreTaps(for touchPoint: CGPoint) -> Bool { + let dist: CGFloat = calculateDistance(fromCenter: touchPoint) + + if dist < self.separatorPadding || dist > self.container.bounds.width/2 { + print("Ignoring tap \(touchPoint.x), \(touchPoint.y)") + self.deSelectSegment(at: self.currentSectorIndex) + return false + } + return true + } + + private func getPointCoordinate(withCenterIn center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint { + return CGPoint(x: center.x + radius*cos(angle), y: center.y + radius*sin(angle)) + } + + private func drawSegmentPoints(inView view: UIView, innerPadding: CGFloat, side: CGFloat, leadingAngle: CGFloat, trailingAngle: CGFloat) { + let bottomCenter = CGPoint(x: view.bounds.width/2, y: view.bounds.height) + + let leadingBottomPoint = self.getPointCoordinate(withCenterIn: bottomCenter, + radius: innerPadding, + angle: leadingAngle) + let leadingTopPoint = self.getPointCoordinate(withCenterIn: bottomCenter, + radius: innerPadding + side, + angle: leadingAngle) + + let trailingBottomPoint = self.getPointCoordinate(withCenterIn: bottomCenter, + radius: innerPadding, + angle: trailingAngle) + let path = UIBezierPath() + path.move(to: leadingBottomPoint) + path.addLine(to: leadingTopPoint) + path.addArc(withCenter: bottomCenter, radius: innerPadding + side, startAngle: leadingAngle, endAngle: trailingAngle, clockwise: true) + path.addLine(to: trailingBottomPoint) + path.addArc(withCenter: bottomCenter, radius: innerPadding, startAngle: trailingAngle, endAngle: leadingAngle, clockwise: false) + path.close() + + view.applyMask(withPath: path, inverse: false) + } +} diff --git a/Nynja/CircleMenuControl/Core/CircleMenuDelegate.swift b/Nynja/CircleMenuControl/Core/CircleMenuDelegate.swift new file mode 100644 index 000000000..135f692ca --- /dev/null +++ b/Nynja/CircleMenuControl/Core/CircleMenuDelegate.swift @@ -0,0 +1,14 @@ +// +// CircleMenuDelegate.swift +// CircleMenuControl +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import Foundation + +protocol CircleMenuDelegate: class { + func didSelectItem(_ menu: CircleMenu, type: CircleMenuItemType) + func didChangeCurrentSet(_ menu: CircleMenu, title: String) +} diff --git a/Nynja/CircleMenuControl/Core/Sector/SectionView.swift b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift new file mode 100644 index 000000000..e4978fdf2 --- /dev/null +++ b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift @@ -0,0 +1,136 @@ +// +// SectionView.swift +// rrrrrr +// +// Created by Roman Chopovenko on 7/23/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import UIKit + +fileprivate let iconSide: CGFloat = 34 +fileprivate let padding: CGFloat = 8 +fileprivate let labelHeight: CGFloat = 22 +fileprivate let height = iconSide + padding + labelHeight + +class SectionView: UIView { + + var angle: CGFloat! + var model: CircleMenuItem! + + var isHighlighted: Bool = false { + didSet { + self.backgroundView.alpha = isHighlighted ? 1.0 : 0 + self.backgroundView.setNeedsDisplay() + } + } + + // MARK: - Views + lazy var backgroundView: UIView = { + let view = GradientView(colors: [Colors.gradientView.start, + Colors.gradientView.end]) + self.addSubview(view) + view.snp.makeConstraints({ (make) in + make.edges.equalToSuperview() + }) + return view + }() + + private lazy var buttonContentHolder: UIView = { + let view = UIView() + + view.backgroundColor = .clear + view.transform = CGAffineTransform(rotationAngle: self.angle) + + let shiftCenterY: CGFloat = 10.0 + let width: CGFloat = 2*height + + self.addSubview(view) + view.snp.makeConstraints({ (make) in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview().offset(-shiftCenterY) + make.width.equalTo(width) + make.height.equalTo(height) + }) + return view + }() + + private lazy var iconView: UIImageView = { + let icon = self.createIconView(with: self.model) + + buttonContentHolder.addSubview(icon) + icon.snp.makeConstraints({ (make) in + make.width.height.equalTo(iconSide) + make.top.centerX.equalToSuperview() + }) + return icon + }() + + private lazy var titleLabel: UILabel = { + let label = self.createTitleLabel(with: self.model) + + buttonContentHolder.addSubview(label) + label.snp.makeConstraints({ (make) in + make.height.equalTo(labelHeight) + make.top.equalTo(self.iconView.snp.bottom).offset(padding) + make.left.right.bottom.equalToSuperview() + }) + return label + }() + + // MARK: - Init + required init(frame: CGRect, model: CircleMenuItem, angle: CGFloat) { + super.init(frame: frame) + self.model = model + self.angle = angle + self.setupUI() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupUI() { + self.backgroundView.isHidden = false + self.titleLabel.isHidden = false + } + + // MARK: Private methods + private func createIconView(with model: CircleMenuItem) -> UIImageView { + let imageView = UIImageView(frame: .zero) + imageView.contentMode = .scaleAspectFit + imageView.image = model.icon.withRenderingMode(.alwaysTemplate) + imageView.tintColor = model.isEnabled ? Colors.iconView.enabled : Colors.iconView.disabled + return imageView + } + + private func createTitleLabel(with model: CircleMenuItem) -> UILabel { + let label = UILabel(frame: .zero) + label.numberOfLines = 1 + label.textAlignment = .center + let font = UIFont(name: Constants.fonts.medium, size: 12) + label.font = font + label.text = model.title + label.textColor = model.isEnabled ? Colors.titleLabel.enabled : Colors.titleLabel.disabled + return label + } +} + +extension SectionView { + struct Colors { + struct titleLabel { + static let enabled: UIColor = Constants.colors.marketplaceMunu.activeTitle.getColor() + static let disabled: UIColor = Constants.colors.marketplaceMunu.inActiveTitle.getColor() + } + + struct iconView { + static let enabled: UIColor = Constants.colors.marketplaceMunu.activeIcon.getColor() + static let disabled: UIColor = Constants.colors.marketplaceMunu.inactiveIcon.getColor() + } + + struct gradientView { + static let start: UIColor = Constants.colors.marketplaceMunu.gradient.getColor(withAlpha: 0.1) + static let end: UIColor = Constants.colors.marketplaceMunu.gradient.getColor(withAlpha: 0.5) + } + } +} diff --git a/Nynja/CircleMenuControl/Core/Sector/Sector.swift b/Nynja/CircleMenuControl/Core/Sector/Sector.swift new file mode 100644 index 000000000..7b7210065 --- /dev/null +++ b/Nynja/CircleMenuControl/Core/Sector/Sector.swift @@ -0,0 +1,25 @@ +// +// Sector.swift +// rrrrrr +// +// Created by Roman Chopovenko on 7/19/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import UIKit + +class Sector { + var minValue: CGFloat + var midValue: CGFloat + var maxValue: CGFloat + var index: Int + + var section: SectionView? + + required init(index: Int, min: CGFloat, mid: CGFloat, max: CGFloat) { + self.index = index + self.minValue = min + self.midValue = mid + self.maxValue = max + } +} diff --git a/Nynja/CircleMenuControl/Extension/UIView+Mask.swift b/Nynja/CircleMenuControl/Extension/UIView+Mask.swift new file mode 100644 index 000000000..deb0a6efc --- /dev/null +++ b/Nynja/CircleMenuControl/Extension/UIView+Mask.swift @@ -0,0 +1,50 @@ +// +// UIView+Mask.swift +// rrrrrr +// +// Created by Roman Chopovenko on 7/23/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import UIKit + +extension UIView { + func applyMask(withRect rect: CGRect, inverse: Bool = false) { + let path = UIBezierPath(rect: rect) + let maskLayer = CAShapeLayer() + + if inverse { + path.append(UIBezierPath(rect: self.bounds)) + maskLayer.fillRule = kCAFillRuleEvenOdd + } + + maskLayer.path = path.cgPath + + self.layer.mask = maskLayer + } + + func applyMask(withPath path: UIBezierPath, inverse: Bool = false) { + let path = path + let maskLayer = CAShapeLayer() + + if inverse { + path.append(UIBezierPath(rect: self.bounds)) + maskLayer.fillRule = kCAFillRuleEvenOdd + } + + maskLayer.path = path.cgPath + + self.layer.mask = maskLayer + } +} + +extension BinaryInteger { + var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 } +} + +extension FloatingPoint { + var degreesToRadians: Self { return self * .pi / 180 } + var radiansToDegrees: Self { return self * 180 / .pi } + + var toPositiveRadians: Self { return self < 0 ? 2 * .pi + self : self} +} diff --git a/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift b/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift new file mode 100644 index 000000000..0e3cfa38e --- /dev/null +++ b/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift @@ -0,0 +1,85 @@ +// +// CircleMenuFactory.swift +// CircleMenuControl +// +// Created by Roman Chopovenko on 7/23/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import UIKit + +enum CircleMenuItemType: String, RawRepresentable { + case marketplace + case freelance + case accessMarketplace + case virtualGoods + case stickers + case mediaContent + case groupsAndChannels + case bots + case apps + case interpretation + case nynjaSupport + case design + + var icon: UIImage { + + switch self { + case .freelance: return #imageLiteral(resourceName: "marketplace_freelance") + case .accessMarketplace: return #imageLiteral(resourceName: "marketplace_access") + case .virtualGoods: return #imageLiteral(resourceName: "marketplace_virtual_goods") + case .stickers: return #imageLiteral(resourceName: "marketplace_sticker") + case .mediaContent: return #imageLiteral(resourceName: "marketplace_media_content") + case .groupsAndChannels: return #imageLiteral(resourceName: "marketplace_groups_channels") + case .bots: return #imageLiteral(resourceName: "marketplace_bots") + case .apps: return #imageLiteral(resourceName: "marketplace_apps") + case .interpretation: return #imageLiteral(resourceName: "marketplace_interpretation") + case .nynjaSupport: return #imageLiteral(resourceName: "marketplace_support") + case .design: return #imageLiteral(resourceName: "marketplace_design") + default:return #imageLiteral(resourceName: "ava_placeholder") + } + } +} + +class CircleMenuFactory { + + var marketplace: CircleMenuSet { + let freelance = CircleMenuItem(type: .freelance, isEnabled: true) + + + let accessMarketPlace = CircleMenuItem(type: .accessMarketplace, isEnabled: true) + let virtualGoods = CircleMenuItem(type: .virtualGoods, isEnabled: true) + + return CircleMenuSet(title: CircleMenuItemType.marketplace.localized.uppercased(), items: [freelance, accessMarketPlace, virtualGoods]) + } + + var virtualGoods: CircleMenuSet { + let stickers = CircleMenuItem(type: .stickers, isEnabled: false) + let mediaContent = CircleMenuItem(type: .mediaContent, isEnabled: false) + return CircleMenuSet(title: CircleMenuItemType.virtualGoods.localized.uppercased(), items: [stickers, mediaContent]) + } + + var accessMarketPlace: CircleMenuSet { + let groupsAndChannels = CircleMenuItem(type: .groupsAndChannels, isEnabled: false) + let apps = CircleMenuItem(type: .apps, isEnabled: false) + let bots = CircleMenuItem(type: .bots, isEnabled: false) + return CircleMenuSet(title: CircleMenuItemType.accessMarketplace.localized.uppercased(), items: [groupsAndChannels, apps, bots]) + } + + var freelance: CircleMenuSet { + let interpretation = CircleMenuItem(type: .interpretation, isEnabled: true) + let nynjaSupport = CircleMenuItem(type: .nynjaSupport, isEnabled: false) + let design = CircleMenuItem(type: .design, isEnabled: false) + let title = CircleMenuItemType.freelance.localized + " " + CircleMenuItemType.marketplace.localized + return CircleMenuSet(title: title.uppercased(), items: [interpretation, nynjaSupport, design]) + } + + var allElementsDictionary: [CircleMenuItemType : CircleMenuSet] { + return [.marketplace : self.marketplace, + .virtualGoods : self.virtualGoods, + .accessMarketplace : self.accessMarketPlace, + .freelance : self.freelance] + } + +} + diff --git a/Nynja/CircleMenuControl/Model/CircleMenuItem.swift b/Nynja/CircleMenuControl/Model/CircleMenuItem.swift new file mode 100644 index 000000000..a59a9be1a --- /dev/null +++ b/Nynja/CircleMenuControl/Model/CircleMenuItem.swift @@ -0,0 +1,18 @@ +// +// CircleMenuItem.swift +// CircleMenuControl +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import UIKit + +struct CircleMenuItem { + + let type: CircleMenuItemType + let isEnabled: Bool + + var icon: UIImage { return type.icon } + var title: String { return type.localized } +} diff --git a/Nynja/CircleMenuControl/Model/CircleMenuSet.swift b/Nynja/CircleMenuControl/Model/CircleMenuSet.swift new file mode 100644 index 000000000..11fd3cae6 --- /dev/null +++ b/Nynja/CircleMenuControl/Model/CircleMenuSet.swift @@ -0,0 +1,14 @@ +// +// CircleMenuSet.swift +// CircleMenuControl +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import Foundation + +struct CircleMenuSet { + let title: String + let items: [CircleMenuItem] +} diff --git a/Nynja/Modules/Main/MainProtocols.swift b/Nynja/Modules/Main/MainProtocols.swift index b922560f9..15b77d7c1 100644 --- a/Nynja/Modules/Main/MainProtocols.swift +++ b/Nynja/Modules/Main/MainProtocols.swift @@ -95,6 +95,7 @@ protocol MainWireFrameProtocol: class { func getRecentsLocation() -> [LocationType] func getStarredLocation() -> [LocationType] func getRecentsMedia() -> [Media] + func openMarketplace() // Group func showAddParticipants() diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 9dd2be624..6ae0efd79 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -112,6 +112,12 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato SelectCountryWireFrame().presentSelectCountry(navigation: navigation, main: self, selectCountryDelegate: selectCountryDelegate) } } + + func openMarketplace() { + if let navigation = self.navigation { + MarketplaceWireFrame().presentMarketplace(navigation: navigation, main: self) + } + } func showAddContactByUserName() { if let navigation = self.contentNavigation { diff --git a/Nynja/Modules/Marketplace/MarketplaceInteractor.swift b/Nynja/Modules/Marketplace/MarketplaceInteractor.swift new file mode 100644 index 000000000..8c338d31e --- /dev/null +++ b/Nynja/Modules/Marketplace/MarketplaceInteractor.swift @@ -0,0 +1,25 @@ +// +// MarketplaceInteractor.swift +// Nynja +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class MarketplaceInteractor: MarketplaceInteractorInputProtocol { + + weak var presenter: MarketplaceInteractorOutputProtocol! +} + +extension MarketplaceInteractor: SetInjectable { + func inject(dependencies: MarketplaceInteractor.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: MarketplaceInteractorOutputProtocol + } +} + diff --git a/Nynja/Modules/Marketplace/MarketplacePresenter.swift b/Nynja/Modules/Marketplace/MarketplacePresenter.swift new file mode 100644 index 000000000..1f5efadbc --- /dev/null +++ b/Nynja/Modules/Marketplace/MarketplacePresenter.swift @@ -0,0 +1,38 @@ +// +// MarketplacePresenter.swift +// Nynja +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class MarketplacePresenter: BasePresenter, MarketplacePresenterProtocol, MarketplaceInteractorOutputProtocol { + + weak var view: MarketplaceViewProtocol! + var interactor: MarketplaceInteractorInputProtocol! + var wireFrame: MarketplaceWireFrameProtocol! + + func showed() { + + } + + func dismissMarketplace() { + self.wireFrame.dismissMarketplace() + } +} + +extension MarketplacePresenter: SetInjectable { + func inject(dependencies: MarketplacePresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireFrame = dependencies.wireFrame + } + + struct Dependencies { + let view: MarketplaceViewProtocol + let interactor: MarketplaceInteractorInputProtocol + let wireFrame: MarketplaceWireFrameProtocol + } +} diff --git a/Nynja/Modules/Marketplace/MarketplaceProtocols.swift b/Nynja/Modules/Marketplace/MarketplaceProtocols.swift new file mode 100644 index 000000000..3f0a0ec8f --- /dev/null +++ b/Nynja/Modules/Marketplace/MarketplaceProtocols.swift @@ -0,0 +1,59 @@ +// +// MarketplaceProtocols.swift +// Nynja +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol MarketplaceWireFrameProtocol: class { + + func presentMarketplace(navigation: UINavigationController, main: MainWireFrame?) + + /** + * Add here your methods for communication PRESENTER -> WIREFRAME + */ + func dismissMarketplace() +} + +protocol MarketplaceViewProtocol: class { + + var presenter: MarketplacePresenterProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> VIEW + */ + +} + +protocol MarketplacePresenterProtocol: class, BasePresenterProtocol { + + var view: MarketplaceViewProtocol! { get set } + var interactor: MarketplaceInteractorInputProtocol! { get set } + var wireFrame: MarketplaceWireFrameProtocol! { get set } + + /** + * Add here your methods for communication VIEW -> PRESENTER + */ + + func showed() + func dismissMarketplace() +} + +protocol MarketplaceInteractorOutputProtocol: class { + + /** + * Add here your methods for communication INTERACTOR -> PRESENTER + */ +} + +protocol MarketplaceInteractorInputProtocol: class { + + var presenter: MarketplaceInteractorOutputProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> INTERACTOR + */ +} diff --git a/Nynja/Modules/Marketplace/MarketplaceViewController.swift b/Nynja/Modules/Marketplace/MarketplaceViewController.swift new file mode 100644 index 000000000..2a998aff5 --- /dev/null +++ b/Nynja/Modules/Marketplace/MarketplaceViewController.swift @@ -0,0 +1,92 @@ +// +// MarketplaceViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class MarketplaceViewController: BaseVC, MarketplaceViewProtocol { + + var presenter: MarketplacePresenterProtocol! { + didSet { + _presenter = presenter + } + } + + private let initialMenuSet: CircleMenuSet = CircleMenuFactory().marketplace + + lazy var circleMenu: CircleMenu = { + let menu = CircleMenu(rect: .zero, delegate: self, menuSet: initialMenuSet) + self.view.addSubview(menu) + + let horizontalPaddingsSum: CGFloat = 2 * 16 + let side = (self.view.bounds.width - horizontalPaddingsSum) + + menu.snp.makeConstraints({ (make) in + make.centerX.centerY.equalToSuperview() + make.width.height.equalTo(side) + }) + return menu + }() + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + presenter.showed() + + self.navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: navigationView.isSeparatorVisible ?? false, + isVisibleBackButton: true, + title: title, + navigationHandler: self, + backButtonImage: UIImage(named:"ic_back_navigation"))) + self.screenTitle = self.initialMenuSet.title + } + + + // MARK: - UI Setup + + private func setupUI() { + self.circleMenu.isHidden = false + } + +} + +extension MarketplaceViewController: SetInjectable { + func inject(dependencies: MarketplaceViewController.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: MarketplacePresenterProtocol + } +} + +extension MarketplaceViewController: CircleMenuDelegate { + + func didSelectItem(_ menu: CircleMenu, type: CircleMenuItemType) { + guard let menuSet = CircleMenuFactory().allElementsDictionary[type] else { return } + menu.updateMenu(with: menuSet) + } + + func didChangeCurrentSet(_ menu: CircleMenu, title: String) { + self.screenTitle = title + } +} + +extension MarketplaceViewController: NavigationProtocol { + func back() { + let canNavigateBackInsideControl = self.circleMenu.navigateBackInMenuStack() + if canNavigateBackInsideControl == false { + self.presenter.dismissMarketplace() + } + } +} diff --git a/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift b/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift new file mode 100644 index 000000000..b84e6c939 --- /dev/null +++ b/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift @@ -0,0 +1,40 @@ +// +// MarketplaceWireFrame.swift +// Nynja +// +// Created by Roman Chopovenko on 7/24/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class MarketplaceWireFrame: MarketplaceWireFrameProtocol { + + weak var navigation: UINavigationController? + weak var main: MainWireFrame? + + func presentMarketplace(navigation: UINavigationController, main: MainWireFrame?) { + self.navigation = navigation + self.main = main + + let view = MarketplaceViewController() + let presenter = MarketplacePresenter() + let interactor = MarketplaceInteractor() + + // Connecting + let viewDependencies = MarketplaceViewController.Dependencies(presenter: presenter) + view.inject(dependencies: viewDependencies) + let presenterDependencies = MarketplacePresenter.Dependencies(view: view, interactor: interactor, wireFrame: self) + presenter.inject(dependencies: presenterDependencies) + let interactorDependencies = MarketplaceInteractor.Dependencies(presenter: presenter) + interactor.inject(dependencies: interactorDependencies) + + view.modalTransitionStyle = .crossDissolve + view.modalPresentationStyle = .overCurrentContext + navigation.pushViewController(view as UIViewController, animated: true) + } + + func dismissMarketplace() { + navigation?.popToRootViewController(animated: true) + } +} diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index e79f94391..2c75b9737 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -974,6 +974,10 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract func rejoinRunningCall() { interactor.rejoinRunningCall() } + + func openMarketplaceScreen() { + self.wireFrame.openMarketplaceScreen() + } } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 4ae1ebb15..22823aeb0 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -39,6 +39,7 @@ protocol MessageWireframeProtocol: DocumentInteractionInput { func openChat(_ chat: ChatModel, originLocalId: String?) func showLanguageSelector(input: LanguageSelector.Input) func show(alert: UIAlertController) + func openMarketplaceScreen() } //MARK: Presenter - @@ -122,6 +123,7 @@ protocol MessagePresenterProtocol: BasePresenterProtocol, func hasRunningCall() -> Bool func rejoinRunningCall() + func openMarketplaceScreen() } diff --git a/Nynja/Modules/Message/View/MessageVC+ContextMenuDelegate.swift b/Nynja/Modules/Message/View/MessageVC+ContextMenuDelegate.swift index 5e6be46c7..90875402b 100644 --- a/Nynja/Modules/Message/View/MessageVC+ContextMenuDelegate.swift +++ b/Nynja/Modules/Message/View/MessageVC+ContextMenuDelegate.swift @@ -161,6 +161,6 @@ extension MessageVC: NynjaContextMenuDelegate { } private func marketPlace(menu: NynjaContextMenu, userInfo: NynjaContextMenuUserInfo?) { - + self.presenter.openMarketplaceScreen() } } diff --git a/Nynja/Modules/Message/WireFrame/MessageWireframe.swift b/Nynja/Modules/Message/WireFrame/MessageWireframe.swift index a42b2d386..60acf1192 100644 --- a/Nynja/Modules/Message/WireFrame/MessageWireframe.swift +++ b/Nynja/Modules/Message/WireFrame/MessageWireframe.swift @@ -151,4 +151,9 @@ class MessageWireFrame: MessageWireframeProtocol, DocumentInteractionWireFrame { func show(alert: UIAlertController) { navigation?.present(alert, animated: true, completion: nil) } + + func openMarketplaceScreen() { + main?.openMarketplace() + } + } diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/Contents.json b/Nynja/Resources/Assets.xcassets/Marketplace/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Marketplace/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/marketplace_swap_button.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Marketplace/marketplace_swap_button.imageset/Contents.json new file mode 100644 index 000000000..497e4b30d --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Marketplace/marketplace_swap_button.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "marketplace_swap_button.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/marketplace_swap_button.imageset/marketplace_swap_button.pdf b/Nynja/Resources/Assets.xcassets/Marketplace/marketplace_swap_button.imageset/marketplace_swap_button.pdf new file mode 100644 index 0000000000000000000000000000000000000000..02beb1afb31dec6cac23b7d2528020e659270423 GIT binary patch literal 12016 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 akUz`K4M!=B9~&YDl@tT>^Q-Erf&T-Pe9C?R literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_bots.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_bots.imageset/Contents.json new file mode 100644 index 000000000..92ea8cf36 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_bots.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "marketplace_bots.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_bots.imageset/marketplace_bots.pdf b/Nynja/Resources/Assets.xcassets/Marketplace/menu/access/marketplace_bots.imageset/marketplace_bots.pdf new file mode 100644 index 0000000000000000000000000000000000000000..775ed007da562d6b007d7255a41a2b4af1f63ad4 GIT binary patch literal 5831 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 literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/Contents.json new file mode 100644 index 000000000..d4e643bea --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "marketplace_interpretation.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/marketplace_interpretation.pdf b/Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/marketplace_interpretation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dc1a3df1f8405acdbcdca5f00f556c8357f7300c GIT binary patch literal 6447 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 literal 0 HcmV?d00001 diff --git a/Nynja/Resources/Constants.swift b/Nynja/Resources/Constants.swift index fff625bf7..9ff01b517 100644 --- a/Nynja/Resources/Constants.swift +++ b/Nynja/Resources/Constants.swift @@ -101,6 +101,18 @@ struct Constants { static let separator = Color(hex: "#2C2E33") } + + struct marketplaceMunu { + static let activeIcon = Color(hex: "#d80027") + static let inactiveIcon = Color(hex: "#505255") + static let activeTitle = colors.white + static let inActiveTitle = colors.darkGray + + static let separator = colors.red + + static let gradient = Color(hex: "#C90010") + } + } enum tableView { diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 960dd85eb..d7f10e6a8 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -827,3 +827,20 @@ "wallet_created" = "Wallet has been created"; "wallet_add_title" = "Add\nWallet"; "wallet_transfer_deletion_info" = "The message with Transfer Information will be deleted only for you in chat history, but this message will be still available in Transfer History"; + +//MARK: Marketplace + +//**Submenu titles < CircleMenuItemType case naming are constrained to this values > +"marketplace" = "Marketplace"; +"freelance" = "Freelance"; +"accessMarketplace" = "Access marketplace"; +"virtualGoods" = "Virtual goods"; +"stickers" = "Stickers"; +"mediaContent" = "Media Content"; +"groupsAndChannels" = "Groups and Channels"; +"bots" = "Bots"; +"apps" = "Apps"; +"interpretation" = "Interpretation"; +"nynjaSupport" = "NYNJA Support"; +"design" = "Design"; +//Submenu titles** diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings index 6443ea3ae..00ff486a2 100644 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ b/Nynja/Resources/ru.lproj/Localizable.strings @@ -722,6 +722,7 @@ "wallet_header" = "Кошелек"; "wallet_created" = "Кошелек успешно создан"; "wallet_add_title" = "Создать\nКошелек"; +"wallet_transfer_deletion_info" = "Сообщение с информацей о трансфере будет удалено только для вас в истории чата, но это сообщение будет по-прежнему доступно в истории передачи"; // MARK: Data and Storage "data_and_storage" = "Хранилище"; @@ -744,4 +745,20 @@ "files" = "Files"; "music" = "Music"; "gifs" = "GIFs"; -"wallet_transfer_deletion_info" = "Сообщение с информацей о трансфере будет удалено только для вас в истории чата, но это сообщение будет по-прежнему доступно в истории передачи"; + +//MARK: Marketplace + +//**Submenu titles < CircleMenuItemType case naming are constrained to this values > +"marketplace" = "Marketplace"; +"freelance" = "Freelance"; +"accessMarketplace" = "Access marketplace"; +"virtualGoods" = "Virtual goods"; +"stickers" = "Stickers"; +"mediaContent" = "Media Content"; +"groupsAndChannels" = "Groups and Channels"; +"bots" = "Bots"; +"apps" = "Apps"; +"interpretation" = "Interpretation"; +"nynjaSupport" = "NYNJA Support"; +"design" = "Design"; +//Submenu titles** -- GitLab From 691d91fe9a9f7504f3f878c09269830fb4d07188 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Thu, 2 Aug 2018 15:04:39 +0300 Subject: [PATCH 04/32] Interpretation Screen UI implemented --- Nynja.xcodeproj/project.pbxproj | 128 ++++++ Nynja/CircleMenuControl/Core/CircleMenu.swift | 1 + .../Core/Sector/SectionView.swift | 2 + .../Core/Sector/TheGradientView.swift | 43 ++ .../Model/CircleMenuFactory.swift | 8 +- .../Model/CircleMenuItem.swift | 1 - Nynja/Library/UI/Extensions/Localizable.swift | 8 + .../AssigningInterpreterInteractor.swift | 24 ++ .../AssigningInterpreterPresenter.swift | 34 ++ .../AssigningInterpreterProtocols.swift | 57 +++ .../AssigningInterpreterViewController.swift | 45 ++ .../AssigningInterpreterWireFrame.swift | 35 ++ .../Interactor/InterpretationInteractor.swift | 24 ++ .../InterpretationProtocols.swift | 56 +++ .../Presenter/InterpretationPresenter.swift | 34 ++ .../View/InterpretationLayout.swift | 91 ++++ .../View/InterpretationViewController.swift | 405 ++++++++++++++++++ .../View/LanguagePickerDelegate.swift | 79 ++++ .../Wireframe/InterpretationWireFrame.swift | 35 ++ .../InterpretationTypeInteractor.swift | 24 ++ .../InterpretationTypePresenter.swift | 34 ++ .../InterpretationTypeProtocols.swift | 57 +++ .../InterpretationTypeViewController.swift | 45 ++ .../InterpretationTypeWireFrame.swift | 35 ++ .../Marketplace/MarketplacePresenter.swift | 4 + .../Marketplace/MarketplaceProtocols.swift | 5 +- .../MarketplaceViewController.swift | 5 +- .../Marketplace/MarketplaceWireFrame.swift | 10 + Nynja/Resources/en.lproj/Localizable.strings | 2 + Nynja/Resources/ru.lproj/Localizable.strings | 2 + 30 files changed, 1323 insertions(+), 10 deletions(-) create mode 100644 Nynja/CircleMenuControl/Core/Sector/TheGradientView.swift create mode 100644 Nynja/Modules/AssigningInterpreter/AssigningInterpreterInteractor.swift create mode 100644 Nynja/Modules/AssigningInterpreter/AssigningInterpreterPresenter.swift create mode 100644 Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift create mode 100644 Nynja/Modules/AssigningInterpreter/AssigningInterpreterViewController.swift create mode 100644 Nynja/Modules/AssigningInterpreter/AssigningInterpreterWireFrame.swift create mode 100644 Nynja/Modules/Interpretation/Interactor/InterpretationInteractor.swift create mode 100644 Nynja/Modules/Interpretation/InterpretationProtocols.swift create mode 100644 Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift create mode 100644 Nynja/Modules/Interpretation/View/InterpretationLayout.swift create mode 100644 Nynja/Modules/Interpretation/View/InterpretationViewController.swift create mode 100644 Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift create mode 100644 Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift create mode 100644 Nynja/Modules/InterpretationType/InterpretationTypeInteractor.swift create mode 100644 Nynja/Modules/InterpretationType/InterpretationTypePresenter.swift create mode 100644 Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift create mode 100644 Nynja/Modules/InterpretationType/InterpretationTypeViewController.swift create mode 100644 Nynja/Modules/InterpretationType/InterpretationTypeWireFrame.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 74b61ab42..79e120a5d 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1579,9 +1579,13 @@ B723C632204D9E5100884FFD /* DataAndStorageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C631204D9E5100884FFD /* DataAndStorageOption.swift */; }; B723C634204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C633204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift */; }; B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C635204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift */; }; + + B745F2E42109B9E500488A91 /* LanguagePickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */; }; + B745F2E62109BB0100488A91 /* InterpretationLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B745F2E52109BB0100488A91 /* InterpretationLayout.swift */; }; B74BAFFB21076AFA0049CD27 /* CircleMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */; }; B74BAFFC21076AFA0049CD27 /* SectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF121076AFA0049CD27 /* SectionView.swift */; }; B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF221076AFA0049CD27 /* Sector.swift */; }; + B74BAFFF21076AFA0049CD27 /* CircleMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF421076AFA0049CD27 /* CircleMenuDelegate.swift */; }; B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF621076AFA0049CD27 /* UIView+Mask.swift */; }; B74BB00121076AFA0049CD27 /* CircleMenuSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF821076AFA0049CD27 /* CircleMenuSet.swift */; }; @@ -1591,12 +1595,27 @@ B750EF042046D69C00A99F9C /* SpeedMesurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B750EF032046D69C00A99F9C /* SpeedMesurement.swift */; }; B750EF062046D7C700A99F9C /* SpeedStringRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B750EF052046D7C700A99F9C /* SpeedStringRepresentable.swift */; }; B763DD9320AA1C3400A30B63 /* ContactCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B763DD9220AA1C3400A30B63 /* ContactCellLayout.swift */; }; + B77C11DB2109242200CCB42E /* AssigningInterpreterPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11D62109242200CCB42E /* AssigningInterpreterPresenter.swift */; }; + B77C11DC2109242200CCB42E /* AssigningInterpreterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11D72109242200CCB42E /* AssigningInterpreterViewController.swift */; }; + B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11D82109242200CCB42E /* AssigningInterpreterProtocols.swift */; }; + B77C11DE2109242200CCB42E /* AssigningInterpreterInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11D92109242200CCB42E /* AssigningInterpreterInteractor.swift */; }; + B77C11DF2109242200CCB42E /* AssigningInterpreterWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11DA2109242200CCB42E /* AssigningInterpreterWireFrame.swift */; }; + B77C11E62109254800CCB42E /* InterpretationTypePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11E12109254800CCB42E /* InterpretationTypePresenter.swift */; }; + B77C11E72109254800CCB42E /* InterpretationTypeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11E22109254800CCB42E /* InterpretationTypeViewController.swift */; }; + B77C11E82109254800CCB42E /* InterpretationTypeProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11E32109254800CCB42E /* InterpretationTypeProtocols.swift */; }; + B77C11E92109254800CCB42E /* InterpretationTypeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11E42109254800CCB42E /* InterpretationTypeInteractor.swift */; }; + B77C11EA2109254800CCB42E /* InterpretationTypeWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C11E52109254800CCB42E /* InterpretationTypeWireFrame.swift */; }; B79B996E20CA88D100BEF5DE /* InviteFriendsMessageComposeDelegateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79B996D20CA88D100BEF5DE /* InviteFriendsMessageComposeDelegateHandler.swift */; }; B79FA02B2107731400F286BF /* MarketplacePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0262107731400F286BF /* MarketplacePresenter.swift */; }; B79FA02C2107731400F286BF /* MarketplaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0272107731400F286BF /* MarketplaceViewController.swift */; }; B79FA02D2107731400F286BF /* MarketplaceProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0282107731400F286BF /* MarketplaceProtocols.swift */; }; B79FA02E2107731400F286BF /* MarketplaceInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA0292107731400F286BF /* MarketplaceInteractor.swift */; }; B79FA02F2107731400F286BF /* MarketplaceWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA02A2107731400F286BF /* MarketplaceWireFrame.swift */; }; + B79FA03621091ED000F286BF /* InterpretationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03121091ED000F286BF /* InterpretationPresenter.swift */; }; + B79FA03721091ED000F286BF /* InterpretationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03221091ED000F286BF /* InterpretationViewController.swift */; }; + B79FA03821091ED000F286BF /* InterpretationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03321091ED000F286BF /* InterpretationProtocols.swift */; }; + B79FA03921091ED000F286BF /* InterpretationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03421091ED000F286BF /* InterpretationInteractor.swift */; }; + B79FA03A21091ED000F286BF /* InterpretationWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */; }; B7F505192061158800C28FA1 /* SettingsArrowCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */; }; B7F5051B20611A0900C28FA1 /* DownloadSettingsArrowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */; }; B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */; }; @@ -3413,6 +3432,8 @@ B723C631204D9E5100884FFD /* DataAndStorageOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageOption.swift; sourceTree = ""; }; B723C633204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataAndStorageTableDataSource.swift; sourceTree = ""; }; B723C635204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataAndStorageTableDelegate.swift; sourceTree = ""; }; + B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePickerDelegate.swift; sourceTree = ""; }; + B745F2E52109BB0100488A91 /* InterpretationLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationLayout.swift; sourceTree = ""; }; B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMenu.swift; sourceTree = ""; }; B74BAFF121076AFA0049CD27 /* SectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionView.swift; sourceTree = ""; }; B74BAFF221076AFA0049CD27 /* Sector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sector.swift; sourceTree = ""; }; @@ -3425,6 +3446,16 @@ B750EF032046D69C00A99F9C /* SpeedMesurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedMesurement.swift; sourceTree = ""; }; B750EF052046D7C700A99F9C /* SpeedStringRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedStringRepresentable.swift; sourceTree = ""; }; B763DD9220AA1C3400A30B63 /* ContactCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCellLayout.swift; sourceTree = ""; }; + B77C11D62109242200CCB42E /* AssigningInterpreterPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssigningInterpreterPresenter.swift; sourceTree = ""; }; + B77C11D72109242200CCB42E /* AssigningInterpreterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssigningInterpreterViewController.swift; sourceTree = ""; }; + B77C11D82109242200CCB42E /* AssigningInterpreterProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssigningInterpreterProtocols.swift; sourceTree = ""; }; + B77C11D92109242200CCB42E /* AssigningInterpreterInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssigningInterpreterInteractor.swift; sourceTree = ""; }; + B77C11DA2109242200CCB42E /* AssigningInterpreterWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssigningInterpreterWireFrame.swift; sourceTree = ""; }; + B77C11E12109254800CCB42E /* InterpretationTypePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypePresenter.swift; sourceTree = ""; }; + B77C11E22109254800CCB42E /* InterpretationTypeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeViewController.swift; sourceTree = ""; }; + B77C11E32109254800CCB42E /* InterpretationTypeProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeProtocols.swift; sourceTree = ""; }; + B77C11E42109254800CCB42E /* InterpretationTypeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeInteractor.swift; sourceTree = ""; }; + B77C11E52109254800CCB42E /* InterpretationTypeWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeWireFrame.swift; sourceTree = ""; }; B79B996D20CA88D100BEF5DE /* InviteFriendsMessageComposeDelegateHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteFriendsMessageComposeDelegateHandler.swift; sourceTree = ""; }; B79D8BCE2020C35300184D5D /* UIEdgeInsets+Adjust.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Adjust.swift"; sourceTree = ""; }; B79FA0262107731400F286BF /* MarketplacePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplacePresenter.swift; sourceTree = ""; }; @@ -3432,6 +3463,11 @@ B79FA0282107731400F286BF /* MarketplaceProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceProtocols.swift; sourceTree = ""; }; B79FA0292107731400F286BF /* MarketplaceInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceInteractor.swift; sourceTree = ""; }; B79FA02A2107731400F286BF /* MarketplaceWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceWireFrame.swift; sourceTree = ""; }; + B79FA03121091ED000F286BF /* InterpretationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationPresenter.swift; sourceTree = ""; }; + B79FA03221091ED000F286BF /* InterpretationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationViewController.swift; sourceTree = ""; }; + B79FA03321091ED000F286BF /* InterpretationProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationProtocols.swift; sourceTree = ""; }; + B79FA03421091ED000F286BF /* InterpretationInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationInteractor.swift; sourceTree = ""; }; + B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationWireFrame.swift; sourceTree = ""; }; B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsArrowCellViewModel.swift; sourceTree = ""; }; B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadSettingsArrowModel.swift; sourceTree = ""; }; B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageItemsFactory.swift; sourceTree = ""; }; @@ -5873,6 +5909,9 @@ E1BF3560A2E8EE8B02A9A9FB /* DateTimePicker */, 859B86352048224B003272B2 /* Settings */, B79FA025210772CF00F286BF /* Marketplace */, + B79FA03021091EA400F286BF /* Interpretation */, + B77C11E0210924F800CCB42E /* InterpretationType */, + B77C11D5210923F200CCB42E /* AssigningInterpreter */, ); path = Modules; sourceTree = ""; @@ -9967,6 +10006,40 @@ path = TableView; sourceTree = ""; }; + B745F2DF2109B94300488A91 /* View */ = { + isa = PBXGroup; + children = ( + B79FA03221091ED000F286BF /* InterpretationViewController.swift */, + B745F2E52109BB0100488A91 /* InterpretationLayout.swift */, + B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */, + ); + path = View; + sourceTree = ""; + }; + B745F2E02109B99F00488A91 /* Presenter */ = { + isa = PBXGroup; + children = ( + B79FA03121091ED000F286BF /* InterpretationPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + B745F2E12109B9A800488A91 /* Interactor */ = { + isa = PBXGroup; + children = ( + B79FA03421091ED000F286BF /* InterpretationInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + B745F2E22109B9B600488A91 /* Wireframe */ = { + isa = PBXGroup; + children = ( + B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; B74BAFED21076ADB0049CD27 /* CircleMenuControl */ = { isa = PBXGroup; children = ( @@ -9992,6 +10065,7 @@ children = ( B74BAFF121076AFA0049CD27 /* SectionView.swift */, B74BAFF221076AFA0049CD27 /* Sector.swift */, + B74BAFF321076AFA0049CD27 /* TheGradientView.swift */, ); path = Sector; sourceTree = ""; @@ -10014,6 +10088,30 @@ path = Model; sourceTree = ""; }; + B77C11D5210923F200CCB42E /* AssigningInterpreter */ = { + isa = PBXGroup; + children = ( + B77C11D62109242200CCB42E /* AssigningInterpreterPresenter.swift */, + B77C11D72109242200CCB42E /* AssigningInterpreterViewController.swift */, + B77C11D82109242200CCB42E /* AssigningInterpreterProtocols.swift */, + B77C11D92109242200CCB42E /* AssigningInterpreterInteractor.swift */, + B77C11DA2109242200CCB42E /* AssigningInterpreterWireFrame.swift */, + ); + path = AssigningInterpreter; + sourceTree = ""; + }; + B77C11E0210924F800CCB42E /* InterpretationType */ = { + isa = PBXGroup; + children = ( + B77C11E32109254800CCB42E /* InterpretationTypeProtocols.swift */, + B77C11E22109254800CCB42E /* InterpretationTypeViewController.swift */, + B77C11E12109254800CCB42E /* InterpretationTypePresenter.swift */, + B77C11E42109254800CCB42E /* InterpretationTypeInteractor.swift */, + B77C11E52109254800CCB42E /* InterpretationTypeWireFrame.swift */, + ); + path = InterpretationType; + sourceTree = ""; + }; B79B996F20CA89BA00BEF5DE /* MessageComposeHandler */ = { isa = PBXGroup; children = ( @@ -10034,6 +10132,18 @@ path = Marketplace; sourceTree = ""; }; + B79FA03021091EA400F286BF /* Interpretation */ = { + isa = PBXGroup; + children = ( + B79FA03321091ED000F286BF /* InterpretationProtocols.swift */, + B745F2DF2109B94300488A91 /* View */, + B745F2E02109B99F00488A91 /* Presenter */, + B745F2E12109B9A800488A91 /* Interactor */, + B745F2E22109B9B600488A91 /* Wireframe */, + ); + path = Interpretation; + sourceTree = ""; + }; B7B3AAAF204D3F1400756B77 /* TableHeaderFooter */ = { isa = PBXGroup; children = ( @@ -13276,10 +13386,12 @@ 859C42AA2056B05D00AE3797 /* SoundBundle.swift in Sources */, 265AEA151FE9AFA700AC4806 /* MemberHandler.swift in Sources */, E7C36C311FC4399B00740630 /* DBService.swift in Sources */, + B77C11DE2109242200CCB42E /* AssigningInterpreterInteractor.swift in Sources */, C940514D204C7FAF00D72B04 /* DataAndStorageWireFrame.swift in Sources */, 26DCB22D2064B872001EF0AB /* ContactsProtocols.swift in Sources */, A42D52CA206A53AB00EEB952 /* muc_Spec.swift in Sources */, 850C301D204DA87A00DB26C2 /* PrivacyListProtocols.swift in Sources */, + B77C11DF2109242200CCB42E /* AssigningInterpreterWireFrame.swift in Sources */, 8520040D20D513B8007C0036 /* OpponentMessageStickerRepliedView.swift in Sources */, A458FAC420EBA58A0075D55E /* MuteChatService.swift in Sources */, 2661D12F1F373D1700F3E125 /* BorderView.swift in Sources */, @@ -13471,6 +13583,7 @@ A45F112E20B4218D00F45004 /* MessageContentProtocol.swift in Sources */, 0008E9132032D5AC003E316E /* MQTTServiceSchedule.swift in Sources */, A432CF1A20B4347D00993AFB /* MaterialTextInput.swift in Sources */, + B77C11E62109254800CCB42E /* InterpretationTypePresenter.swift in Sources */, B750EF062046D7C700A99F9C /* SpeedStringRepresentable.swift in Sources */, 8580BAE220BD99D200239D9D /* InputContent.swift in Sources */, A43B25AD20AB1DFA00FF8107 /* MyTextField.swift in Sources */, @@ -13675,6 +13788,7 @@ A42D52CB206A53AB00EEB952 /* CDR_Spec.swift in Sources */, 2603139220A0A4B9009AC66D /* LanguageSettingProtocol.swift in Sources */, F105C6BD20A1347E0091786A /* PhotoPreviewViewController.swift in Sources */, + B79FA03921091ED000F286BF /* InterpretationInteractor.swift in Sources */, 26A856242074C50D00C642EA /* ActionsView+ScheduleAction.swift in Sources */, B1B8ED3EDB12866323C9EE74 /* QRCodeGeneratorInteractor.swift in Sources */, 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */, @@ -13833,6 +13947,7 @@ A42D52B9206A53AA00EEB952 /* Profile_Spec.swift in Sources */, 26DCB25420692237001EF0AB /* Array+Feature.swift in Sources */, F1607B2E20B2DE8A00BDF60A /* CameraQRPreviewInteractor.swift in Sources */, + B77C11EA2109254800CCB42E /* InterpretationTypeWireFrame.swift in Sources */, 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, @@ -13937,6 +14052,7 @@ F117872920ACF2DB007A9A1B /* CameraSetting.swift in Sources */, E77D58971F98B91600FBE926 /* ProfileTableViewDelegate.swift in Sources */, B79FA02D2107731400F286BF /* MarketplaceProtocols.swift in Sources */, + B77C11DC2109242200CCB42E /* AssigningInterpreterViewController.swift in Sources */, A45F111420B4218D00F45004 /* SystemCell.swift in Sources */, 26C1A3E92031AAA30009F7F0 /* OtherUserViewController.swift in Sources */, 2606F3BC20BFE20500CF7F15 /* MessageInteractor+Translation.swift in Sources */, @@ -14023,6 +14139,7 @@ A4679B8620B2DA550021FE9C /* Array+Grouped.swift in Sources */, A432CF1D20B4427000993AFB /* MTIConfigProtocol.swift in Sources */, 4B06D3102028A472003B275B /* P2pChatsItemsFactory.swift in Sources */, + B77C11E92109254800CCB42E /* InterpretationTypeInteractor.swift in Sources */, 26DCB23E2064B9A7001EF0AB /* ContactsWireframe.swift in Sources */, 853E594F20D6AED2007799B9 /* Desc+Messages.swift in Sources */, A460324F2105C9A1009783DA /* InputsCachePolicy.swift in Sources */, @@ -14070,6 +14187,7 @@ A40F18B920BFD81B0091B09E /* EmptyStateView.swift in Sources */, 06E67B3C3ED6CE5F6A913762 /* MainProtocols.swift in Sources */, 85082DDF2045A8C2000AE4B2 /* WheelPosition.swift in Sources */, + B79FA03721091ED000F286BF /* InterpretationViewController.swift in Sources */, F1607B2820B2DE4D00BDF60A /* CameraQRPreviewPresenter.swift in Sources */, A42D51AB206A361400EEB952 /* Person.swift in Sources */, A415131E20DBD10400C2C01F /* MessageLinkDAO.swift in Sources */, @@ -14143,12 +14261,15 @@ 8557988720932401007050B8 /* StickerStaticMenuActionCollectionViewCell.swift in Sources */, 00E9825A205FC324008BF03D /* SessionDescView.swift in Sources */, E7229A4A1F8CAD72003AEE04 /* TutorialViewControllerLayout.swift in Sources */, + B79FA03821091ED000F286BF /* InterpretationProtocols.swift in Sources */, 85CB25DA20D723B900D5E565 /* StickerPackDAO.swift in Sources */, 2603068D20FFF9CA00C10DD9 /* MessageCallView.swift in Sources */, E70402BD1FF6972B00182D81 /* BaseView.swift in Sources */, A45F59AD2058263F00EAA780 /* RosterDAO.swift in Sources */, 4C5EEA13EBC6A8398F08DCD1 /* MainWireframe.swift in Sources */, A42D52CF206A53AB00EEB952 /* Person_Spec.swift in Sources */, + B77C11E82109254800CCB42E /* InterpretationTypeProtocols.swift in Sources */, + B77C11E72109254800CCB42E /* InterpretationTypeViewController.swift in Sources */, 005886C92030F13100FE2E89 /* NynjaTimeHoursDelegate.swift in Sources */, 4BAB9CE02035CAE700385520 /* ScheduleInfo.swift in Sources */, 8ECC067E1FC5BCC6002CF225 /* TransferManager.swift in Sources */, @@ -14257,8 +14378,10 @@ E7E6E3DE1FB2F37900401D9E /* ParticipantsDelegate.swift in Sources */, A42D51AA206A361400EEB952 /* error.swift in Sources */, 628E2C26BE0854DB1DF64990 /* SplashWireframe.swift in Sources */, + B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */, 26FA420E201812D600E6F6EC /* StarTableDS.swift in Sources */, F10B0E2820B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift in Sources */, + B74BAFFE21076AFA0049CD27 /* TheGradientView.swift in Sources */, E7C36C351FC448CB00740630 /* DBFeature.swift in Sources */, 4B06D3122028A4CF003B275B /* GroupChatsItemsFactory.swift in Sources */, 0062D93B2062EC4100B915AC /* PhoneContact.swift in Sources */, @@ -14378,6 +14501,7 @@ A45F110320B4218D00F45004 /* MessagePresenter.swift in Sources */, 26541F742007B9A200AAEACF /* MessageActionTable.swift in Sources */, 8502DB532061030100613C8C /* WheelPositionCollectionViewCell.swift in Sources */, + B79FA03621091ED000F286BF /* InterpretationPresenter.swift in Sources */, 8FD597B66534DDBD1DDB58AC /* FavoritesInteractor.swift in Sources */, 82FCF48AA4A8C04CC8B0B5B6 /* FavoritesWireframe.swift in Sources */, 26AD28391FFB0AF9009E4580 /* StorageObserver.swift in Sources */, @@ -14441,6 +14565,7 @@ 2689CDEA20C48AD8007816B9 /* TranslationManualView.swift in Sources */, 6C25C4720B043D98729C02C8 /* TopUpAccountPresenter.swift in Sources */, A42D51CE206A361400EEB952 /* Search.swift in Sources */, + B77C11DB2109242200CCB42E /* AssigningInterpreterPresenter.swift in Sources */, 26342CB220ECDDC400D2196B /* Encodable+Dictionary.swift in Sources */, 3CDA490701EC3FEAAC2E9AFE /* TopUpAccountInteractor.swift in Sources */, 26B32B691FE1715500888A0A /* WeakRef.swift in Sources */, @@ -14481,6 +14606,7 @@ 87DE79674FF430A52D2A0BB7 /* MyGroupAliasProtocols.swift in Sources */, A45F111E20B4218D00F45004 /* FileTransferInfoView.swift in Sources */, 0B79E13E95305A80847AA99F /* MyGroupAliasViewController.swift in Sources */, + B745F2E42109B9E500488A91 /* LanguagePickerDelegate.swift in Sources */, 990A25B2C84CE09B4CE64533 /* MyGroupAliasPresenter.swift in Sources */, D839883F9B7A8CD245A85701 /* MyGroupAliasInteractor.swift in Sources */, 4B06D3202028A9B1003B275B /* P2pChatItemsFactory.swift in Sources */, @@ -14639,6 +14765,7 @@ 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */, F1313B0220888FE600E04092 /* ThirdPartyServices.swift in Sources */, 9BD8E40720F3576F001384EC /* CallInProgressWireframe.swift in Sources */, + B745F2E62109BB0100488A91 /* InterpretationLayout.swift in Sources */, 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */, BDC42BA204F86F13E9FE24FA /* ScheduleMessageProtocols.swift in Sources */, 0062D9462062EC4100B915AC /* InviteFriendsViewControllerLayout.swift in Sources */, @@ -14658,6 +14785,7 @@ 8F4E135F7485898D06EEABAB /* ScheduleMessageWireframe.swift in Sources */, B83D907D324553FB792968EE /* TimeZoneSelectorProtocols.swift in Sources */, 06084879DD92F39E637C21F2 /* TimeZoneSelectorViewController.swift in Sources */, + B79FA03A21091ED000F286BF /* InterpretationWireFrame.swift in Sources */, 2648C4142069B52100863614 /* ChangeNumberStep3Protocols.swift in Sources */, A839130C3B1AEFC6EBDA71A4 /* TimeZoneSelectorPresenter.swift in Sources */, A81BC507BA308AEE05A9B5E1 /* TimeZoneSelectorInteractor.swift in Sources */, diff --git a/Nynja/CircleMenuControl/Core/CircleMenu.swift b/Nynja/CircleMenuControl/Core/CircleMenu.swift index 3f99f28eb..27cab0ffb 100644 --- a/Nynja/CircleMenuControl/Core/CircleMenu.swift +++ b/Nynja/CircleMenuControl/Core/CircleMenu.swift @@ -29,6 +29,7 @@ class CircleMenu: UIControl { self.setNeedsDisplay() } } + private var dataSource: CircleMenuSet? { return self.menuNavigationStack.last } diff --git a/Nynja/CircleMenuControl/Core/Sector/SectionView.swift b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift index e4978fdf2..761af4af6 100644 --- a/Nynja/CircleMenuControl/Core/Sector/SectionView.swift +++ b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift @@ -11,6 +11,7 @@ import UIKit fileprivate let iconSide: CGFloat = 34 fileprivate let padding: CGFloat = 8 fileprivate let labelHeight: CGFloat = 22 + fileprivate let height = iconSide + padding + labelHeight class SectionView: UIView { @@ -106,6 +107,7 @@ class SectionView: UIView { private func createTitleLabel(with model: CircleMenuItem) -> UILabel { let label = UILabel(frame: .zero) + label.numberOfLines = 1 label.textAlignment = .center let font = UIFont(name: Constants.fonts.medium, size: 12) diff --git a/Nynja/CircleMenuControl/Core/Sector/TheGradientView.swift b/Nynja/CircleMenuControl/Core/Sector/TheGradientView.swift new file mode 100644 index 000000000..336adce7e --- /dev/null +++ b/Nynja/CircleMenuControl/Core/Sector/TheGradientView.swift @@ -0,0 +1,43 @@ +// +// TheGradientView.swift +// rrrrrr +// +// Created by Roman Chopovenko on 7/21/18. +// Copyright © 2018 Roman Chopovenko. All rights reserved. +// + +import UIKit + +class TheGradientView: UIView { + + var startColor: UIColor = Constants.colors.marketplaceMunu.gradient.getColor(withAlpha: 0.1) + var endColor: UIColor = Constants.colors.marketplaceMunu.gradient.getColor(withAlpha: 0.5) + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .clear + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + let context = UIGraphicsGetCurrentContext()! + let colors = [startColor.cgColor, endColor.cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let colorLocations: [CGFloat] = [0.0, 0.8] + + let gradient = CGGradient(colorsSpace: colorSpace, + colors: colors as CFArray, + locations: colorLocations)! + + let startPoint = CGPoint.zero + let endPoint = CGPoint(x: 0, y: bounds.height) + context.drawLinearGradient(gradient, + start: startPoint, + end: endPoint, + options: []) + } +} diff --git a/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift b/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift index 0e3cfa38e..b2da07cb9 100644 --- a/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift +++ b/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift @@ -8,7 +8,8 @@ import UIKit -enum CircleMenuItemType: String, RawRepresentable { + +enum CircleMenuItemType: String { case marketplace case freelance case accessMarketplace @@ -21,7 +22,6 @@ enum CircleMenuItemType: String, RawRepresentable { case interpretation case nynjaSupport case design - var icon: UIImage { switch self { @@ -45,8 +45,6 @@ class CircleMenuFactory { var marketplace: CircleMenuSet { let freelance = CircleMenuItem(type: .freelance, isEnabled: true) - - let accessMarketPlace = CircleMenuItem(type: .accessMarketplace, isEnabled: true) let virtualGoods = CircleMenuItem(type: .virtualGoods, isEnabled: true) @@ -80,6 +78,4 @@ class CircleMenuFactory { .accessMarketplace : self.accessMarketPlace, .freelance : self.freelance] } - } - diff --git a/Nynja/CircleMenuControl/Model/CircleMenuItem.swift b/Nynja/CircleMenuControl/Model/CircleMenuItem.swift index a59a9be1a..ec488799c 100644 --- a/Nynja/CircleMenuControl/Model/CircleMenuItem.swift +++ b/Nynja/CircleMenuControl/Model/CircleMenuItem.swift @@ -9,7 +9,6 @@ import UIKit struct CircleMenuItem { - let type: CircleMenuItemType let isEnabled: Bool diff --git a/Nynja/Library/UI/Extensions/Localizable.swift b/Nynja/Library/UI/Extensions/Localizable.swift index 521411cb3..57dd6a276 100644 --- a/Nynja/Library/UI/Extensions/Localizable.swift +++ b/Nynja/Library/UI/Extensions/Localizable.swift @@ -26,6 +26,7 @@ enum Language: String { case chinese_simplified = "zh-Hans" case japanese = "ja" case korean = "ko" + case empty_default = "empty" static let allValues = [english, german, spanish, french, italian, portuguese, @@ -46,6 +47,12 @@ enum Language: String { } } + static var allValuesWithDefault: [Language] { + var languages = Language.allValues + languages.append(.empty_default) + return languages + } + var longValue : String { switch self { case .english: return "English" @@ -59,6 +66,7 @@ enum Language: String { case .chinese_simplified: return "Chinese Simplified (简体中文)" case .japanese: return "Japanese (日本語)" case .korean: return "Korean (한국어)" + case .empty_default: return "-" } } diff --git a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterInteractor.swift b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterInteractor.swift new file mode 100644 index 000000000..2f7d23247 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterInteractor.swift @@ -0,0 +1,24 @@ +// +// AssigningInterpreterInteractor.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AssigningInterpreterInteractor: AssigningInterpreterInteractorInputProtocol { + + weak var presenter: AssigningInterpreterInteractorOutputProtocol! +} + +extension AssigningInterpreterInteractor: SetInjectable { + func inject(dependencies: AssigningInterpreterInteractor.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: AssigningInterpreterInteractorOutputProtocol + } +} diff --git a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterPresenter.swift b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterPresenter.swift new file mode 100644 index 000000000..70094e9e9 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterPresenter.swift @@ -0,0 +1,34 @@ +// +// AssigningInterpreterPresenter.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AssigningInterpreterPresenter: BasePresenter, AssigningInterpreterPresenterProtocol, AssigningInterpreterInteractorOutputProtocol { + + weak var view: AssigningInterpreterViewProtocol! + var interactor: AssigningInterpreterInteractorInputProtocol! + var wireFrame: AssigningInterpreterWireFrameProtocol! + + func showed() { + + } +} + +extension AssigningInterpreterPresenter: SetInjectable { + func inject(dependencies: AssigningInterpreterPresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireFrame = dependencies.wireFrame + } + + struct Dependencies { + let view: AssigningInterpreterViewProtocol + let interactor: AssigningInterpreterInteractorInputProtocol + let wireFrame: AssigningInterpreterWireFrameProtocol + } +} diff --git a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift new file mode 100644 index 000000000..8286fe159 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift @@ -0,0 +1,57 @@ +// +// AssigningInterpreterProtocols.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol AssigningInterpreterWireFrameProtocol: class { + + func presentAssigningInterpreter(navigation: UINavigationController) + + /** + * Add here your methods for communication PRESENTER -> WIREFRAME + */ +} + +protocol AssigningInterpreterViewProtocol: class { + + var presenter: AssigningInterpreterPresenterProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> VIEW + */ + +} + +protocol AssigningInterpreterPresenterProtocol: class, BasePresenterProtocol { + + var view: AssigningInterpreterViewProtocol! { get set } + var interactor: AssigningInterpreterInteractorInputProtocol! { get set } + var wireFrame: AssigningInterpreterWireFrameProtocol! { get set } + + /** + * Add here your methods for communication VIEW -> PRESENTER + */ + + func showed() +} + +protocol AssigningInterpreterInteractorOutputProtocol: class { + + /** + * Add here your methods for communication INTERACTOR -> PRESENTER + */ +} + +protocol AssigningInterpreterInteractorInputProtocol: class { + + var presenter: AssigningInterpreterInteractorOutputProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> INTERACTOR + */ +} diff --git a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterViewController.swift b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterViewController.swift new file mode 100644 index 000000000..40fc10c20 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterViewController.swift @@ -0,0 +1,45 @@ +// +// AssigningInterpreterViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class AssigningInterpreterViewController: BaseVC, AssigningInterpreterViewProtocol { + + var presenter: AssigningInterpreterPresenterProtocol! { + didSet { + _presenter = presenter + } + } + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + presenter.showed() + } + + + // MARK: - UI Setup + + private func setupUI() { + } + +} + +extension AssigningInterpreterViewController: SetInjectable { + func inject(dependencies: AssigningInterpreterViewController.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: AssigningInterpreterPresenterProtocol + } +} diff --git a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterWireFrame.swift b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterWireFrame.swift new file mode 100644 index 000000000..951c71b20 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterWireFrame.swift @@ -0,0 +1,35 @@ +// +// AssigningInterpreterWireFrame.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class AssigningInterpreterWireFrame: AssigningInterpreterWireFrameProtocol { + + weak var navigation: UINavigationController? + + func presentAssigningInterpreter(navigation: UINavigationController) { + self.navigation = navigation + + let view = AssigningInterpreterViewController() + let presenter = AssigningInterpreterPresenter() + let interactor = AssigningInterpreterInteractor() + + // Connecting + let viewDependencies = AssigningInterpreterViewController.Dependencies(presenter: presenter) + view.inject(dependencies: viewDependencies) + let presenterDependencies = AssigningInterpreterPresenter.Dependencies(view: view, interactor: interactor, wireFrame: self) + presenter.inject(dependencies: presenterDependencies) + let interactorDependencies = AssigningInterpreterInteractor.Dependencies(presenter: presenter) + interactor.inject(dependencies: interactorDependencies) + + view.modalTransitionStyle = .crossDissolve + view.modalPresentationStyle = .overCurrentContext + navigation.pushViewController(view as UIViewController, animated: true) + } + +} diff --git a/Nynja/Modules/Interpretation/Interactor/InterpretationInteractor.swift b/Nynja/Modules/Interpretation/Interactor/InterpretationInteractor.swift new file mode 100644 index 000000000..a660e82cd --- /dev/null +++ b/Nynja/Modules/Interpretation/Interactor/InterpretationInteractor.swift @@ -0,0 +1,24 @@ +// +// InterpretationInteractor.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class InterpretationInteractor: InterpretationInteractorInputProtocol { + + weak var presenter: InterpretationInteractorOutputProtocol! +} + +extension InterpretationInteractor: SetInjectable { + func inject(dependencies: InterpretationInteractor.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: InterpretationInteractorOutputProtocol + } +} diff --git a/Nynja/Modules/Interpretation/InterpretationProtocols.swift b/Nynja/Modules/Interpretation/InterpretationProtocols.swift new file mode 100644 index 000000000..2baec5650 --- /dev/null +++ b/Nynja/Modules/Interpretation/InterpretationProtocols.swift @@ -0,0 +1,56 @@ +// +// InterpretationProtocols.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol InterpretationWireFrameProtocol: class { + + func presentInterpretation(navigation: UINavigationController) + + /** + * Add here your methods for communication PRESENTER -> WIREFRAME + */ +} + +protocol InterpretationViewProtocol: class { + + var presenter: InterpretationPresenterProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> VIEW + */ + +} + +protocol InterpretationPresenterProtocol: BasePresenterProtocol { + + var view: InterpretationViewProtocol! { get set } + var interactor: InterpretationInteractorInputProtocol! { get set } + var wireFrame: InterpretationWireFrameProtocol! { get set } + + /** + * Add here your methods for communication VIEW -> PRESENTER + */ + func showed() +} + +protocol InterpretationInteractorOutputProtocol: class { + + /** + * Add here your methods for communication INTERACTOR -> PRESENTER + */ +} + +protocol InterpretationInteractorInputProtocol: class { + + var presenter: InterpretationInteractorOutputProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> INTERACTOR + */ +} diff --git a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift new file mode 100644 index 000000000..ce1f0fd52 --- /dev/null +++ b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift @@ -0,0 +1,34 @@ +// +// InterpretationPresenter.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class InterpretationPresenter: BasePresenter, InterpretationPresenterProtocol, InterpretationInteractorOutputProtocol { + + weak var view: InterpretationViewProtocol! + var interactor: InterpretationInteractorInputProtocol! + var wireFrame: InterpretationWireFrameProtocol! + + func showed() { + + } +} + +extension InterpretationPresenter: SetInjectable { + func inject(dependencies: InterpretationPresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireFrame = dependencies.wireFrame + } + + struct Dependencies { + let view: InterpretationViewProtocol + let interactor: InterpretationInteractorInputProtocol + let wireFrame: InterpretationWireFrameProtocol + } +} diff --git a/Nynja/Modules/Interpretation/View/InterpretationLayout.swift b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift new file mode 100644 index 000000000..5572b9758 --- /dev/null +++ b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift @@ -0,0 +1,91 @@ +// +// InterpretationLayout.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension InterpretationViewController { + + // MARK: Text + struct Text { + struct defaultWhite { + static let height = CGFloat(22.adjustedByWidth) + static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let fontColor = Constants.colors.white.getColor() + } + struct defaultGray { + static let height = CGFloat(20.adjustedByWidth) + static let font = UIFont(fontName: Constants.fonts.regular, height: height) + static let fontColor = Constants.colors.gray.getColor() + } + } + + struct Constraints { + + static let sidePadding = 16.adjustedByWidth + static let interitemPadding = 8.adjustedByWidth + static let separatorHeight = 1 + + struct fromLangPickerView { + static let widthOffset = Constraints.swapButton.side/2 + static let height = 158.adjustedByWidth + } + + struct langForInterpretationLabel { + static let bottomPadding = 8.adjustedByWidth + } + + struct toLangPickerView { + static let bottomPadding = 16.adjustedByWidth + } + + struct swapButton { + static let side = 42.adjustedByWidth + static let topPadding = 8.adjustedByWidth + } + + struct fromLabel { + static let rightPadding = Constraints.swapButton.side + } + + struct interpretationTypeLabel { + static let bottomPadding = 8.adjustedByWidth + } + + struct interpretationTypeButton { + static let width = 30 + } + + struct searchButton { + static let height = 44.adjustedByWidth + static let topPading = 24.adjustedByWidth + static let bottomPadding = 16.adjustedByWidth + } + + struct separatorBottom { + static let topPading = 8.adjustedByWidth + } + } + + // MARK: String + enum Strings: String { + case interpretationTime = "Approximate interpretation time" + case langForInterpretation = "Language for interpretation" + case from = "From:" + case to = "To:" + case interpretationType = "Interpretation type" + case totalPrice = "Total price" + case search = "Search Interpreter" + } + + struct Colors { + struct gradientView { + static let colors: [UIColor] = [Constants.colors.marketplaceMunu.gradient.getColor(withAlpha: 0.1), + Constants.colors.marketplaceMunu.gradient.getColor(withAlpha: 0.5)] + } + } +} diff --git a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift new file mode 100644 index 000000000..73c21fb5b --- /dev/null +++ b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift @@ -0,0 +1,405 @@ +// +// InterpretationViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class InterpretationViewController: BaseVC, InterpretationViewProtocol { + + var presenter: InterpretationPresenterProtocol! { + didSet { + _presenter = presenter + } + } + + lazy var swipeBackHelper: SwipeBackHelper = { + return SwipeBackHelper(with: self, gestureCompletion: nil) + }() + + private var langFrom: Language = Language.empty_default + private var langTo: Language = Language.current + + private var fromLangDelegate: LanguagePickerDelegate! + private var toLangDelegate: LanguagePickerDelegate! + + lazy var interpretationTimeLabel: UILabel = { + let label = UILabel() + label.text = Strings.interpretationTime.localized + label.font = Text.defaultWhite.font + label.textColor = Text.defaultWhite.fontColor + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.top.equalTo(navigationView.snp.bottomMargin).offset(Constraints.sidePadding) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalTo(interpretationTimeButton.snp.left).offset(-Constraints.interitemPadding) + make.height.equalTo(Text.defaultWhite.height) + make.bottom.equalTo(separatorTop.snp.top).offset(-Constraints.sidePadding) + }) + return label + }() + + lazy var interpretationTimeButton: UIButton = { + let button = UIButton() + button.setTitle("30 min", for: .normal) + button.titleLabel?.font = Text.defaultGray.font + button.setTitleColor(Text.defaultGray.fontColor, for: .normal) + button.addTarget(self, action: #selector(interpretationTimeButtonTapped), for: .touchUpInside) + + self.view.addSubview(button) + + button.snp.makeConstraints({ (make) in + make.width.equalTo(30).priority(200) + make.top.equalTo(navigationView.snp.bottomMargin).offset(Constraints.sidePadding) + make.height.equalTo(Text.defaultGray.height) + make.right.equalToSuperview().inset(Constraints.sidePadding) + }) + return button + }() + + lazy var separatorTop: UIView = { + let separator = UIView() + separator.backgroundColor = Constants.colors.separatorGrayColor.getColor() + + self.view.addSubview(separator) + separator.snp.makeConstraints({ (make) in + make.height.equalTo(Constraints.separatorHeight) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().inset(Constraints.sidePadding) + make.bottom.equalTo(langForInterpretationLabel.snp.top).offset(-Constraints.sidePadding) + }) + return separator + }() + + lazy var langForInterpretationLabel: UILabel = { + let label = UILabel() + label.text = Strings.langForInterpretation.localized + label.font = Text.defaultWhite.font + label.textColor = Text.defaultWhite.fontColor + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.left.right.equalToSuperview().offset(Constraints.sidePadding) + make.height.equalTo(Text.defaultWhite.height) + make.bottom.equalTo(fromLabel.snp.top).offset(-Constraints.langForInterpretationLabel.bottomPadding) + }) + return label + }() + + lazy var fromLabel: UILabel = { + let label = UILabel() + label.text = Strings.from.localized + label.font = Text.defaultGray.font + label.textColor = Text.defaultGray.fontColor + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.height.equalTo(Text.defaultGray.height) + make.width.equalTo(fromLangPickerView) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalTo(toLabel.snp.left).offset(-Constraints.fromLabel.rightPadding) + make.top.equalTo(toLabel) + make.bottom.equalTo(fromLangPickerView.snp.top) + make.bottom.equalTo(swapTopLine.snp.top) + }) + return label + }() + + lazy var toLabel: UILabel = { + let label = UILabel() + label.text = Strings.to.localized + label.font = Text.defaultGray.font + label.textColor = Text.defaultGray.fontColor + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.height.equalTo(Text.defaultGray.height) + make.right.equalToSuperview().offset(Constraints.sidePadding) + make.bottom.equalTo(toLangPickerView.snp.top) + }) + return label + }() + + lazy var fromLangPickerView: UIPickerView = { + let picker = UIPickerView() + + self.view.addSubview(picker) + picker.snp.makeConstraints({ (make) in + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.height.equalTo(Constraints.fromLangPickerView.height) + make.width.equalToSuperview().dividedBy(2).offset(-Constraints.fromLangPickerView.widthOffset) + make.right.equalTo(swapButton.snp.left) + make.centerY.equalTo(swapButton) + + make.bottom.equalTo(interpretationTypeLabel.snp.top).offset(-Constraints.sidePadding) + make.bottom.equalTo(swapBottomLine.snp.bottom) + }) + return picker + }() + + lazy var swapButton: UIButton = { + let button = UIButton() + self.view.addSubview(button) + button.setImage(#imageLiteral(resourceName: "marketplace_swap_button"), for: .normal) + button.addTarget(self, action: #selector(swapButtonTapped), for: .touchUpInside) + + button.snp.makeConstraints({ (make) in + make.width.height.equalTo(Constraints.swapButton.side) + make.right.equalTo(toLangPickerView.snp.left) + make.top.equalTo(swapTopLine.snp.bottom).offset(Constraints.swapButton.topPadding) + make.centerX.equalTo(swapTopLine) + make.centerX.equalTo(swapBottomLine) + make.bottom.equalTo(swapBottomLine.snp.top) + }) + return button + }() + + lazy var swapTopLine: GradientView = { + let view = GradientView(colors: Colors.gradientView.colors) + self.view.addSubview(view) + + view.snp.makeConstraints({ (make) in + make.width.equalTo(Constraints.separatorHeight) + }) + return view + }() + + lazy var swapBottomLine: GradientView = { + let view = GradientView(colors: Colors.gradientView.colors) + view.transform = CGAffineTransform(rotationAngle: .pi) + self.view.addSubview(view) + + view.snp.makeConstraints({ (make) in + make.width.equalTo(Constraints.separatorHeight) + }) + return view + }() + + lazy var toLangPickerView: UIPickerView = { + let picker = UIPickerView() + self.view.addSubview(picker) + picker.snp.makeConstraints({ (make) in + make.height.equalTo(Constraints.fromLangPickerView.height) + make.right.equalToSuperview().offset(-Constraints.sidePadding) + make.bottom.equalTo(interpretationTypeButton.snp.top).offset(-Constraints.toLangPickerView.bottomPadding) + }) + return picker + }() + + lazy var interpretationTypeLabel: UILabel = { + let label = UILabel() + label.text = Strings.interpretationType.localized + label.font = Text.defaultWhite.font + label.textColor = Text.defaultWhite.fontColor + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.height.equalTo(Text.defaultWhite.height) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalTo(interpretationTypeButton.snp.left).offset(-Constraints.interitemPadding) + make.bottom.equalTo(separatorMiddle.snp.top).offset(-Constraints.interpretationTypeLabel.bottomPadding) + }) + return label + }() + + lazy var interpretationTypeButton: UIButton = { + let button = UIButton() + button.setTitle("General", for: .normal) + button.titleLabel?.font = Text.defaultGray.font + button.setTitleColor(Text.defaultGray.fontColor, for: .normal) + button.addTarget(self, action: #selector(interpretationTypeButtonTapped), for: .touchUpInside) + self.view.addSubview(button) + + button.snp.makeConstraints({ (make) in + make.height.equalTo(Text.defaultGray.height) + make.width.equalTo(Constraints.interpretationTypeButton.width).priority(200) + make.right.equalToSuperview().inset(Constraints.sidePadding) + }) + return button + }() + + lazy var separatorMiddle: UIView = { + let separator = UIView() + + separator.backgroundColor = Constants.colors.separatorGrayColor.getColor() + + view.addSubview(separator) + separator.snp.makeConstraints({ (make) in + make.height.equalTo(Constraints.separatorHeight) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().inset(Constraints.sidePadding) + }) + return separator + }() + + private lazy var searchButton: NynjaButton = { + + let button = NynjaButton(height: CGFloat(Constraints.searchButton.height)) + button.titleLabel?.font = Text.defaultWhite.font + button.setTitle(Strings.search.localized.uppercased(), for: .normal) + self.view.addSubview(button) + + button.addTarget(self, action: #selector(searchButtonTapped), for: .touchUpInside) + + button.snp.makeConstraints({ (make) in + make.height.equalTo(Constraints.searchButton.height) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().offset(-Constraints.sidePadding) + make.bottom.equalToSuperview().offset(-Constraints.searchButton.bottomPadding) + make.top.equalTo(separatorBottom.snp.bottom).offset(Constraints.searchButton.topPading) + }) + return button + }() + + lazy var separatorBottom: UIView = { + let separator = UIView() + + separator.backgroundColor = Constants.colors.separatorGrayColor.getColor() + + view.addSubview(separator) + separator.snp.makeConstraints({ (make) in + make.height.equalTo(Constraints.separatorHeight) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().inset(Constraints.sidePadding) + make.top.equalTo(totalPriceTitleLabel.snp.bottom).offset(Constraints.separatorBottom.topPading) + make.top.equalTo(totalPriceValueLabel.snp.bottom).offset(Constraints.separatorBottom.topPading) + }) + return separator + }() + + + lazy var totalPriceTitleLabel: UILabel = { + let label = UILabel() + label.text = Strings.totalPrice.localized + label.font = Text.defaultWhite.font + label.textColor = Text.defaultWhite.fontColor + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.height.equalTo(Text.defaultWhite.height) + make.right.equalTo(totalPriceValueLabel.snp.left).offset(-Constraints.interitemPadding) + }) + return label + }() + + lazy var totalPriceValueLabel: UILabel = { + let label = UILabel() + label.text = "12.0 NYN" + label.font = Text.defaultWhite.font + label.textColor = Constants.colors.red.getColor() + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.height.equalTo(Text.defaultWhite.height) + make.width.equalTo(30).priority(200) + make.right.equalToSuperview().offset(-Constraints.sidePadding) + }) + return label + }() + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + presenter.showed() + } + + // MARK: - UI Setup + + private func setupUI() { + swipeBackHelper.addGesture() + self.screenTitle = "interpretation".localized.uppercased() + self.interpretationTimeLabel.isHidden = false + self.searchButton.isHidden = false + self.setupPickers() + } + + private func setupPickers() { + self.setupLanguagePicker(picker: self.fromLangPickerView, with: fromLangDelegate, callback: self) + self.setupLanguagePicker(picker: self.toLangPickerView, with: self.toLangDelegate, callback: self) + + self.selectRow(with: self.langFrom, in: self.fromLangPickerView, animated: false) + self.selectRow(with: self.langTo, in: self.toLangPickerView, animated: false) + } + + private func setupLanguagePicker(picker: UIPickerView, with delegate: LanguagePickerDelegate, callback: LanguagePickerDelegateCallback) { + delegate.callback = callback + picker.dataSource = delegate + picker.delegate = delegate + } + + private func selectRow(with language: Language, in picker: UIPickerView, animated: Bool) { + var row: Int? = fromLangDelegate.positionIn(value: language) + if picker == self.toLangPickerView { + row = toLangDelegate.positionIn(value: language) + } + picker.selectRow(row ?? 0, inComponent: 0, animated: animated) + } + + // MARK: - Actions + @objc + private func swapButtonTapped() { + guard self.langFrom != .empty_default, self.langFrom != self.langTo else { return } + let temp = langFrom + + self.langFrom = langTo + self.langTo = temp + + self.selectRow(with: self.langFrom, in: self.fromLangPickerView, animated: true) + self.selectRow(with: self.langTo, in: toLangPickerView, animated: true) + } + + @objc + private func interpretationTimeButtonTapped() { + print("set interpretation TIME pressed") + } + + @objc + private func interpretationTypeButtonTapped() { + print("set interpretation TYPE pressed") + } + + @objc + private func searchButtonTapped() { + print("search interpreter pressed") + } +} + +extension InterpretationViewController: LanguagePickerDelegateCallback { + func didSelectLanguage(_ pickerView: UIPickerView, language: Language) { + if pickerView == self.fromLangPickerView { + self.langFrom = language + } else if pickerView == self.toLangPickerView { + self.langTo = language + } + } +} + +extension InterpretationViewController: SetInjectable { + func inject(dependencies: InterpretationViewController.Dependencies) { + self.presenter = dependencies.presenter + self.fromLangDelegate = dependencies.fromLangDelegate + self.toLangDelegate = dependencies.toLangDelegate + } + + struct Dependencies { + let presenter: InterpretationPresenterProtocol + let fromLangDelegate: LanguagePickerDelegate + let toLangDelegate: LanguagePickerDelegate + } +} diff --git a/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift b/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift new file mode 100644 index 000000000..7eacf60bb --- /dev/null +++ b/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift @@ -0,0 +1,79 @@ +// +// LanguagePickerDelegate.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol LanguagePickerDelegateCallback : class { + func didSelectLanguage(_ pickerView: UIPickerView, language : Language) +} + +class LanguagePickerDelegate : NSObject, UIPickerViewDataSource, UIPickerViewDelegate { + + private let rows = 100000 + var pickerViewMiddle: Int { return ((self.rows / self.data.count) / 2) * self.data.count} + + weak var callback : LanguagePickerDelegateCallback? + + private var data: [Language]! + + //MARK: - Init + required init(defaultEmptyLanguage: Bool) { + self.data = defaultEmptyLanguage ? Language.allValuesWithDefault : Language.allValues + } + + //MARK: - UIPickerViewDataSource + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return rows + } + + //MARK: - UIPickerViewDelegate + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + return CGFloat(Values.height) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + pickerView.hideLines() + return self.createLabel(forRow: row, pickerView: pickerView) + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + callback?.didSelectLanguage(pickerView, language: valueForRow(row)) + } + + func positionIn(value : Language) -> Int? { + if let i = data.index(of: value) { + return pickerViewMiddle + i + } + return nil + } + + private func createLabel(forRow row: Int, pickerView: UIPickerView) -> UILabel { + let size = CGSize(width: pickerView.bounds.width, + height: Values.height) + let label = UILabel(frame: CGRect(origin: .zero, size: size)) + label.text = valueForRow(row).longValue + label.font = Values.font + label.textAlignment = NSTextAlignment.center + label.textColor = Values.fontColor + return label + } + + private func valueForRow(_ row: Int) -> Language { + return self.data[row % data.count] + } + + struct Values { + static let height = CGFloat(24.adjustedByWidth) + static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let fontColor = Constants.colors.white.getColor() + } +} diff --git a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift new file mode 100644 index 000000000..1db8f4187 --- /dev/null +++ b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift @@ -0,0 +1,35 @@ +// +// InterpretationWireFrame.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class InterpretationWireFrame: InterpretationWireFrameProtocol { + + weak var navigation: UINavigationController? + + func presentInterpretation(navigation: UINavigationController) { + self.navigation = navigation + + let view = InterpretationViewController() + let presenter = InterpretationPresenter() + let interactor = InterpretationInteractor() + + // Connecting + let viewDependencies = InterpretationViewController.Dependencies(presenter: presenter, fromLangDelegate: LanguagePickerDelegate(defaultEmptyLanguage: true), toLangDelegate: LanguagePickerDelegate(defaultEmptyLanguage: false)) + view.inject(dependencies: viewDependencies) + let presenterDependencies = InterpretationPresenter.Dependencies(view: view, interactor: interactor, wireFrame: self) + presenter.inject(dependencies: presenterDependencies) + let interactorDependencies = InterpretationInteractor.Dependencies(presenter: presenter) + interactor.inject(dependencies: interactorDependencies) + + view.modalTransitionStyle = .crossDissolve + view.modalPresentationStyle = .overCurrentContext + navigation.pushViewController(view as UIViewController, animated: true) + } + +} diff --git a/Nynja/Modules/InterpretationType/InterpretationTypeInteractor.swift b/Nynja/Modules/InterpretationType/InterpretationTypeInteractor.swift new file mode 100644 index 000000000..85be4ae29 --- /dev/null +++ b/Nynja/Modules/InterpretationType/InterpretationTypeInteractor.swift @@ -0,0 +1,24 @@ +// +// InterpretationTypeInteractor.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class InterpretationTypeInteractor: InterpretationTypeInteractorInputProtocol { + + weak var presenter: InterpretationTypeInteractorOutputProtocol! +} + +extension InterpretationTypeInteractor: SetInjectable { + func inject(dependencies: InterpretationTypeInteractor.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: InterpretationTypeInteractorOutputProtocol + } +} diff --git a/Nynja/Modules/InterpretationType/InterpretationTypePresenter.swift b/Nynja/Modules/InterpretationType/InterpretationTypePresenter.swift new file mode 100644 index 000000000..99e2ea5f2 --- /dev/null +++ b/Nynja/Modules/InterpretationType/InterpretationTypePresenter.swift @@ -0,0 +1,34 @@ +// +// InterpretationTypePresenter.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class InterpretationTypePresenter: BasePresenter, InterpretationTypePresenterProtocol, InterpretationTypeInteractorOutputProtocol { + + weak var view: InterpretationTypeViewProtocol! + var interactor: InterpretationTypeInteractorInputProtocol! + var wireFrame: InterpretationTypeWireFrameProtocol! + + func showed() { + + } +} + +extension InterpretationTypePresenter: SetInjectable { + func inject(dependencies: InterpretationTypePresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireFrame = dependencies.wireFrame + } + + struct Dependencies { + let view: InterpretationTypeViewProtocol + let interactor: InterpretationTypeInteractorInputProtocol + let wireFrame: InterpretationTypeWireFrameProtocol + } +} diff --git a/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift b/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift new file mode 100644 index 000000000..6580b8dcd --- /dev/null +++ b/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift @@ -0,0 +1,57 @@ +// +// InterpretationTypeProtocols.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol InterpretationTypeWireFrameProtocol: class { + + func presentInterpretationType(navigation: UINavigationController) + + /** + * Add here your methods for communication PRESENTER -> WIREFRAME + */ +} + +protocol InterpretationTypeViewProtocol: class { + + var presenter: InterpretationTypePresenterProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> VIEW + */ + +} + +protocol InterpretationTypePresenterProtocol: class, BasePresenterProtocol { + + var view: InterpretationTypeViewProtocol! { get set } + var interactor: InterpretationTypeInteractorInputProtocol! { get set } + var wireFrame: InterpretationTypeWireFrameProtocol! { get set } + + /** + * Add here your methods for communication VIEW -> PRESENTER + */ + + func showed() +} + +protocol InterpretationTypeInteractorOutputProtocol: class { + + /** + * Add here your methods for communication INTERACTOR -> PRESENTER + */ +} + +protocol InterpretationTypeInteractorInputProtocol: class { + + var presenter: InterpretationTypeInteractorOutputProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> INTERACTOR + */ +} diff --git a/Nynja/Modules/InterpretationType/InterpretationTypeViewController.swift b/Nynja/Modules/InterpretationType/InterpretationTypeViewController.swift new file mode 100644 index 000000000..f1196dcab --- /dev/null +++ b/Nynja/Modules/InterpretationType/InterpretationTypeViewController.swift @@ -0,0 +1,45 @@ +// +// InterpretationTypeViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class InterpretationTypeViewController: BaseVC, InterpretationTypeViewProtocol { + + var presenter: InterpretationTypePresenterProtocol! { + didSet { + _presenter = presenter + } + } + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + presenter.showed() + } + + + // MARK: - UI Setup + + private func setupUI() { + } + +} + +extension InterpretationTypeViewController: SetInjectable { + func inject(dependencies: InterpretationTypeViewController.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: InterpretationTypePresenterProtocol + } +} diff --git a/Nynja/Modules/InterpretationType/InterpretationTypeWireFrame.swift b/Nynja/Modules/InterpretationType/InterpretationTypeWireFrame.swift new file mode 100644 index 000000000..65fb6d852 --- /dev/null +++ b/Nynja/Modules/InterpretationType/InterpretationTypeWireFrame.swift @@ -0,0 +1,35 @@ +// +// InterpretationTypeWireFrame.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class InterpretationTypeWireFrame: InterpretationTypeWireFrameProtocol { + + weak var navigation: UINavigationController? + + func presentInterpretationType(navigation: UINavigationController) { + self.navigation = navigation + + let view = InterpretationTypeViewController() + let presenter = InterpretationTypePresenter() + let interactor = InterpretationTypeInteractor() + + // Connecting + let viewDependencies = InterpretationTypeViewController.Dependencies(presenter: presenter) + view.inject(dependencies: viewDependencies) + let presenterDependencies = InterpretationTypePresenter.Dependencies(view: view, interactor: interactor, wireFrame: self) + presenter.inject(dependencies: presenterDependencies) + let interactorDependencies = InterpretationTypeInteractor.Dependencies(presenter: presenter) + interactor.inject(dependencies: interactorDependencies) + + view.modalTransitionStyle = .crossDissolve + view.modalPresentationStyle = .overCurrentContext + navigation.pushViewController(view as UIViewController, animated: true) + } + +} diff --git a/Nynja/Modules/Marketplace/MarketplacePresenter.swift b/Nynja/Modules/Marketplace/MarketplacePresenter.swift index 1f5efadbc..9d284f264 100644 --- a/Nynja/Modules/Marketplace/MarketplacePresenter.swift +++ b/Nynja/Modules/Marketplace/MarketplacePresenter.swift @@ -18,6 +18,10 @@ final class MarketplacePresenter: BasePresenter, MarketplacePresenterProtocol, M } + func openMarketplaceMenu(type: CircleMenuItemType) { + self.wireFrame.openMarketplaceMenu(type: type) + } + func dismissMarketplace() { self.wireFrame.dismissMarketplace() } diff --git a/Nynja/Modules/Marketplace/MarketplaceProtocols.swift b/Nynja/Modules/Marketplace/MarketplaceProtocols.swift index 3f0a0ec8f..7b44670a3 100644 --- a/Nynja/Modules/Marketplace/MarketplaceProtocols.swift +++ b/Nynja/Modules/Marketplace/MarketplaceProtocols.swift @@ -15,6 +15,7 @@ protocol MarketplaceWireFrameProtocol: class { /** * Add here your methods for communication PRESENTER -> WIREFRAME */ + func openMarketplaceMenu(type: CircleMenuItemType) func dismissMarketplace() } @@ -25,10 +26,9 @@ protocol MarketplaceViewProtocol: class { /** * Add here your methods for communication PRESENTER -> VIEW */ - } -protocol MarketplacePresenterProtocol: class, BasePresenterProtocol { +protocol MarketplacePresenterProtocol: BasePresenterProtocol { var view: MarketplaceViewProtocol! { get set } var interactor: MarketplaceInteractorInputProtocol! { get set } @@ -39,6 +39,7 @@ protocol MarketplacePresenterProtocol: class, BasePresenterProtocol { */ func showed() + func openMarketplaceMenu(type: CircleMenuItemType) func dismissMarketplace() } diff --git a/Nynja/Modules/Marketplace/MarketplaceViewController.swift b/Nynja/Modules/Marketplace/MarketplaceViewController.swift index 2a998aff5..01b0c6417 100644 --- a/Nynja/Modules/Marketplace/MarketplaceViewController.swift +++ b/Nynja/Modules/Marketplace/MarketplaceViewController.swift @@ -73,7 +73,10 @@ extension MarketplaceViewController: SetInjectable { extension MarketplaceViewController: CircleMenuDelegate { func didSelectItem(_ menu: CircleMenu, type: CircleMenuItemType) { - guard let menuSet = CircleMenuFactory().allElementsDictionary[type] else { return } + guard let menuSet = CircleMenuFactory().allElementsDictionary[type] else { + self.presenter.openMarketplaceMenu(type: type) + return + } menu.updateMenu(with: menuSet) } diff --git a/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift b/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift index b84e6c939..933ac921f 100644 --- a/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift +++ b/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift @@ -34,6 +34,16 @@ final class MarketplaceWireFrame: MarketplaceWireFrameProtocol { navigation.pushViewController(view as UIViewController, animated: true) } + func openMarketplaceMenu(type: CircleMenuItemType) { + guard let navigation = self.navigation else { return } + switch type { + case .interpretation: + InterpretationWireFrame().presentInterpretation(navigation: navigation) + default: + print("No subMenu found! Define action here") + } + } + func dismissMarketplace() { navigation?.popToRootViewController(animated: true) } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index d7f10e6a8..47db344df 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -844,3 +844,5 @@ "nynjaSupport" = "NYNJA Support"; "design" = "Design"; //Submenu titles** + +"interpretation" = "Interpretation"; diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings index 00ff486a2..3502a1511 100644 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ b/Nynja/Resources/ru.lproj/Localizable.strings @@ -762,3 +762,5 @@ "nynjaSupport" = "NYNJA Support"; "design" = "Design"; //Submenu titles** + +"interpretation" = "Interpretation"; -- GitLab From 4951af2345c6036acca04e194f0ef885344a3543 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Fri, 3 Aug 2018 14:29:25 +0300 Subject: [PATCH 05/32] InterpretationType Screen --- Nynja.xcodeproj/project.pbxproj | 88 +++++++++++-- Nynja/CircleMenuControl/Core/CircleMenu.swift | 4 +- .../Interpretation/InterpretationModel.swift | 17 +++ .../InterpretationProtocols.swift | 8 ++ .../Presenter/InterpretationPresenter.swift | 10 ++ .../View/InterpretationLayout.swift | 16 +-- .../View/InterpretationViewController.swift | 44 ++++--- .../Wireframe/InterpretationWireFrame.swift | 6 +- .../InterpretationTypeInteractor.swift | 23 ++++ .../InterpretationType.swift | 42 ++++++ .../InterpretationTypeProtocols.swift | 12 +- .../InterpretationTypePresenter.swift | 41 ++++++ .../InterpretationTypeViewController.swift | 77 +++++++++++ .../Cell/InterpretationTypeCell.swift | 121 ++++++++++++++++++ .../Cell/InterpretationTypeCellLayout.swift | 65 ++++++++++ .../Cell/InterpretationTypeCellModel.swift | 25 ++++ .../InterpretationTypeTableDataSource.swift | 29 +++++ .../InterpretationTypeTableDelegate.swift | 38 ++++++ .../InterpretationTypeWireFrame.swift | 43 +++++++ .../MarketplaceViewController.swift | 1 - .../Marketplace/MarketplaceWireFrame.swift | 2 +- Nynja/Resources/en.lproj/Localizable.strings | 19 +++ Nynja/Resources/ru.lproj/Localizable.strings | 19 +++ 23 files changed, 702 insertions(+), 48 deletions(-) create mode 100644 Nynja/Modules/Interpretation/InterpretationModel.swift create mode 100644 Nynja/Modules/InterpretationType/Interactor/InterpretationTypeInteractor.swift create mode 100644 Nynja/Modules/InterpretationType/InterpretationType.swift create mode 100644 Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift create mode 100644 Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift create mode 100644 Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift create mode 100644 Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift create mode 100644 Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellModel.swift create mode 100644 Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDataSource.swift create mode 100644 Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDelegate.swift create mode 100644 Nynja/Modules/InterpretationType/Wireframe/InterpretationTypeWireFrame.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 79e120a5d..f54414344 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1579,13 +1579,11 @@ B723C632204D9E5100884FFD /* DataAndStorageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C631204D9E5100884FFD /* DataAndStorageOption.swift */; }; B723C634204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C633204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift */; }; B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C635204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift */; }; - B745F2E42109B9E500488A91 /* LanguagePickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */; }; B745F2E62109BB0100488A91 /* InterpretationLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B745F2E52109BB0100488A91 /* InterpretationLayout.swift */; }; B74BAFFB21076AFA0049CD27 /* CircleMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */; }; B74BAFFC21076AFA0049CD27 /* SectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF121076AFA0049CD27 /* SectionView.swift */; }; B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF221076AFA0049CD27 /* Sector.swift */; }; - B74BAFFF21076AFA0049CD27 /* CircleMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF421076AFA0049CD27 /* CircleMenuDelegate.swift */; }; B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF621076AFA0049CD27 /* UIView+Mask.swift */; }; B74BB00121076AFA0049CD27 /* CircleMenuSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFF821076AFA0049CD27 /* CircleMenuSet.swift */; }; @@ -1616,6 +1614,13 @@ B79FA03821091ED000F286BF /* InterpretationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03321091ED000F286BF /* InterpretationProtocols.swift */; }; B79FA03921091ED000F286BF /* InterpretationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03421091ED000F286BF /* InterpretationInteractor.swift */; }; B79FA03A21091ED000F286BF /* InterpretationWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */; }; + B7EF8ED0210C501F00E0E981 /* InterpretationTypeTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ECF210C501F00E0E981 /* InterpretationTypeTableDataSource.swift */; }; + B7EF8ED2210C502D00E0E981 /* InterpretationTypeTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED1210C502D00E0E981 /* InterpretationTypeTableDelegate.swift */; }; + B7EF8ED4210C511C00E0E981 /* InterpretationTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED3210C511C00E0E981 /* InterpretationTypeCell.swift */; }; + B7EF8ED7210C598800E0E981 /* InterpretationTypeCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED6210C598800E0E981 /* InterpretationTypeCellLayout.swift */; }; + B7EF8ED9210C71E800E0E981 /* InterpretationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED8210C71E800E0E981 /* InterpretationType.swift */; }; + B7EF8EDB210C759400E0E981 /* InterpretationTypeCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8EDA210C759300E0E981 /* InterpretationTypeCellModel.swift */; }; + B7EF8EDD210CB0A200E0E981 /* InterpretationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8EDC210CB0A200E0E981 /* InterpretationModel.swift */; }; B7F505192061158800C28FA1 /* SettingsArrowCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */; }; B7F5051B20611A0900C28FA1 /* DownloadSettingsArrowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */; }; B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */; }; @@ -3468,6 +3473,13 @@ B79FA03321091ED000F286BF /* InterpretationProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationProtocols.swift; sourceTree = ""; }; B79FA03421091ED000F286BF /* InterpretationInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationInteractor.swift; sourceTree = ""; }; B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationWireFrame.swift; sourceTree = ""; }; + B7EF8ECF210C501F00E0E981 /* InterpretationTypeTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeTableDataSource.swift; sourceTree = ""; }; + B7EF8ED1210C502D00E0E981 /* InterpretationTypeTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeTableDelegate.swift; sourceTree = ""; }; + B7EF8ED3210C511C00E0E981 /* InterpretationTypeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeCell.swift; sourceTree = ""; }; + B7EF8ED6210C598800E0E981 /* InterpretationTypeCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeCellLayout.swift; sourceTree = ""; }; + B7EF8ED8210C71E800E0E981 /* InterpretationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationType.swift; sourceTree = ""; }; + B7EF8EDA210C759300E0E981 /* InterpretationTypeCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeCellModel.swift; sourceTree = ""; }; + B7EF8EDC210CB0A200E0E981 /* InterpretationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationModel.swift; sourceTree = ""; }; B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsArrowCellViewModel.swift; sourceTree = ""; }; B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadSettingsArrowModel.swift; sourceTree = ""; }; B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageItemsFactory.swift; sourceTree = ""; }; @@ -10065,7 +10077,6 @@ children = ( B74BAFF121076AFA0049CD27 /* SectionView.swift */, B74BAFF221076AFA0049CD27 /* Sector.swift */, - B74BAFF321076AFA0049CD27 /* TheGradientView.swift */, ); path = Sector; sourceTree = ""; @@ -10104,10 +10115,11 @@ isa = PBXGroup; children = ( B77C11E32109254800CCB42E /* InterpretationTypeProtocols.swift */, - B77C11E22109254800CCB42E /* InterpretationTypeViewController.swift */, - B77C11E12109254800CCB42E /* InterpretationTypePresenter.swift */, - B77C11E42109254800CCB42E /* InterpretationTypeInteractor.swift */, - B77C11E52109254800CCB42E /* InterpretationTypeWireFrame.swift */, + B7EF8ED8210C71E800E0E981 /* InterpretationType.swift */, + B7EF8ECA210C4E1400E0E981 /* View */, + B7EF8ECB210C4E1E00E0E981 /* Presenter */, + B7EF8ECC210C4E2800E0E981 /* Interactor */, + B7EF8ECD210C4E3000E0E981 /* Wireframe */, ); path = InterpretationType; sourceTree = ""; @@ -10136,6 +10148,7 @@ isa = PBXGroup; children = ( B79FA03321091ED000F286BF /* InterpretationProtocols.swift */, + B7EF8EDC210CB0A200E0E981 /* InterpretationModel.swift */, B745F2DF2109B94300488A91 /* View */, B745F2E02109B99F00488A91 /* Presenter */, B745F2E12109B9A800488A91 /* Interactor */, @@ -10152,6 +10165,59 @@ path = TableHeaderFooter; sourceTree = ""; }; + B7EF8ECA210C4E1400E0E981 /* View */ = { + isa = PBXGroup; + children = ( + B77C11E22109254800CCB42E /* InterpretationTypeViewController.swift */, + B7EF8ECE210C4FE500E0E981 /* TableView */, + ); + path = View; + sourceTree = ""; + }; + B7EF8ECB210C4E1E00E0E981 /* Presenter */ = { + isa = PBXGroup; + children = ( + B77C11E12109254800CCB42E /* InterpretationTypePresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + B7EF8ECC210C4E2800E0E981 /* Interactor */ = { + isa = PBXGroup; + children = ( + B77C11E42109254800CCB42E /* InterpretationTypeInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + B7EF8ECD210C4E3000E0E981 /* Wireframe */ = { + isa = PBXGroup; + children = ( + B77C11E52109254800CCB42E /* InterpretationTypeWireFrame.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + B7EF8ECE210C4FE500E0E981 /* TableView */ = { + isa = PBXGroup; + children = ( + B7EF8ECF210C501F00E0E981 /* InterpretationTypeTableDataSource.swift */, + B7EF8ED1210C502D00E0E981 /* InterpretationTypeTableDelegate.swift */, + B7EF8ED5210C593E00E0E981 /* Cell */, + ); + path = TableView; + sourceTree = ""; + }; + B7EF8ED5210C593E00E0E981 /* Cell */ = { + isa = PBXGroup; + children = ( + B7EF8ED3210C511C00E0E981 /* InterpretationTypeCell.swift */, + B7EF8EDA210C759300E0E981 /* InterpretationTypeCellModel.swift */, + B7EF8ED6210C598800E0E981 /* InterpretationTypeCellLayout.swift */, + ); + path = Cell; + sourceTree = ""; + }; B8DCBB4ACE8A650987F2D234 /* Presenter */ = { isa = PBXGroup; children = ( @@ -13183,6 +13249,7 @@ buildActionMask = 2147483647; files = ( A432CF1B20B4347D00993AFB /* MaterialTextView.swift in Sources */, + B7EF8ED0210C501F00E0E981 /* InterpretationTypeTableDataSource.swift in Sources */, F11DF06620BD96D000F3E005 /* GalleryFilterGroupType.swift in Sources */, F11786F120AC5482007A9A1B /* ServiceFactory.swift in Sources */, 4B7B81C62044790700C2EFCF /* TimeZoneLocal.swift in Sources */, @@ -13280,6 +13347,7 @@ 859B862C204820DC003272B2 /* ThemePickerPresenter.swift in Sources */, 2603139B20A0A4BA009AC66D /* LanguageSelectorViewController.swift in Sources */, 2686D3201FC3E39C0079CB75 /* ContentNavigationVC.swift in Sources */, + B7EF8EDB210C759400E0E981 /* InterpretationTypeCellModel.swift in Sources */, 0062D93F2062EC4100B915AC /* InviteFriendsCellLayout.swift in Sources */, 001F0CF5202C38FA006B4304 /* TimeZoneCell.swift in Sources */, C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */, @@ -13829,6 +13897,7 @@ 26C0C1EE2073DE1600C530DA /* ForwardSelectorProtocols+ShareExt.swift in Sources */, 8511D3742034596E00B2A620 /* Collection+ViewLayout.swift in Sources */, F10B0E1720B4401500528E7A /* GalleryPresenter.swift in Sources */, + B7EF8ED9210C71E800E0E981 /* InterpretationType.swift in Sources */, 6D5157D21F30B822002A27DB /* MicrophoneView.swift in Sources */, 260313AF20A0A50D009AC66D /* TranslationService.swift in Sources */, A42D51AD206A361400EEB952 /* cur.swift in Sources */, @@ -14165,6 +14234,7 @@ FE58F9B3208F0583004AFDD3 /* DBMessageEditAction.swift in Sources */, 26FA420C2017AE3300E6F6EC /* StarMessageCellLayout.swift in Sources */, 2633EF6E205212F700DB3868 /* MemberDAOProtocol.swift in Sources */, + B7EF8EDD210CB0A200E0E981 /* InterpretationModel.swift in Sources */, E79117921F97A48900462D68 /* ProfileDetailsViewLayout.swift in Sources */, 8595E0DC204863DB00178171 /* CarouselPickerCellModel.swift in Sources */, 2648C41C2069B52100863614 /* ChangeNumberStep2Interactor.swift in Sources */, @@ -14381,7 +14451,6 @@ B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */, 26FA420E201812D600E6F6EC /* StarTableDS.swift in Sources */, F10B0E2820B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift in Sources */, - B74BAFFE21076AFA0049CD27 /* TheGradientView.swift in Sources */, E7C36C351FC448CB00740630 /* DBFeature.swift in Sources */, 4B06D3122028A4CF003B275B /* GroupChatsItemsFactory.swift in Sources */, 0062D93B2062EC4100B915AC /* PhoneContact.swift in Sources */, @@ -14576,6 +14645,7 @@ 8580BAC620BD983400239D9D /* MessageProtocols.swift in Sources */, 2683F77A203F38E30003181A /* UIPickerView.swift in Sources */, A4679BAB20B2DD100021FE9C /* SubscribersCollectionDataSource.swift in Sources */, + B7EF8ED7210C598800E0E981 /* InterpretationTypeCellLayout.swift in Sources */, 2648C40D2069B52100863614 /* ChangeNumberStep1Protocols.swift in Sources */, A43B25B920AB1E7600FF8107 /* String+Range.swift in Sources */, E7C9CEC51FCC245F0090C2E0 /* P2pExtension.swift in Sources */, @@ -14667,6 +14737,7 @@ 896D51F07E2F79C8B5502DBF /* EditGroupPhotoInteractor.swift in Sources */, A4ED79AA20C704F500A41F67 /* MyChannelsItemsFactory.swift in Sources */, 5BC1D38420D3B670002A44B3 /* CallCreatorMediator.swift in Sources */, + B7EF8ED2210C502D00E0E981 /* InterpretationTypeTableDelegate.swift in Sources */, 1325429A6216D23E2E67B6B7 /* EditGroupPhotoWireframe.swift in Sources */, 2603139720A0A4B9009AC66D /* LangCell.swift in Sources */, 99B9D27D2F0EFE051E6581ED /* CreateGroupProtocols.swift in Sources */, @@ -14762,6 +14833,7 @@ 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */, F1607B2A20B2DE6500BDF60A /* CameraQRPreviewWireframe.swift in Sources */, E3BE59F069959DA2523EF3DC /* MapSearchWireframe.swift in Sources */, + B7EF8ED4210C511C00E0E981 /* InterpretationTypeCell.swift in Sources */, 8572C3BE2092368600E4840C /* StickerDataSource.swift in Sources */, F1313B0220888FE600E04092 /* ThirdPartyServices.swift in Sources */, 9BD8E40720F3576F001384EC /* CallInProgressWireframe.swift in Sources */, diff --git a/Nynja/CircleMenuControl/Core/CircleMenu.swift b/Nynja/CircleMenuControl/Core/CircleMenu.swift index 27cab0ffb..1befa132e 100644 --- a/Nynja/CircleMenuControl/Core/CircleMenu.swift +++ b/Nynja/CircleMenuControl/Core/CircleMenu.swift @@ -59,7 +59,6 @@ class CircleMenu: UIControl { return false } - //MARK: - Init required init(rect: CGRect, delegate: CircleMenuDelegate, menuSet: CircleMenuSet) { @@ -82,6 +81,7 @@ class CircleMenu: UIControl { for index in 0.. self.container.bounds.width/2 { - print("Ignoring tap \(touchPoint.x), \(touchPoint.y)") self.deSelectSegment(at: self.currentSectorIndex) return false } diff --git a/Nynja/Modules/Interpretation/InterpretationModel.swift b/Nynja/Modules/Interpretation/InterpretationModel.swift new file mode 100644 index 000000000..5a8da2eb2 --- /dev/null +++ b/Nynja/Modules/Interpretation/InterpretationModel.swift @@ -0,0 +1,17 @@ +// +// InterpretationModel.swift +// Nynja +// +// Created by Roman Chopovenko on 7/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class InterpretationModel { + var type: InterpretationType = .general + var fromLang: Language = Language.empty_default + var toLang: Language = Language.current + var time: Int = 30 + var price: CGFloat = 12 +} diff --git a/Nynja/Modules/Interpretation/InterpretationProtocols.swift b/Nynja/Modules/Interpretation/InterpretationProtocols.swift index 2baec5650..e31470201 100644 --- a/Nynja/Modules/Interpretation/InterpretationProtocols.swift +++ b/Nynja/Modules/Interpretation/InterpretationProtocols.swift @@ -15,6 +15,7 @@ protocol InterpretationWireFrameProtocol: class { /** * Add here your methods for communication PRESENTER -> WIREFRAME */ + func openInterpretationType(delegate: SelectInterpretationTypeDelegate) } protocol InterpretationViewProtocol: class { @@ -25,6 +26,8 @@ protocol InterpretationViewProtocol: class { * Add here your methods for communication PRESENTER -> VIEW */ + func setInterpretationType(_ type: InterpretationType) + } protocol InterpretationPresenterProtocol: BasePresenterProtocol { @@ -36,6 +39,7 @@ protocol InterpretationPresenterProtocol: BasePresenterProtocol { /** * Add here your methods for communication VIEW -> PRESENTER */ + func openInterpretationType() func showed() } @@ -54,3 +58,7 @@ protocol InterpretationInteractorInputProtocol: class { * Add here your methods for communication PRESENTER -> INTERACTOR */ } + +protocol SelectInterpretationTypeDelegate: class { + func interpretationTypeSelected(_ type: InterpretationType) +} diff --git a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift index ce1f0fd52..53706354e 100644 --- a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift +++ b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift @@ -17,6 +17,16 @@ final class InterpretationPresenter: BasePresenter, InterpretationPresenterProto func showed() { } + + func openInterpretationType() { + self.wireFrame.openInterpretationType(delegate: self) + } +} + +extension InterpretationPresenter: SelectInterpretationTypeDelegate { + func interpretationTypeSelected(_ type: InterpretationType) { + self.view.setInterpretationType(type) + } } extension InterpretationPresenter: SetInjectable { diff --git a/Nynja/Modules/Interpretation/View/InterpretationLayout.swift b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift index 5572b9758..b0f866fea 100644 --- a/Nynja/Modules/Interpretation/View/InterpretationLayout.swift +++ b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift @@ -31,7 +31,7 @@ extension InterpretationViewController { static let separatorHeight = 1 struct fromLangPickerView { - static let widthOffset = Constraints.swapButton.side/2 + static let widthOffset = Constraints.swapButton.side/2 + Constraints.sidePadding static let height = 158.adjustedByWidth } @@ -73,13 +73,13 @@ extension InterpretationViewController { // MARK: String enum Strings: String { - case interpretationTime = "Approximate interpretation time" - case langForInterpretation = "Language for interpretation" - case from = "From:" - case to = "To:" - case interpretationType = "Interpretation type" - case totalPrice = "Total price" - case search = "Search Interpreter" + case interpretationTime = "interpretation_time" + case langForInterpretation = "lang_for_interpretation" + case from = "from" + case to = "to" + case interpretationType = "interpretation_type" + case totalPrice = "total_price" + case search = "search_interpreter" } struct Colors { diff --git a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift index 73c21fb5b..400afe23f 100644 --- a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift +++ b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift @@ -17,13 +17,12 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { } } + private let model = InterpretationModel() + lazy var swipeBackHelper: SwipeBackHelper = { return SwipeBackHelper(with: self, gestureCompletion: nil) }() - private var langFrom: Language = Language.empty_default - private var langTo: Language = Language.current - private var fromLangDelegate: LanguagePickerDelegate! private var toLangDelegate: LanguagePickerDelegate! @@ -47,7 +46,6 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { lazy var interpretationTimeButton: UIButton = { let button = UIButton() - button.setTitle("30 min", for: .normal) button.titleLabel?.font = Text.defaultGray.font button.setTitleColor(Text.defaultGray.fontColor, for: .normal) button.addTarget(self, action: #selector(interpretationTimeButtonTapped), for: .touchUpInside) @@ -86,7 +84,8 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { self.view.addSubview(label) label.snp.makeConstraints({ (make) in - make.left.right.equalToSuperview().offset(Constraints.sidePadding) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().offset(-Constraints.sidePadding) make.height.equalTo(Text.defaultWhite.height) make.bottom.equalTo(fromLabel.snp.top).offset(-Constraints.langForInterpretationLabel.bottomPadding) }) @@ -123,7 +122,7 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { label.snp.makeConstraints({ (make) in make.height.equalTo(Text.defaultGray.height) - make.right.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().offset(-Constraints.sidePadding) make.bottom.equalTo(toLangPickerView.snp.top) }) return label @@ -243,7 +242,6 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { }() private lazy var searchButton: NynjaButton = { - let button = NynjaButton(height: CGFloat(Constraints.searchButton.height)) button.titleLabel?.font = Text.defaultWhite.font button.setTitle(Strings.search.localized.uppercased(), for: .normal) @@ -296,7 +294,6 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { lazy var totalPriceValueLabel: UILabel = { let label = UILabel() - label.text = "12.0 NYN" label.font = Text.defaultWhite.font label.textColor = Constants.colors.red.getColor() @@ -327,14 +324,17 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { self.interpretationTimeLabel.isHidden = false self.searchButton.isHidden = false self.setupPickers() + self.interpretationTimeButton.setTitle("\(self.model.time) min", for: .normal) + self.interpretationTypeButton.setTitle(self.model.type.localized, for: .normal) + self.totalPriceValueLabel.text = "\(self.model.price) NYN" } private func setupPickers() { self.setupLanguagePicker(picker: self.fromLangPickerView, with: fromLangDelegate, callback: self) self.setupLanguagePicker(picker: self.toLangPickerView, with: self.toLangDelegate, callback: self) - self.selectRow(with: self.langFrom, in: self.fromLangPickerView, animated: false) - self.selectRow(with: self.langTo, in: self.toLangPickerView, animated: false) + self.selectRow(with: self.model.fromLang, in: self.fromLangPickerView, animated: false) + self.selectRow(with: self.model.toLang, in: self.toLangPickerView, animated: false) } private func setupLanguagePicker(picker: UIPickerView, with delegate: LanguagePickerDelegate, callback: LanguagePickerDelegateCallback) { @@ -354,38 +354,42 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { // MARK: - Actions @objc private func swapButtonTapped() { - guard self.langFrom != .empty_default, self.langFrom != self.langTo else { return } - let temp = langFrom + guard self.model.fromLang != .empty_default, self.model.fromLang != self.model.toLang else { return } + let temp = self.model.fromLang - self.langFrom = langTo - self.langTo = temp + self.model.fromLang = self.model.toLang + self.model.toLang = temp - self.selectRow(with: self.langFrom, in: self.fromLangPickerView, animated: true) - self.selectRow(with: self.langTo, in: toLangPickerView, animated: true) + self.selectRow(with: self.model.fromLang, in: self.fromLangPickerView, animated: true) + self.selectRow(with: self.model.toLang, in: toLangPickerView, animated: true) } @objc private func interpretationTimeButtonTapped() { - print("set interpretation TIME pressed") + } @objc private func interpretationTypeButtonTapped() { - print("set interpretation TYPE pressed") + self.presenter.openInterpretationType() } @objc private func searchButtonTapped() { print("search interpreter pressed") } + + func setInterpretationType(_ type: InterpretationType) { + self.interpretationTypeButton.setTitle(type.localized, for: .normal) + } } extension InterpretationViewController: LanguagePickerDelegateCallback { func didSelectLanguage(_ pickerView: UIPickerView, language: Language) { if pickerView == self.fromLangPickerView { - self.langFrom = language + self.model.fromLang = language } else if pickerView == self.toLangPickerView { - self.langTo = language + self.model.toLang = language } } } diff --git a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift index 1db8f4187..f6f652755 100644 --- a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift +++ b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift @@ -9,7 +9,6 @@ import UIKit final class InterpretationWireFrame: InterpretationWireFrameProtocol { - weak var navigation: UINavigationController? func presentInterpretation(navigation: UINavigationController) { @@ -20,6 +19,7 @@ final class InterpretationWireFrame: InterpretationWireFrameProtocol { let interactor = InterpretationInteractor() // Connecting + let viewDependencies = InterpretationViewController.Dependencies(presenter: presenter, fromLangDelegate: LanguagePickerDelegate(defaultEmptyLanguage: true), toLangDelegate: LanguagePickerDelegate(defaultEmptyLanguage: false)) view.inject(dependencies: viewDependencies) let presenterDependencies = InterpretationPresenter.Dependencies(view: view, interactor: interactor, wireFrame: self) @@ -32,4 +32,8 @@ final class InterpretationWireFrame: InterpretationWireFrameProtocol { navigation.pushViewController(view as UIViewController, animated: true) } + func openInterpretationType(delegate: SelectInterpretationTypeDelegate) { + guard let navigation = self.navigation else { return } + InterpretationTypeWireFrame().presentInterpretationType(navigation: navigation, delegate: delegate) + } } diff --git a/Nynja/Modules/InterpretationType/Interactor/InterpretationTypeInteractor.swift b/Nynja/Modules/InterpretationType/Interactor/InterpretationTypeInteractor.swift new file mode 100644 index 000000000..e2c61b814 --- /dev/null +++ b/Nynja/Modules/InterpretationType/Interactor/InterpretationTypeInteractor.swift @@ -0,0 +1,23 @@ +// +// InterpretationTypeInteractor.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class InterpretationTypeInteractor: InterpretationTypeInteractorInputProtocol { + weak var presenter: InterpretationTypeInteractorOutputProtocol! +} + +extension InterpretationTypeInteractor: SetInjectable { + func inject(dependencies: InterpretationTypeInteractor.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: InterpretationTypeInteractorOutputProtocol + } +} diff --git a/Nynja/Modules/InterpretationType/InterpretationType.swift b/Nynja/Modules/InterpretationType/InterpretationType.swift new file mode 100644 index 000000000..da752e2ad --- /dev/null +++ b/Nynja/Modules/InterpretationType/InterpretationType.swift @@ -0,0 +1,42 @@ +// +// InterpretationType.swift +// Nynja +// +// Created by Roman Chopovenko on 7/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +enum InterpretationType: String { + case general = "interpretation_type_general" + case technology = "interpretation_type_technology" + case legal = "interpretation_type_legal" + case medical = "interpretation_type_medical" + + var description: String { + switch self { + case .general: + return "interpretation_type_description_general".localized + case .technology: + return "interpretation_type_description_technology".localized + case .legal: + return "interpretation_type_description_legal".localized + case .medical: + return "interpretation_type_description_medical".localized + } + } + + var price: CGFloat { + switch self { + case .general: + return 0.40 + case .technology: + return 0.50 + case .legal: + return 0.63 + case .medical: + return 0.75 + } + } +} diff --git a/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift b/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift index 6580b8dcd..906d826ab 100644 --- a/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift +++ b/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift @@ -10,33 +10,33 @@ import UIKit protocol InterpretationTypeWireFrameProtocol: class { - func presentInterpretationType(navigation: UINavigationController) + func presentInterpretationType(navigation: UINavigationController, delegate: SelectInterpretationTypeDelegate) /** * Add here your methods for communication PRESENTER -> WIREFRAME */ + func dismiss() } protocol InterpretationTypeViewProtocol: class { - + var presenter: InterpretationTypePresenterProtocol! { get set } - /** * Add here your methods for communication PRESENTER -> VIEW */ - } -protocol InterpretationTypePresenterProtocol: class, BasePresenterProtocol { +protocol InterpretationTypePresenterProtocol: BasePresenterProtocol { var view: InterpretationTypeViewProtocol! { get set } var interactor: InterpretationTypeInteractorInputProtocol! { get set } var wireFrame: InterpretationTypeWireFrameProtocol! { get set } - + var delegate: SelectInterpretationTypeDelegate? { get set } /** * Add here your methods for communication VIEW -> PRESENTER */ + func selectItem(type: InterpretationType) func showed() } diff --git a/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift b/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift new file mode 100644 index 000000000..1d6e7cc24 --- /dev/null +++ b/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift @@ -0,0 +1,41 @@ +// +// InterpretationTypePresenter.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class InterpretationTypePresenter: BasePresenter, InterpretationTypePresenterProtocol, InterpretationTypeInteractorOutputProtocol { + + + weak var view: InterpretationTypeViewProtocol! + var interactor: InterpretationTypeInteractorInputProtocol! + var wireFrame: InterpretationTypeWireFrameProtocol! + weak var delegate: SelectInterpretationTypeDelegate? + + func showed() { } + + func selectItem(type: InterpretationType) { + self.delegate?.interpretationTypeSelected(type) + self.wireFrame.dismiss() + } +} + +extension InterpretationTypePresenter: SetInjectable { + func inject(dependencies: InterpretationTypePresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireFrame = dependencies.wireFrame + self.delegate = dependencies.delegate + } + + struct Dependencies { + let view: InterpretationTypeViewProtocol + let interactor: InterpretationTypeInteractorInputProtocol + let wireFrame: InterpretationTypeWireFrameProtocol + let delegate: SelectInterpretationTypeDelegate + } +} diff --git a/Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift b/Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift new file mode 100644 index 000000000..9ad5b38de --- /dev/null +++ b/Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift @@ -0,0 +1,77 @@ +// +// InterpretationTypeViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class InterpretationTypeViewController: BaseVC, InterpretationTypeViewProtocol { + + var presenter: InterpretationTypePresenterProtocol! { + didSet { + _presenter = presenter + } + } + + lazy var swipeBackHelper: SwipeBackHelper = { + return SwipeBackHelper(with: self, gestureCompletion: nil) + }() + + private var tableDataSource: InterpretationTypeTableDataSource! + private var tableDelegate: InterpretationTypeTableDelegate! + + private lazy var tableView: UITableView = { + let table = UITableView() + table.backgroundColor = .clear + table.separatorStyle = .none + table.bounces = false + table.register(viewModel: InterpretationTypeCellModel.self) + + self.view.addSubview(table) + table.snp.makeConstraints({ (make) in + make.top.equalTo(navigationView.snp.bottom) + make.left.right.bottom.equalToSuperview() + }) + return table + }() + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + presenter.showed() + } + + // MARK: - UI Setup + + private func setupUI() { + swipeBackHelper.addGesture() + self.screenTitle = "interpretation_type".localized.uppercased() + self.tableView.dataSource = self.tableDataSource + self.tableView.delegate = self.tableDelegate + + tableDelegate.selectAction = { [unowned self] type in + self.presenter.selectItem(type: type) + } + } +} + +extension InterpretationTypeViewController: SetInjectable { + func inject(dependencies: InterpretationTypeViewController.Dependencies) { + self.presenter = dependencies.presenter + self.tableDataSource = dependencies.tableDataSource + self.tableDelegate = dependencies.tableDelegate + } + + struct Dependencies { + let presenter: InterpretationTypePresenterProtocol + let tableDataSource: InterpretationTypeTableDataSource + let tableDelegate: InterpretationTypeTableDelegate + } +} diff --git a/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift new file mode 100644 index 000000000..0ee69af2d --- /dev/null +++ b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift @@ -0,0 +1,121 @@ +// +// InterpretationTypeCell.swift +// Nynja +// +// Created by Roman Chopovenko on 7/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class InterpretationTypeCell: UITableViewCell { + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.isUserInteractionEnabled = true + label.font = Text.defaultWhite.font + label.textColor = Text.defaultWhite.color + label.setContentHuggingPriority(.defaultHigh, for: .vertical) + self.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.top.equalToSuperview().offset(Constarints.titleLabel.topPadding) + make.left.equalToSuperview().offset(Constarints.sidePadding) + make.right.equalToSuperview().offset(-Constarints.sidePadding) + make.height.equalTo(Constarints.titleLabel.height) + make.bottom.equalTo(self.descriptionLabel.snp.top) + + }) + return label + }() + + lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.isUserInteractionEnabled = true + label.numberOfLines = 0 + label.font = Text.defaultGray.font + label.textColor = Text.defaultGray.color + + self.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.left.equalToSuperview().offset(Constarints.sidePadding) + make.right.equalToSuperview().offset(-Constarints.sidePadding) + make.bottom.equalTo(self.priceTitleLabel.snp.top).offset(-Constarints.descriptionLabel.bottomPadding) + make.bottom.equalTo(self.priceValueLabel.snp.top).offset(-Constarints.descriptionLabel.bottomPadding) + }) + return label + }() + + lazy var priceTitleLabel: UILabel = { + let label = UILabel() + label.isUserInteractionEnabled = true + label.text = "price_per_minute".localized + label.font = Text.defaultWhite.font + label.textColor = Text.defaultWhite.color + + self.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.height.equalTo(Constarints.priceTitleLabel.height) + make.left.equalToSuperview().offset(Constarints.sidePadding) + make.right.equalTo(self.priceValueLabel.snp.left).offset(-Constarints.priceTitleLabel.rightPadding) + make.bottom.equalTo(self.separator.snp.top).offset(-Constarints.priceTitleLabel.bottomPadding) + }) + return label + }() + + lazy var priceValueLabel: UILabel = { + let label = UILabel() + label.isUserInteractionEnabled = true + label.font = Text.defaultRed.font + label.textColor = Text.defaultRed.color + + self.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.height.equalTo(Constarints.priceTitleLabel.height) + make.width.equalTo(Constarints.priceValueLabel.width).priority(200) + make.right.equalToSuperview().offset(-Constarints.sidePadding) + }) + return label + }() + + lazy var separator: UIView = { + let separator = UIView() + + separator.backgroundColor = Constants.colors.separatorGrayColor.getColor() + + self.addSubview(separator) + separator.snp.makeConstraints({ (make) in + make.height.equalTo(Constarints.separatorHeight) + make.left.equalToSuperview().offset(Constarints.sidePadding) + make.right.equalToSuperview().offset(-Constarints.sidePadding) + make.bottom.equalToSuperview() + }) + return separator + }() + + // MARK: Init + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + baseSetup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + baseSetup() + } + + private func baseSetup() { + self.backgroundColor = UIColor.clear + self.selectionStyle = .none + } + + // MARK: ConfigurableCell + func setup(with type: InterpretationType) { + self.titleLabel.text = type.localized + self.descriptionLabel.text = type.description + self.priceValueLabel.text = "\(type.price) NYN" + } +} diff --git a/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift new file mode 100644 index 000000000..89c87015d --- /dev/null +++ b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift @@ -0,0 +1,65 @@ +// +// InterpretationTypeCellLayout.swift +// Nynja +// +// Created by Roman Chopovenko on 7/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension InterpretationTypeCell { + struct Constarints { + + static let sidePadding = 16.adjustedByWidth + static let separatorHeight = 1 + + struct titleLabel { + static let topPadding = 8.adjustedByWidth + static let height = Text.defaultWhite.height + static let bottomPadding = 16.adjustedByWidth + } + + struct descriptionLabel { + static let bottomPadding = 8.adjustedByWidth + } + + struct priceTitleLabel { + static let height = Text.defaultWhite.height + static let rightPadding = 8.adjustedByWidth + static let bottomPadding = 8.adjustedByWidth + } + + struct priceValueLabel { + static let height = Text.defaultRed.height + static let width = 50.adjustedByWidth + static let rightPadding = 8.adjustedByWidth + } + + static var sumHeight: CGFloat { + let height = titleLabel.topPadding + titleLabel.height + titleLabel.bottomPadding + descriptionLabel.bottomPadding + priceTitleLabel.height + priceTitleLabel.bottomPadding + separatorHeight + return CGFloat(height) + } + } + + // MARK: Text + struct Text { + struct defaultWhite { + static let height = 22.adjustedByWidth + static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(height)) + static let color = Constants.colors.white.getColor() + } + + struct defaultGray { + static let height = 20.adjustedByWidth + static let font = UIFont(fontName: Constants.fonts.regular, height: CGFloat(height)) + static let color = Constants.colors.gray.getColor() + } + + struct defaultRed { + static let height = 22.adjustedByWidth + static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(height)) + static let color = Constants.colors.red.getColor() + } + } +} diff --git a/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellModel.swift b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellModel.swift new file mode 100644 index 000000000..2a28e11d3 --- /dev/null +++ b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellModel.swift @@ -0,0 +1,25 @@ +// +// InterpretationTypeCellModel.swift +// Nynja +// +// Created by Roman Chopovenko on 7/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import NynjaUIKit + +final class InterpretationTypeCellModel: CellViewModel { + var accessibilityIdentifier: String { + return "InterpretationTypeCell" + } + + let type: InterpretationType + + init(type: InterpretationType) { + self.type = type + } + + func setup(cell: InterpretationTypeCell) { + cell.setup(with: type) + } +} diff --git a/Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDataSource.swift b/Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDataSource.swift new file mode 100644 index 000000000..3686b39b3 --- /dev/null +++ b/Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDataSource.swift @@ -0,0 +1,29 @@ +// +// InterpretationTypeTableDataSource.swift +// Nynja +// +// Created by Roman Chopovenko on 7/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class InterpretationTypeTableDataSource: NSObject, UITableViewDataSource { + + var rows: [InterpretationType] = [.general, .technology, .legal, .medical] + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return rows.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return tableView.dequeueReusableCell(withModel: self.typeModel(at: indexPath), for: indexPath) + + } + + private func typeModel(at indexPath: IndexPath) -> InterpretationTypeCellModel { + let type = self.rows[indexPath.row] + return InterpretationTypeCellModel(type: type) + } + +} diff --git a/Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDelegate.swift b/Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDelegate.swift new file mode 100644 index 000000000..d8ec049a6 --- /dev/null +++ b/Nynja/Modules/InterpretationType/View/TableView/InterpretationTypeTableDelegate.swift @@ -0,0 +1,38 @@ +// +// InterpretationTypeTableDelegate.swift +// Nynja +// +// Created by Roman Chopovenko on 7/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class InterpretationTypeTableDelegate: NSObject, UITableViewDelegate { + typealias SelectAction = (InterpretationType) -> Void + + var selectAction: SelectAction? + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let item = self.self.getCurrentType(tableView: tableView, indexPath: indexPath) else { return } + selectAction?(item) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return self.getRowHeight(tableView: tableView, indexPath: indexPath) + } + + func getRowHeight(tableView: UITableView, indexPath: IndexPath) -> CGFloat { + guard let item = self.getCurrentType(tableView: tableView, indexPath: indexPath) else { return 0 } + let width = tableView.frame.size.width - (2 * CGFloat(InterpretationTypeCell.Constarints.sidePadding)) + let heightDescription = item.description.height(withConstrainedWidth: width, font: InterpretationTypeCell.Text.defaultGray.font!) + + return InterpretationTypeCell.Constarints.sumHeight + heightDescription + } + + private func getCurrentType(tableView: UITableView, indexPath: IndexPath) -> InterpretationType? { + guard let ds = tableView.dataSource as? InterpretationTypeTableDataSource else { return nil } + let item = ds.rows[indexPath.row] + return item + } +} diff --git a/Nynja/Modules/InterpretationType/Wireframe/InterpretationTypeWireFrame.swift b/Nynja/Modules/InterpretationType/Wireframe/InterpretationTypeWireFrame.swift new file mode 100644 index 000000000..c546098a6 --- /dev/null +++ b/Nynja/Modules/InterpretationType/Wireframe/InterpretationTypeWireFrame.swift @@ -0,0 +1,43 @@ +// +// InterpretationTypeWireFrame.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class InterpretationTypeWireFrame: InterpretationTypeWireFrameProtocol { + + weak var navigation: UINavigationController? + + func presentInterpretationType(navigation: UINavigationController, delegate: SelectInterpretationTypeDelegate) { + self.navigation = navigation + + let view = InterpretationTypeViewController() + let presenter = InterpretationTypePresenter() + let interactor = InterpretationTypeInteractor() + + // Connecting + let viewDependencies = InterpretationTypeViewController.Dependencies(presenter: presenter, + tableDataSource: InterpretationTypeTableDataSource(), + tableDelegate:InterpretationTypeTableDelegate()) + view.inject(dependencies: viewDependencies) + let presenterDependencies = InterpretationTypePresenter.Dependencies(view: view, + interactor: interactor, + wireFrame: self, + delegate: delegate) + presenter.inject(dependencies: presenterDependencies) + let interactorDependencies = InterpretationTypeInteractor.Dependencies(presenter: presenter) + interactor.inject(dependencies: interactorDependencies) + + view.modalTransitionStyle = .crossDissolve + view.modalPresentationStyle = .overCurrentContext + navigation.pushViewController(view as UIViewController, animated: true) + } + + func dismiss() { + self.navigation?.popViewController(animated: true) + } +} diff --git a/Nynja/Modules/Marketplace/MarketplaceViewController.swift b/Nynja/Modules/Marketplace/MarketplaceViewController.swift index 01b0c6417..b895c45c4 100644 --- a/Nynja/Modules/Marketplace/MarketplaceViewController.swift +++ b/Nynja/Modules/Marketplace/MarketplaceViewController.swift @@ -71,7 +71,6 @@ extension MarketplaceViewController: SetInjectable { } extension MarketplaceViewController: CircleMenuDelegate { - func didSelectItem(_ menu: CircleMenu, type: CircleMenuItemType) { guard let menuSet = CircleMenuFactory().allElementsDictionary[type] else { self.presenter.openMarketplaceMenu(type: type) diff --git a/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift b/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift index 933ac921f..b8df785b1 100644 --- a/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift +++ b/Nynja/Modules/Marketplace/MarketplaceWireFrame.swift @@ -40,7 +40,7 @@ final class MarketplaceWireFrame: MarketplaceWireFrameProtocol { case .interpretation: InterpretationWireFrame().presentInterpretation(navigation: navigation) default: - print("No subMenu found! Define action here") + break } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 47db344df..a155d390d 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -846,3 +846,22 @@ //Submenu titles** "interpretation" = "Interpretation"; + +"interpretation_type_general" = "General"; +"interpretation_type_technology" = "Technology"; +"interpretation_type_legal" = "Legal"; +"interpretation_type_medical" = "Medical"; + +"interpretation_type_description_general" = "NYNJA users who speak multiple languages will help you right now to interprate your call in real time. Use this for general language that most people would know."; +"interpretation_type_description_technology" = "Choose this for interpretation that require technology specific terminology and we will find someone with this capability."; +"interpretation_type_description_legal" = "Perfect for legal topics and other interpretation work typically done by a lawyer or paralegal."; +"interpretation_type_description_medical" = "Specialists with a background in medical interpretation will only be assigned these interpretations."; + +"interpretation_time" = "Approximate interpretation time"; +"lang_for_interpretation" = "Language for interpretation"; +"from" = "From:"; +"to" = "To:"; +"interpretation_type" = "Interpretation type"; +"total_price" = "Total price"; +"search_interpreter" = "Search Interpreter"; +"price_per_minute" = "Price per minute"; diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings index 3502a1511..52bbf157f 100644 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ b/Nynja/Resources/ru.lproj/Localizable.strings @@ -764,3 +764,22 @@ //Submenu titles** "interpretation" = "Interpretation"; + +"interpretation_type_general" = "General"; +"interpretation_type_technology" = "Technology"; +"interpretation_type_legal" = "Legal"; +"interpretation_type_medical" = "Medical"; + +"interpretation_type_description_general" = "NYNJA users who speak multiple languages will help you right now to interprate your call in real time. Use this for general language that most people would know."; +"interpretation_type_description_technology" = "Choose this for interpretation that require technology specific terminology and we will find someone with this capability."; +"interpretation_type_description_legal" = "Perfect for legal topics and other interpretation work typically done by a lawyer or paralegal."; +"interpretation_type_description_medical" = "Specialists with a background in medical interpretation will only be assigned these interpretations."; + +"interpretation_time" = "Approximate interpretation time"; +"lang_for_interpretation" = "Language for interpretation"; +"from" = "From:"; +"to" = "To:"; +"interpretation_type" = "Interpretation type"; +"total_price" = "Total price"; +"search_interpreter" = "Search Interpreter"; +"price_per_minute" = "Price per minute"; -- GitLab From 78499d7df17fd9d954478eee7e84dcba2314753f Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Fri, 3 Aug 2018 19:07:15 +0300 Subject: [PATCH 06/32] Assigning Interpreter Screen --- Nynja.xcodeproj/project.pbxproj | 61 ++++++++++- Nynja/CircleMenuControl/Core/CircleMenu.swift | 14 +-- .../AssigningInterpreterProtocols.swift | 2 - .../AssigningInterpreterInteractor.swift | 24 +++++ .../AssigningInterpreterPresenter.swift | 29 +++++ .../View/AsigningInterpreterLayout.swift | 33 ++++++ .../AssigningInterpreterViewController.swift | 102 ++++++++++++++++++ .../CircleLoadingView/CircleLoadingView.swift | 89 +++++++++++++++ .../View/CircleLoadingView/CircleView.swift | 44 ++++++++ .../AssigningInterpreterWireFrame.swift | 35 ++++++ .../InterpretationProtocols.swift | 5 +- .../Presenter/InterpretationPresenter.swift | 10 +- .../View/InterpretationViewController.swift | 3 +- .../View/LanguagePickerDelegate.swift | 1 - .../Wireframe/InterpretationWireFrame.swift | 5 + .../InterpretationTypePresenter.swift | 2 +- Nynja/Resources/en.lproj/Localizable.strings | 4 + Nynja/Resources/ru.lproj/Localizable.strings | 4 + 18 files changed, 445 insertions(+), 22 deletions(-) create mode 100644 Nynja/Modules/AssigningInterpreter/Interactor/AssigningInterpreterInteractor.swift create mode 100644 Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift create mode 100644 Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift create mode 100644 Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift create mode 100644 Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift create mode 100644 Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleView.swift create mode 100644 Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index f54414344..f57a19aa1 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1579,6 +1579,7 @@ B723C632204D9E5100884FFD /* DataAndStorageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C631204D9E5100884FFD /* DataAndStorageOption.swift */; }; B723C634204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C633204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift */; }; B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B723C635204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift */; }; + B742053F211448BD004FDE16 /* AsigningInterpreterLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B742053E211448BD004FDE16 /* AsigningInterpreterLayout.swift */; }; B745F2E42109B9E500488A91 /* LanguagePickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */; }; B745F2E62109BB0100488A91 /* InterpretationLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B745F2E52109BB0100488A91 /* InterpretationLayout.swift */; }; B74BAFFB21076AFA0049CD27 /* CircleMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */; }; @@ -1614,6 +1615,8 @@ B79FA03821091ED000F286BF /* InterpretationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03321091ED000F286BF /* InterpretationProtocols.swift */; }; B79FA03921091ED000F286BF /* InterpretationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03421091ED000F286BF /* InterpretationInteractor.swift */; }; B79FA03A21091ED000F286BF /* InterpretationWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */; }; + B7B546AE210D9C8C002DCA55 /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B546AD210D9C8C002DCA55 /* CircleView.swift */; }; + B7B546B0210DC68E002DCA55 /* CircleLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B546AF210DC68E002DCA55 /* CircleLoadingView.swift */; }; B7EF8ED0210C501F00E0E981 /* InterpretationTypeTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ECF210C501F00E0E981 /* InterpretationTypeTableDataSource.swift */; }; B7EF8ED2210C502D00E0E981 /* InterpretationTypeTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED1210C502D00E0E981 /* InterpretationTypeTableDelegate.swift */; }; B7EF8ED4210C511C00E0E981 /* InterpretationTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED3210C511C00E0E981 /* InterpretationTypeCell.swift */; }; @@ -3437,6 +3440,7 @@ B723C631204D9E5100884FFD /* DataAndStorageOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageOption.swift; sourceTree = ""; }; B723C633204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataAndStorageTableDataSource.swift; sourceTree = ""; }; B723C635204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataAndStorageTableDelegate.swift; sourceTree = ""; }; + B742053E211448BD004FDE16 /* AsigningInterpreterLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsigningInterpreterLayout.swift; sourceTree = ""; }; B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePickerDelegate.swift; sourceTree = ""; }; B745F2E52109BB0100488A91 /* InterpretationLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationLayout.swift; sourceTree = ""; }; B74BAFEF21076AFA0049CD27 /* CircleMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMenu.swift; sourceTree = ""; }; @@ -3473,6 +3477,8 @@ B79FA03321091ED000F286BF /* InterpretationProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationProtocols.swift; sourceTree = ""; }; B79FA03421091ED000F286BF /* InterpretationInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationInteractor.swift; sourceTree = ""; }; B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationWireFrame.swift; sourceTree = ""; }; + B7B546AD210D9C8C002DCA55 /* CircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleView.swift; sourceTree = ""; }; + B7B546AF210DC68E002DCA55 /* CircleLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleLoadingView.swift; sourceTree = ""; }; B7EF8ECF210C501F00E0E981 /* InterpretationTypeTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeTableDataSource.swift; sourceTree = ""; }; B7EF8ED1210C502D00E0E981 /* InterpretationTypeTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeTableDelegate.swift; sourceTree = ""; }; B7EF8ED3210C511C00E0E981 /* InterpretationTypeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeCell.swift; sourceTree = ""; }; @@ -10102,11 +10108,11 @@ B77C11D5210923F200CCB42E /* AssigningInterpreter */ = { isa = PBXGroup; children = ( - B77C11D62109242200CCB42E /* AssigningInterpreterPresenter.swift */, - B77C11D72109242200CCB42E /* AssigningInterpreterViewController.swift */, B77C11D82109242200CCB42E /* AssigningInterpreterProtocols.swift */, - B77C11D92109242200CCB42E /* AssigningInterpreterInteractor.swift */, - B77C11DA2109242200CCB42E /* AssigningInterpreterWireFrame.swift */, + B7EF8EE0210CDCCE00E0E981 /* View */, + B7EF8EE1210CDCDF00E0E981 /* Presenter */, + B7EF8EE2210CDCF200E0E981 /* Interactor */, + B7EF8EE3210CDCFF00E0E981 /* Wireframe */, ); path = AssigningInterpreter; sourceTree = ""; @@ -10165,6 +10171,15 @@ path = TableHeaderFooter; sourceTree = ""; }; + B7B546B1210DCE24002DCA55 /* CircleLoadingView */ = { + isa = PBXGroup; + children = ( + B7B546AD210D9C8C002DCA55 /* CircleView.swift */, + B7B546AF210DC68E002DCA55 /* CircleLoadingView.swift */, + ); + path = CircleLoadingView; + sourceTree = ""; + }; B7EF8ECA210C4E1400E0E981 /* View */ = { isa = PBXGroup; children = ( @@ -10218,6 +10233,40 @@ path = Cell; sourceTree = ""; }; + B7EF8EE0210CDCCE00E0E981 /* View */ = { + isa = PBXGroup; + children = ( + B77C11D72109242200CCB42E /* AssigningInterpreterViewController.swift */, + B742053E211448BD004FDE16 /* AsigningInterpreterLayout.swift */, + B7B546B1210DCE24002DCA55 /* CircleLoadingView */, + ); + path = View; + sourceTree = ""; + }; + B7EF8EE1210CDCDF00E0E981 /* Presenter */ = { + isa = PBXGroup; + children = ( + B77C11D62109242200CCB42E /* AssigningInterpreterPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + B7EF8EE2210CDCF200E0E981 /* Interactor */ = { + isa = PBXGroup; + children = ( + B77C11D92109242200CCB42E /* AssigningInterpreterInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + B7EF8EE3210CDCFF00E0E981 /* Wireframe */ = { + isa = PBXGroup; + children = ( + B77C11DA2109242200CCB42E /* AssigningInterpreterWireFrame.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; B8DCBB4ACE8A650987F2D234 /* Presenter */ = { isa = PBXGroup; children = ( @@ -13731,6 +13780,7 @@ E7C1D3681F683A7D007D4E1E /* MainNavigationItem.swift in Sources */, E7DD28311F8B6CB200174650 /* LoginViewLayout.swift in Sources */, E749C5671FD4490E0048DEAC /* DefaultMessageProcessingManager.swift in Sources */, + B7B546B0210DC68E002DCA55 /* CircleLoadingView.swift in Sources */, 85482848204EA56600DCBEC8 /* PrivacyListDataSource.swift in Sources */, 2600DAD6203479D000A2D4F7 /* ReturnToHomeHeaderView.swift in Sources */, F117FBD520FF9DAF00BA1F82 /* MediaInfoView.swift in Sources */, @@ -14254,6 +14304,7 @@ 3AE0A84C1F20321A008A04F3 /* WheelItemModel.swift in Sources */, A42D51BB206A361400EEB952 /* userTask.swift in Sources */, 2686D3271FC640440079CB75 /* DBSyncFile.swift in Sources */, + B742053F211448BD004FDE16 /* AsigningInterpreterLayout.swift in Sources */, A40F18B920BFD81B0091B09E /* EmptyStateView.swift in Sources */, 06E67B3C3ED6CE5F6A913762 /* MainProtocols.swift in Sources */, 85082DDF2045A8C2000AE4B2 /* WheelPosition.swift in Sources */, @@ -14451,6 +14502,7 @@ B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */, 26FA420E201812D600E6F6EC /* StarTableDS.swift in Sources */, F10B0E2820B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift in Sources */, + B74BAFFE21076AFA0049CD27 /* TheGradientView.swift in Sources */, E7C36C351FC448CB00740630 /* DBFeature.swift in Sources */, 4B06D3122028A4CF003B275B /* GroupChatsItemsFactory.swift in Sources */, 0062D93B2062EC4100B915AC /* PhoneContact.swift in Sources */, @@ -14761,6 +14813,7 @@ 8562853220D140FC000C9739 /* InputBar+ButtonType.swift in Sources */, A43B25AC20AB1DFA00FF8107 /* BaseInputView.swift in Sources */, F127F3BA20BF03BF007A6F87 /* DateFormatterExtension.swift in Sources */, + B7B546AE210D9C8C002DCA55 /* CircleView.swift in Sources */, E79061B81FBF2243009FD83A /* FeatureTable.swift in Sources */, 5B5EE777EF301CFC1FDCF307 /* CreateGroupInteractor.swift in Sources */, 8580BAD820BD98E700239D9D /* CounterView.swift in Sources */, diff --git a/Nynja/CircleMenuControl/Core/CircleMenu.swift b/Nynja/CircleMenuControl/Core/CircleMenu.swift index 1befa132e..9b9c22bbc 100644 --- a/Nynja/CircleMenuControl/Core/CircleMenu.swift +++ b/Nynja/CircleMenuControl/Core/CircleMenu.swift @@ -33,7 +33,7 @@ class CircleMenu: UIControl { private var dataSource: CircleMenuSet? { return self.menuNavigationStack.last } - + private var containerCenter: CGPoint { return CGPoint(x: self.container.bounds.width/2, y: self.container.bounds.height/2) } @@ -58,7 +58,7 @@ class CircleMenu: UIControl { } return false } - + //MARK: - Init required init(rect: CGRect, delegate: CircleMenuDelegate, menuSet: CircleMenuSet) { @@ -71,9 +71,9 @@ class CircleMenu: UIControl { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + //MARK: - Override - + override func draw(_ rect: CGRect) { guard let items = self.dataSource?.items else { return } self.container = UIView(frame: rect) @@ -184,7 +184,7 @@ class CircleMenu: UIControl { } //MARK: Private methods - + private func selectSegment(at index: Int) { if let section = self.sectors[index].section { self.currentSectorIndex = index @@ -198,7 +198,7 @@ class CircleMenu: UIControl { section.isHighlighted = false } } - + private func detectSelectedSegment(from touchPoint: CGPoint) -> Sector? { // Get inverted touch angle value let dx = container.center.x - touchPoint.x @@ -228,7 +228,7 @@ class CircleMenu: UIControl { return sqrt(pow(dx, 2) + pow(dy, 2)) } - + private func ignoreTaps(for touchPoint: CGPoint) -> Bool { let dist: CGFloat = calculateDistance(fromCenter: touchPoint) diff --git a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift index 8286fe159..f2b0b8584 100644 --- a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift +++ b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift @@ -36,8 +36,6 @@ protocol AssigningInterpreterPresenterProtocol: class, BasePresenterProtocol { /** * Add here your methods for communication VIEW -> PRESENTER */ - - func showed() } protocol AssigningInterpreterInteractorOutputProtocol: class { diff --git a/Nynja/Modules/AssigningInterpreter/Interactor/AssigningInterpreterInteractor.swift b/Nynja/Modules/AssigningInterpreter/Interactor/AssigningInterpreterInteractor.swift new file mode 100644 index 000000000..2f7d23247 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/Interactor/AssigningInterpreterInteractor.swift @@ -0,0 +1,24 @@ +// +// AssigningInterpreterInteractor.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AssigningInterpreterInteractor: AssigningInterpreterInteractorInputProtocol { + + weak var presenter: AssigningInterpreterInteractorOutputProtocol! +} + +extension AssigningInterpreterInteractor: SetInjectable { + func inject(dependencies: AssigningInterpreterInteractor.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: AssigningInterpreterInteractorOutputProtocol + } +} diff --git a/Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift b/Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift new file mode 100644 index 000000000..37bc3722a --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift @@ -0,0 +1,29 @@ +// +// AssigningInterpreterPresenter.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AssigningInterpreterPresenter: BasePresenter, AssigningInterpreterPresenterProtocol, AssigningInterpreterInteractorOutputProtocol { + weak var view: AssigningInterpreterViewProtocol! + var interactor: AssigningInterpreterInteractorInputProtocol! + var wireFrame: AssigningInterpreterWireFrameProtocol! +} + +extension AssigningInterpreterPresenter: SetInjectable { + func inject(dependencies: AssigningInterpreterPresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireFrame = dependencies.wireFrame + } + + struct Dependencies { + let view: AssigningInterpreterViewProtocol + let interactor: AssigningInterpreterInteractorInputProtocol + let wireFrame: AssigningInterpreterWireFrameProtocol + } +} diff --git a/Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift b/Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift new file mode 100644 index 000000000..195b7817a --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift @@ -0,0 +1,33 @@ +// +// AsigningInterpreterLayout.swift +// Nynja +// +// Created by Roman Chopovenko on 8/3/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +extension AssigningInterpreterViewController { + + struct Constraints { + static let sidePadding = 32.adjustedByWidth + struct loadingView { + static let bottomPadding = 24.adjustedByWidth + } + } + + struct Texts { + struct bottomLabel { + static let height = CGFloat(33.adjustedByWidth) + static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let color = Constants.colors.red.getColor() + } + + struct descriptionLabel { + static let height = CGFloat(22.adjustedByWidth) + static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let color = Constants.colors.darkGray.getColor() + } + } +} diff --git a/Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift b/Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift new file mode 100644 index 000000000..86e6596d6 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift @@ -0,0 +1,102 @@ +// +// AssigningInterpreterViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class AssigningInterpreterViewController: BaseVC, AssigningInterpreterViewProtocol { + + var presenter: AssigningInterpreterPresenterProtocol! { + didSet { + _presenter = presenter + } + } + + lazy var swipeBackHelper: SwipeBackHelper = { + return SwipeBackHelper(with: self, gestureCompletion: nil) + }() + + lazy var loadingView: CircleLoadingView = { + + let lineWidth: CGFloat = 8 + + let view = CircleLoadingView(frame: .zero, color: Constants.colors.red.getColor(), lineWidth: lineWidth) + view.backgroundColor = .clear + + self.view.addSubview(view) + + view.snp.makeConstraints({ (make) in + make.width.height.equalTo(self.view.snp.width).dividedBy(3) + make.centerX.equalToSuperview() + make.bottom.equalTo(self.view.snp.centerY) + make.bottom.equalTo(self.descriptionLabel.snp.top).offset(-Constraints.loadingView.bottomPadding) + }) + + return view + }() + + lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.textAlignment = .center + label.text = "asigning_interpreter_description".localized + label.font = Texts.descriptionLabel.font + label.textColor = Texts.descriptionLabel.color + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().offset(-Constraints.sidePadding) + }) + return label + }() + + lazy var bottomLabel: UILabel = { + let label = UILabel() + label.text = "coming_soon".localized.uppercased() + label.font = Texts.bottomLabel.font + label.textColor = Texts.bottomLabel.color + + self.view.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.bottom.equalToSuperview().offset(-Constraints.sidePadding) + make.centerX.equalToSuperview() + }) + return label + }() + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + + // MARK: - UI Setup + + private func setupUI() { + swipeBackHelper.addGesture() + self.screenTitle = "assigning_interpreter".localized.uppercased() + self.loadingView.isHidden = false + self.bottomLabel.isHidden = false + } +} + +extension AssigningInterpreterViewController: SetInjectable { + func inject(dependencies: AssigningInterpreterViewController.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: AssigningInterpreterPresenterProtocol + } +} diff --git a/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift b/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift new file mode 100644 index 000000000..ecdf11786 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift @@ -0,0 +1,89 @@ +// +// CircleLoadingView.swift +// Nynja +// +// Created by Roman Chopovenko on 7/29/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class CircleLoadingView: UIView { + + private var color: UIColor = .red + private var lineWidth: CGFloat = 0 + + private var leftView: CircleView! + private var centerView: CircleView! + private var rightView: CircleView! + + //MARK: - Init + + required init(frame: CGRect, color: UIColor, lineWidth: CGFloat) { + super.init(frame: frame) + self.color = color + self.lineWidth = lineWidth + self.backgroundColor = .clear + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: - Override + + override func layoutSubviews() { + super.layoutSubviews() + + for subview in self.subviews { + subview.removeFromSuperview() + } + + let rect = self.bounds + let outer = CircleView(frame: rect, color: self.color, lineWidth: self.lineWidth) + self.addSubview(outer) + + let innerSide: CGFloat = 1.5 * self.lineWidth + let offset: CGFloat = 1.5 * innerSide + + let dx = rect.width/2 - innerSide/2 + let dy = rect.height/2 - innerSide/2 + + let centerRect = CGRect(x: dx, y: dy, width: innerSide, height: innerSide) + let center = CircleView(frame: centerRect, color: color) + self.centerView = center + self.addSubview(center) + + let leftRect = CGRect(x: dx - offset, y: dy, width: innerSide, height: innerSide) + let left = CircleView(frame: leftRect, color: color) + self.leftView = left + self.addSubview(left) + + let rightRect = CGRect(x: dx + offset, y: dy, width: innerSide, height: innerSide) + let right = CircleView(frame: rightRect, color: color) + self.rightView = right + self.addSubview(right) + + self.setupAnimations() + } + + //MARK: - Private methods + + private func setupAnimations() { + let duration = 0.6 + let delay = duration / 3 + + self.animateView(view: self.leftView, duration: duration, delay: 0) + + self.animateView(view: self.centerView, duration: duration, delay: delay) + + self.animateView(view: self.rightView, duration: duration, delay: delay * 2) + } + + private func animateView(view: UIView, duration: Double, delay: Double){ + UIView.animate(withDuration: duration, delay: delay, options: [.curveEaseInOut, .repeat, .autoreverse], animations: { + view.transform = CGAffineTransform(scaleX: 0.2, y: 0.2) + view.alpha = 0.3 + }) + } +} diff --git a/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleView.swift b/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleView.swift new file mode 100644 index 000000000..03ecd78ad --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleView.swift @@ -0,0 +1,44 @@ +// +// CircleView.swift +// Nynja +// +// Created by Roman Chopovenko on 7/29/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class CircleView: UIView { + + private var color: UIColor = .red + private var lineWidth: CGFloat? + + required init(frame: CGRect, color: UIColor, lineWidth: CGFloat? = nil) { + super.init(frame: frame) + self.color = color + self.lineWidth = lineWidth + self.backgroundColor = .clear + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + let center = CGPoint(x: rect.width/2,y: rect.height/2) + + let circlePath = UIBezierPath(arcCenter: center, radius: rect.width/2 - (self.lineWidth ?? 0), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true) + + let shapeLayer = CAShapeLayer() + shapeLayer.path = circlePath.cgPath + + if let lineWidth = lineWidth { + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.strokeColor = color.cgColor + shapeLayer.lineWidth = lineWidth + } else { + shapeLayer.fillColor = color.cgColor + } + layer.addSublayer(shapeLayer) + } +} diff --git a/Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift b/Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift new file mode 100644 index 000000000..72530dae8 --- /dev/null +++ b/Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift @@ -0,0 +1,35 @@ +// +// AssigningInterpreterWireFrame.swift +// Nynja +// +// Created by Roman Chopovenko on 7/26/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class AssigningInterpreterWireFrame: AssigningInterpreterWireFrameProtocol { + + weak var navigation: UINavigationController? + + func presentAssigningInterpreter(navigation: UINavigationController) { + self.navigation = navigation + + let view = AssigningInterpreterViewController() + let presenter = AssigningInterpreterPresenter() + let interactor = AssigningInterpreterInteractor() + + // Connecting + let viewDependencies = AssigningInterpreterViewController.Dependencies.init(presenter: presenter) + view.inject(dependencies: viewDependencies) + let presenterDependencies = AssigningInterpreterPresenter.Dependencies.init(view: view, interactor: interactor, wireFrame: self) + presenter.inject(dependencies: presenterDependencies) + let interactorDependencies = AssigningInterpreterInteractor.Dependencies.init(presenter: presenter) + interactor.inject(dependencies: interactorDependencies) + + view.modalTransitionStyle = .crossDissolve + view.modalPresentationStyle = .overCurrentContext + navigation.pushViewController(view as UIViewController, animated: true) + } + +} diff --git a/Nynja/Modules/Interpretation/InterpretationProtocols.swift b/Nynja/Modules/Interpretation/InterpretationProtocols.swift index e31470201..f87f165db 100644 --- a/Nynja/Modules/Interpretation/InterpretationProtocols.swift +++ b/Nynja/Modules/Interpretation/InterpretationProtocols.swift @@ -16,6 +16,7 @@ protocol InterpretationWireFrameProtocol: class { * Add here your methods for communication PRESENTER -> WIREFRAME */ func openInterpretationType(delegate: SelectInterpretationTypeDelegate) + func openAssigningInterpreter() } protocol InterpretationViewProtocol: class { @@ -27,7 +28,6 @@ protocol InterpretationViewProtocol: class { */ func setInterpretationType(_ type: InterpretationType) - } protocol InterpretationPresenterProtocol: BasePresenterProtocol { @@ -39,6 +39,7 @@ protocol InterpretationPresenterProtocol: BasePresenterProtocol { /** * Add here your methods for communication VIEW -> PRESENTER */ + func openAssigningInterpreter() func openInterpretationType() func showed() } @@ -60,5 +61,5 @@ protocol InterpretationInteractorInputProtocol: class { } protocol SelectInterpretationTypeDelegate: class { - func interpretationTypeSelected(_ type: InterpretationType) + func typeSelected(_ type: InterpretationType) } diff --git a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift index 53706354e..3bd890a5a 100644 --- a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift +++ b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift @@ -14,17 +14,19 @@ final class InterpretationPresenter: BasePresenter, InterpretationPresenterProto var interactor: InterpretationInteractorInputProtocol! var wireFrame: InterpretationWireFrameProtocol! - func showed() { - - } + func showed() { } func openInterpretationType() { self.wireFrame.openInterpretationType(delegate: self) } + + func openAssigningInterpreter() { + self.wireFrame.openAssigningInterpreter() + } } extension InterpretationPresenter: SelectInterpretationTypeDelegate { - func interpretationTypeSelected(_ type: InterpretationType) { + func typeSelected(_ type: InterpretationType) { self.view.setInterpretationType(type) } } diff --git a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift index 400afe23f..dcdf93412 100644 --- a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift +++ b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift @@ -63,6 +63,7 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { lazy var separatorTop: UIView = { let separator = UIView() + separator.backgroundColor = Constants.colors.separatorGrayColor.getColor() self.view.addSubview(separator) @@ -376,7 +377,7 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { @objc private func searchButtonTapped() { - print("search interpreter pressed") + self.presenter.openAssigningInterpreter() } func setInterpretationType(_ type: InterpretationType) { diff --git a/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift b/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift index 7eacf60bb..5fd82bb47 100644 --- a/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift +++ b/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift @@ -13,7 +13,6 @@ protocol LanguagePickerDelegateCallback : class { } class LanguagePickerDelegate : NSObject, UIPickerViewDataSource, UIPickerViewDelegate { - private let rows = 100000 var pickerViewMiddle: Int { return ((self.rows / self.data.count) / 2) * self.data.count} diff --git a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift index f6f652755..6c417f805 100644 --- a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift +++ b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift @@ -36,4 +36,9 @@ final class InterpretationWireFrame: InterpretationWireFrameProtocol { guard let navigation = self.navigation else { return } InterpretationTypeWireFrame().presentInterpretationType(navigation: navigation, delegate: delegate) } + + func openAssigningInterpreter() { + guard let navigation = self.navigation else { return } + AssigningInterpreterWireFrame().presentAssigningInterpreter(navigation: navigation) + } } diff --git a/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift b/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift index 1d6e7cc24..f0ceb4b52 100644 --- a/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift +++ b/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift @@ -19,7 +19,7 @@ final class InterpretationTypePresenter: BasePresenter, InterpretationTypePresen func showed() { } func selectItem(type: InterpretationType) { - self.delegate?.interpretationTypeSelected(type) + self.delegate?.typeSelected(type) self.wireFrame.dismiss() } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index a155d390d..22c30b9c5 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -865,3 +865,7 @@ "total_price" = "Total price"; "search_interpreter" = "Search Interpreter"; "price_per_minute" = "Price per minute"; + +"asigning_interpreter_description" = "NYNJA is finding the perfect interpreter for you. Go ahead and continue using the app and we will let you know when they’re ready to start the call."; +"coming_soon" = "Coming soon"; +"assigning_interpreter" = "Assigning Interpreter"; diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings index 52bbf157f..06d65900c 100644 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ b/Nynja/Resources/ru.lproj/Localizable.strings @@ -783,3 +783,7 @@ "total_price" = "Total price"; "search_interpreter" = "Search Interpreter"; "price_per_minute" = "Price per minute"; + +"asigning_interpreter_description" = "NYNJA is finding the perfect interpreter for you. Go ahead and continue using the app and we will let you know when they’re ready to start the call."; +"coming_soon" = "Coming soon"; +"assigning_interpreter" = "Assigning Interpreter"; -- GitLab From 288d53ada9b3b9f5ed4712597af05da29ef28ca2 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Mon, 6 Aug 2018 14:18:02 +0300 Subject: [PATCH 07/32] Interpretation time input screen --- Nynja.xcodeproj/project.pbxproj | 17 +- .../Interfaces/NavigationProtocol.swift | 2 +- Nynja/Library/UI/AlertManager.swift | 9 + .../View/NavigationView/NavigationView.swift | 2 +- .../AssigningInterpreterProtocols.swift | 4 +- .../AssigningInterpreterPresenter.swift | 4 + .../AssigningInterpreterViewController.swift | 17 +- .../AssigningInterpreterWireFrame.swift | 4 + .../InterpretationProtocols.swift | 2 + .../Presenter/InterpretationPresenter.swift | 4 + .../View/InterpretationLayout.swift | 10 +- .../View/InterpretationViewController.swift | 38 +++- .../AlertTextFieldViewController.swift | 189 ++++++++++++++++++ .../AlertTextFieldViewControllerLayout.swift | 56 ++++++ .../Wireframe/InterpretationWireFrame.swift | 4 + .../InterpretationTypeProtocols.swift | 3 +- .../InterpretationTypePresenter.swift | 6 +- .../InterpretationTypeViewController.swift | 18 +- .../MarketplaceViewController.swift | 1 - Nynja/Resources/en.lproj/Localizable.strings | 2 + Nynja/Resources/ru.lproj/Localizable.strings | 2 + 21 files changed, 362 insertions(+), 32 deletions(-) create mode 100644 Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewController.swift create mode 100644 Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewControllerLayout.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index f57a19aa1..b4e168473 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -610,6 +610,7 @@ 553819525871F7D28AB90364 /* GroupRulesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705B62097A99515B3C778F35 /* GroupRulesPresenter.swift */; }; 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BA66D21FFC1A74CFD2F63C4 /* ProfileWireframe.swift */; }; 5894F4C605B66B55F21D406E /* DateTimePickerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA2BE900351F21464CE687 /* DateTimePickerInteractor.swift */; }; + 5A48445F21178E33000657ED /* AlertTextFieldViewControllerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A48445E21178E33000657ED /* AlertTextFieldViewControllerLayout.swift */; }; 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B772E08B9E40EB48DD87082 /* EditUsernameViewController.swift */; }; 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBACEAABEE65D7EC5572C4E /* CreateGroupPresenter.swift */; }; 5B5EE777EF301CFC1FDCF307 /* CreateGroupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC76E2B3DD0BCA0A622A5CD /* CreateGroupInteractor.swift */; }; @@ -1617,6 +1618,7 @@ B79FA03A21091ED000F286BF /* InterpretationWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */; }; B7B546AE210D9C8C002DCA55 /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B546AD210D9C8C002DCA55 /* CircleView.swift */; }; B7B546B0210DC68E002DCA55 /* CircleLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B546AF210DC68E002DCA55 /* CircleLoadingView.swift */; }; + B7B546B3210DD1EC002DCA55 /* AlertTextFieldViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B546B2210DD1EC002DCA55 /* AlertTextFieldViewController.swift */; }; B7EF8ED0210C501F00E0E981 /* InterpretationTypeTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ECF210C501F00E0E981 /* InterpretationTypeTableDataSource.swift */; }; B7EF8ED2210C502D00E0E981 /* InterpretationTypeTableDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED1210C502D00E0E981 /* InterpretationTypeTableDelegate.swift */; }; B7EF8ED4210C511C00E0E981 /* InterpretationTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED3210C511C00E0E981 /* InterpretationTypeCell.swift */; }; @@ -2627,6 +2629,7 @@ 55EC130CCF07D992BC6DD435 /* MapSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapSearchPresenter.swift; sourceTree = ""; }; 5957BF589EEC24E6799EB4CF /* TimeZoneSelectorViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimeZoneSelectorViewController.swift; sourceTree = ""; }; 59C99DD8A060B0BE6802110F /* AddContactPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactPresenter.swift; sourceTree = ""; }; + 5A48445E21178E33000657ED /* AlertTextFieldViewControllerLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertTextFieldViewControllerLayout.swift; sourceTree = ""; }; 5AEEB3D82E9CF02760DA4CE7 /* Pods-Nynja-Share.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.channels.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.channels.xcconfig"; sourceTree = ""; }; 5B377AA90A6B6BA0120C31F1 /* EditProfileProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditProfileProtocols.swift; sourceTree = ""; }; 5BC1D37220D3B3D8002A44B3 /* NynjaCommunicatorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NynjaCommunicatorService.swift; path = Services/NynjaCommunicatorService.swift; sourceTree = ""; }; @@ -3479,6 +3482,7 @@ B79FA03521091ED000F286BF /* InterpretationWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationWireFrame.swift; sourceTree = ""; }; B7B546AD210D9C8C002DCA55 /* CircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleView.swift; sourceTree = ""; }; B7B546AF210DC68E002DCA55 /* CircleLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleLoadingView.swift; sourceTree = ""; }; + B7B546B2210DD1EC002DCA55 /* AlertTextFieldViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertTextFieldViewController.swift; sourceTree = ""; }; B7EF8ECF210C501F00E0E981 /* InterpretationTypeTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeTableDataSource.swift; sourceTree = ""; }; B7EF8ED1210C502D00E0E981 /* InterpretationTypeTableDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeTableDelegate.swift; sourceTree = ""; }; B7EF8ED3210C511C00E0E981 /* InterpretationTypeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeCell.swift; sourceTree = ""; }; @@ -6330,6 +6334,15 @@ path = WireFrame; sourceTree = ""; }; + 5A48446021178E6B000657ED /* TimeFieldViewController */ = { + isa = PBXGroup; + children = ( + B7B546B2210DD1EC002DCA55 /* AlertTextFieldViewController.swift */, + 5A48445E21178E33000657ED /* AlertTextFieldViewControllerLayout.swift */, + ); + path = TimeFieldViewController; + sourceTree = ""; + }; 5B2B64C658BEC3CCC90B0DEF /* WireFrame */ = { isa = PBXGroup; children = ( @@ -10030,6 +10043,7 @@ B79FA03221091ED000F286BF /* InterpretationViewController.swift */, B745F2E52109BB0100488A91 /* InterpretationLayout.swift */, B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */, + 5A48446021178E6B000657ED /* TimeFieldViewController */, ); path = View; sourceTree = ""; @@ -14220,6 +14234,7 @@ E72906E72011156B007C5C5B /* UITableViewExtensions.swift in Sources */, 2603139320A0A4B9009AC66D /* LanguageSelectorProtocols.swift in Sources */, E7EE893B1F83CEF5009D37F9 /* MainViewControllerLayout.swift in Sources */, + 5A48445F21178E33000657ED /* AlertTextFieldViewControllerLayout.swift in Sources */, 32868DDD1F31CB5D0028B260 /* ChatsListPresenter.swift in Sources */, A42D519D206A361400EEB952 /* muc.swift in Sources */, A407348C20B712E9005762D5 /* UIView+Hierarchy.swift in Sources */, @@ -14338,6 +14353,7 @@ A408A0BA20C174040029F54B /* ChannelsListPresenter.swift in Sources */, 850FC60F203310D200832D87 /* SelectionAvatarView.swift in Sources */, 8E6C4BE41FF6A7AD009C8374 /* GroupStorageListItems.swift in Sources */, + B7B546B3210DD1EC002DCA55 /* AlertTextFieldViewController.swift in Sources */, F117871220ACF018007A9A1B /* CameraSettingsProtocols.swift in Sources */, 8503B526205046A6006F0593 /* NotificationSettingsViewController.swift in Sources */, 8580BAC820BD983400239D9D /* MentionCounterInteractive.swift in Sources */, @@ -14502,7 +14518,6 @@ B77C11DD2109242200CCB42E /* AssigningInterpreterProtocols.swift in Sources */, 26FA420E201812D600E6F6EC /* StarTableDS.swift in Sources */, F10B0E2820B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift in Sources */, - B74BAFFE21076AFA0049CD27 /* TheGradientView.swift in Sources */, E7C36C351FC448CB00740630 /* DBFeature.swift in Sources */, 4B06D3122028A4CF003B275B /* GroupChatsItemsFactory.swift in Sources */, 0062D93B2062EC4100B915AC /* PhoneContact.swift in Sources */, diff --git a/Nynja/Library/Interfaces/NavigationProtocol.swift b/Nynja/Library/Interfaces/NavigationProtocol.swift index 46486c573..81a48066a 100644 --- a/Nynja/Library/Interfaces/NavigationProtocol.swift +++ b/Nynja/Library/Interfaces/NavigationProtocol.swift @@ -9,6 +9,6 @@ import Foundation -protocol NavigationProtocol { +protocol NavigationProtocol: class { func back() } diff --git a/Nynja/Library/UI/AlertManager.swift b/Nynja/Library/UI/AlertManager.swift index 0de42f44c..52b3dab16 100644 --- a/Nynja/Library/UI/AlertManager.swift +++ b/Nynja/Library/UI/AlertManager.swift @@ -223,3 +223,12 @@ extension AlertManager { } } } + +//MARK: - TimeFieldViewController +extension AlertManager { + func showTimeFieldAlert(with initialValue: Int, completion: TimeCallback?) { + let alertControler = AlertTextFieldViewController(initialValue: initialValue) + alertControler.completion = completion + presentingController?.present(alertControler, animated: true, completion: nil) + } +} diff --git a/Nynja/Library/UI/View/NavigationView/NavigationView.swift b/Nynja/Library/UI/View/NavigationView/NavigationView.swift index 8789e7a19..e2cc6a7c0 100644 --- a/Nynja/Library/UI/View/NavigationView/NavigationView.swift +++ b/Nynja/Library/UI/View/NavigationView/NavigationView.swift @@ -44,7 +44,7 @@ class NavigationView: BaseView, Configurable { } private var title: String? - private var navigationHandler: NavigationProtocol? + private weak var navigationHandler: NavigationProtocol? private var backButtonImage: UIImage? = UIImage.backButtonImage { didSet { backButton?.setImage(backButtonImage, for: .normal) } } diff --git a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift index f2b0b8584..74cadbfa0 100644 --- a/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift +++ b/Nynja/Modules/AssigningInterpreter/AssigningInterpreterProtocols.swift @@ -15,6 +15,7 @@ protocol AssigningInterpreterWireFrameProtocol: class { /** * Add here your methods for communication PRESENTER -> WIREFRAME */ + func dismiss() } protocol AssigningInterpreterViewProtocol: class { @@ -27,7 +28,7 @@ protocol AssigningInterpreterViewProtocol: class { } -protocol AssigningInterpreterPresenterProtocol: class, BasePresenterProtocol { +protocol AssigningInterpreterPresenterProtocol: BasePresenterProtocol { var view: AssigningInterpreterViewProtocol! { get set } var interactor: AssigningInterpreterInteractorInputProtocol! { get set } @@ -36,6 +37,7 @@ protocol AssigningInterpreterPresenterProtocol: class, BasePresenterProtocol { /** * Add here your methods for communication VIEW -> PRESENTER */ + func dismiss() } protocol AssigningInterpreterInteractorOutputProtocol: class { diff --git a/Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift b/Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift index 37bc3722a..2d589e246 100644 --- a/Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift +++ b/Nynja/Modules/AssigningInterpreter/Presenter/AssigningInterpreterPresenter.swift @@ -12,6 +12,10 @@ final class AssigningInterpreterPresenter: BasePresenter, AssigningInterpreterPr weak var view: AssigningInterpreterViewProtocol! var interactor: AssigningInterpreterInteractorInputProtocol! var wireFrame: AssigningInterpreterWireFrameProtocol! + + func dismiss() { + self.wireFrame.dismiss() + } } extension AssigningInterpreterPresenter: SetInjectable { diff --git a/Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift b/Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift index 86e6596d6..3ab8ed964 100644 --- a/Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift +++ b/Nynja/Modules/AssigningInterpreter/View/AssigningInterpreterViewController.swift @@ -17,10 +17,6 @@ final class AssigningInterpreterViewController: BaseVC, AssigningInterpreterView } } - lazy var swipeBackHelper: SwipeBackHelper = { - return SwipeBackHelper(with: self, gestureCompletion: nil) - }() - lazy var loadingView: CircleLoadingView = { let lineWidth: CGFloat = 8 @@ -84,13 +80,24 @@ final class AssigningInterpreterViewController: BaseVC, AssigningInterpreterView // MARK: - UI Setup private func setupUI() { - swipeBackHelper.addGesture() + self.navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: navigationView.isSeparatorVisible ?? false, + isVisibleBackButton: true, + title: title, + navigationHandler: self, + backButtonImage: UIImage(named:"ic_back_navigation"))) self.screenTitle = "assigning_interpreter".localized.uppercased() self.loadingView.isHidden = false self.bottomLabel.isHidden = false } } +extension AssigningInterpreterViewController: NavigationProtocol { + func back() { + self.presenter.dismiss() + } +} + extension AssigningInterpreterViewController: SetInjectable { func inject(dependencies: AssigningInterpreterViewController.Dependencies) { self.presenter = dependencies.presenter diff --git a/Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift b/Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift index 72530dae8..7b8262859 100644 --- a/Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift +++ b/Nynja/Modules/AssigningInterpreter/Wireframe/AssigningInterpreterWireFrame.swift @@ -32,4 +32,8 @@ final class AssigningInterpreterWireFrame: AssigningInterpreterWireFrameProtocol navigation.pushViewController(view as UIViewController, animated: true) } + func dismiss() { + self.navigation?.popViewController(animated: true) + } + } diff --git a/Nynja/Modules/Interpretation/InterpretationProtocols.swift b/Nynja/Modules/Interpretation/InterpretationProtocols.swift index f87f165db..a351a5836 100644 --- a/Nynja/Modules/Interpretation/InterpretationProtocols.swift +++ b/Nynja/Modules/Interpretation/InterpretationProtocols.swift @@ -17,6 +17,7 @@ protocol InterpretationWireFrameProtocol: class { */ func openInterpretationType(delegate: SelectInterpretationTypeDelegate) func openAssigningInterpreter() + func dismiss() } protocol InterpretationViewProtocol: class { @@ -41,6 +42,7 @@ protocol InterpretationPresenterProtocol: BasePresenterProtocol { */ func openAssigningInterpreter() func openInterpretationType() + func dismiss() func showed() } diff --git a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift index 3bd890a5a..e1e0b5cea 100644 --- a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift +++ b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift @@ -23,6 +23,10 @@ final class InterpretationPresenter: BasePresenter, InterpretationPresenterProto func openAssigningInterpreter() { self.wireFrame.openAssigningInterpreter() } + + func dismiss() { + self.wireFrame.dismiss() + } } extension InterpretationPresenter: SelectInterpretationTypeDelegate { diff --git a/Nynja/Modules/Interpretation/View/InterpretationLayout.swift b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift index b0f866fea..7aa7cdbbf 100644 --- a/Nynja/Modules/Interpretation/View/InterpretationLayout.swift +++ b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift @@ -57,7 +57,15 @@ extension InterpretationViewController { } struct interpretationTypeButton { - static let width = 30 + static let width = 30.adjustedByWidth + } + + struct interpretationTimeButton { + static let width = 30.adjustedByWidth + } + + struct totalPriceValueLabel { + static let width = 30.adjustedByWidth } struct searchButton { diff --git a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift index dcdf93412..a4b699f81 100644 --- a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift +++ b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift @@ -19,10 +19,6 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { private let model = InterpretationModel() - lazy var swipeBackHelper: SwipeBackHelper = { - return SwipeBackHelper(with: self, gestureCompletion: nil) - }() - private var fromLangDelegate: LanguagePickerDelegate! private var toLangDelegate: LanguagePickerDelegate! @@ -53,7 +49,7 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { self.view.addSubview(button) button.snp.makeConstraints({ (make) in - make.width.equalTo(30).priority(200) + make.width.equalTo(Constraints.interpretationTimeButton.width).priority(200) make.top.equalTo(navigationView.snp.bottomMargin).offset(Constraints.sidePadding) make.height.equalTo(Text.defaultGray.height) make.right.equalToSuperview().inset(Constraints.sidePadding) @@ -63,7 +59,7 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { lazy var separatorTop: UIView = { let separator = UIView() - + separator.backgroundColor = Constants.colors.separatorGrayColor.getColor() self.view.addSubview(separator) @@ -214,9 +210,10 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { lazy var interpretationTypeButton: UIButton = { let button = UIButton() - button.setTitle("General", for: .normal) + button.setTitle(self.model.type.localized, for: .normal) button.titleLabel?.font = Text.defaultGray.font button.setTitleColor(Text.defaultGray.fontColor, for: .normal) + button.addTarget(self, action: #selector(interpretationTypeButtonTapped), for: .touchUpInside) self.view.addSubview(button) @@ -276,7 +273,6 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { return separator }() - lazy var totalPriceTitleLabel: UILabel = { let label = UILabel() label.text = Strings.totalPrice.localized @@ -302,7 +298,7 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { label.snp.makeConstraints({ (make) in make.height.equalTo(Text.defaultWhite.height) - make.width.equalTo(30).priority(200) + make.width.equalTo(Constraints.totalPriceValueLabel.width).priority(200) make.right.equalToSuperview().offset(-Constraints.sidePadding) }) return label @@ -317,10 +313,16 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { presenter.showed() } + // MARK: - UI Setup private func setupUI() { - swipeBackHelper.addGesture() + self.navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: navigationView.isSeparatorVisible ?? false, + isVisibleBackButton: true, + title: title, + navigationHandler: self, + backButtonImage: UIImage(named:"ic_back_navigation"))) self.screenTitle = "interpretation".localized.uppercased() self.interpretationTimeLabel.isHidden = false self.searchButton.isHidden = false @@ -367,7 +369,9 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { @objc private func interpretationTimeButtonTapped() { - + AlertManager().showTimeFieldAlert(with: self.model.time) { [unowned self] (time) in + self.setInterpretationTime(time) + } } @objc @@ -381,8 +385,20 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { } func setInterpretationType(_ type: InterpretationType) { + self.model.type = type self.interpretationTypeButton.setTitle(type.localized, for: .normal) } + + private func setInterpretationTime(_ time: Int) { + self.model.time = time + self.interpretationTimeButton.setTitle("\(time) min", for: .normal) + } +} + +extension InterpretationViewController: NavigationProtocol { + func back() { + self.presenter.dismiss() + } } extension InterpretationViewController: LanguagePickerDelegateCallback { diff --git a/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewController.swift b/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewController.swift new file mode 100644 index 000000000..c8ac74d93 --- /dev/null +++ b/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewController.swift @@ -0,0 +1,189 @@ +// +// AlertTextFieldViewController.swift +// Nynja +// +// Created by Roman Chopovenko on 7/29/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +typealias TimeCallback = (Int) -> () + +class AlertTextFieldViewController: BaseVC { + + var completion: TimeCallback? + private var initTimeValue: Int! + + // MARK: - Views + + lazy var backView: UIView = { + let view = UIView() + self.view.addSubview(view) + + view.snp.makeConstraints({ (make) in + make.top.left.right.equalToSuperview() + make.bottom.equalTo(self.view.keyboardLayoutGuide.snp.top) + }) + return view + }() + + lazy var containerView: UIView = { + let view = UIView() + view.setContentHuggingPriority(.defaultHigh, for: .vertical) + view.backgroundColor = Constants.colors.darkLight.getColor() + self.backView.addSubview(view) + + view.snp.makeConstraints({ (make) in + make.centerX.centerY.equalToSuperview() + }) + return view + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = "enter_interpretation_time".localized + label.font = Texts.dafaultWhite.font + label.textColor = Texts.dafaultWhite.color + + self.containerView.addSubview(label) + + label.snp.makeConstraints({ (make) in + make.top.equalToSuperview().offset(Constraints.titleLabel.top) + make.height.equalTo(Constraints.titleLabel.height) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().inset(Constraints.sidePadding) + make.bottom.equalTo(self.inputField.snp.top) + }) + return label + }() + + lazy var inputField: TextField = { + let field = TextField() + field.keyboardType = .numberPad + field.shouldResetAfterBackground = true + field.prohibitedOptions = .all + field.textAlignment = .left + field.tintColor = Constants.colors.red.getColor() + field.keyboardType = .numberPad + field.autocorrectionType = .no + + field.font = Texts.dafaultWhite.font + field.textColor = Texts.dafaultWhite.color + field.delegate = self + + self.containerView.addSubview(field) + field.snp.makeConstraints { (make) in + make.height.equalTo(Constraints.inputField.height) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().offset(-Constraints.sidePadding) + make.bottom.equalTo(separator.snp.top) + } + return field + }() + + lazy var separator: UIView = { + let separator = UIView() + + separator.backgroundColor = Constants.colors.red.getColor() + + self.containerView.addSubview(separator) + separator.snp.makeConstraints({ (make) in + make.height.equalTo(Constraints.separator.height) + make.left.equalToSuperview().offset(Constraints.sidePadding) + make.right.equalToSuperview().inset(Constraints.sidePadding) + make.bottom.equalTo(okButton.snp.top).offset(-Constraints.separator.bottom) + }) + return separator + }() + + lazy var okButton: UIButton = { + let button = UIButton() + self.containerView.addSubview(button) + let title = "ok".localized.uppercased() + button.setTitle(title, for: .normal) + button.titleLabel?.font = Texts.buttons.font + button.setTitleColor(Texts.buttons.color, for: .normal) + button.addTarget(self, action: #selector(okButtonTapped(_:)), for: .touchUpInside) + + button.snp.makeConstraints({ (make) in + make.width.equalTo(Constraints.okButton.width) + make.height.equalTo(Constraints.okButton.height) + make.right.equalToSuperview().inset(Constraints.sidePadding) + make.left.equalTo(cancelButton.snp.right).offset(Constraints.sidePadding) + make.centerY.equalTo(cancelButton) + make.bottom.equalToSuperview().offset(-Constraints.okButton.bottom) + }) + return button + }() + + lazy var cancelButton: UIButton = { + let button = UIButton() + self.containerView.addSubview(button) + let title = "cancel".localized.capitalized + button.setTitle(title, for: .normal) + button.titleLabel?.font = Texts.buttons.font + button.setTitleColor(Texts.buttons.color, for: .normal) + button.addTarget(self, action: #selector(cancelButtonTapped(_:)), for: .touchUpInside) + + button.snp.makeConstraints({ (make) in + make.width.equalTo(Constraints.cancelButton.width).priority(200) + make.height.equalTo(Constraints.cancelButton.height) + }) + return button + }() + + + // MARK: - Init + + convenience init(initialValue: Int) { + self.init() + initTimeValue = initialValue + + self.modalTransitionStyle = .crossDissolve + self.modalPresentationStyle = .overCurrentContext + } + + override func viewDidLoad() { + super.viewDidLoad() + self.baseSetup() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.inputField.becomeFirstResponder() + } + + private func baseSetup() { + self.backImage.image = nil + self.view.backgroundColor = Constants.colors.black.getColor(withAlpha: 0.4) + self.view.isOpaque = false + self.containerView.isHidden = false + self.titleLabel.isHidden = false + self.inputField.text = String(self.initTimeValue) + } + + // MARK: - Actions + + @objc + func okButtonTapped(_ sender: UIButton) { + guard let text = self.inputField.text else { return } + if let num = Int(text) { + self.completion?(num) + } + self.dismiss(animated: true) + } + + @objc + func cancelButtonTapped(_ sender: UIButton) { + self.dismiss(animated: true) + } +} + +extension AlertTextFieldViewController: UITextFieldDelegate { + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let allowedCharacters = CharacterSet(charactersIn:"0123456789") + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) + } +} diff --git a/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewControllerLayout.swift b/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewControllerLayout.swift new file mode 100644 index 000000000..29871f6c7 --- /dev/null +++ b/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewControllerLayout.swift @@ -0,0 +1,56 @@ +// +// AlertTextFieldViewControllerLayout.swift +// Nynja +// +// Created by Roman Chopovenko on 8/5/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +extension AlertTextFieldViewController { + + struct Constraints { + + static let sidePadding = 24.adjustedByWidth + + struct titleLabel { + static let height = AlertTextFieldViewController.Texts.dafaultWhite.height + static let top = 16.adjustedByWidth + } + + struct inputField { + static let height = 30.adjustedByWidth + } + + struct separator { + static let height = 2 + static let bottom = 8.adjustedByWidth + } + + struct okButton { + static let height = 32.adjustedByWidth + static let width = 44.adjustedByWidth + static let bottom = 8.adjustedByWidth + } + + struct cancelButton { + static let height = 32.adjustedByWidth + static let width = 44.adjustedByWidth + } + } + + struct Texts { + struct dafaultWhite { + static let height = 22.adjustedByWidth + static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(height)) + static let color = Constants.colors.white.getColor() + } + + struct buttons { + private static let titleHeight = 22.adjustedByWidth + static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(titleHeight)) + static let color = Constants.colors.red.getColor() + } + } +} diff --git a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift index 6c417f805..1daa6e20b 100644 --- a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift +++ b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift @@ -41,4 +41,8 @@ final class InterpretationWireFrame: InterpretationWireFrameProtocol { guard let navigation = self.navigation else { return } AssigningInterpreterWireFrame().presentAssigningInterpreter(navigation: navigation) } + + func dismiss() { + self.navigation?.popViewController(animated: true) + } } diff --git a/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift b/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift index 906d826ab..2bb64d5c3 100644 --- a/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift +++ b/Nynja/Modules/InterpretationType/InterpretationTypeProtocols.swift @@ -19,7 +19,6 @@ protocol InterpretationTypeWireFrameProtocol: class { } protocol InterpretationTypeViewProtocol: class { - var presenter: InterpretationTypePresenterProtocol! { get set } /** * Add here your methods for communication PRESENTER -> VIEW @@ -37,7 +36,7 @@ protocol InterpretationTypePresenterProtocol: BasePresenterProtocol { */ func selectItem(type: InterpretationType) - func showed() + func dismiss() } protocol InterpretationTypeInteractorOutputProtocol: class { diff --git a/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift b/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift index f0ceb4b52..bfdaf80e5 100644 --- a/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift +++ b/Nynja/Modules/InterpretationType/Presenter/InterpretationTypePresenter.swift @@ -16,12 +16,14 @@ final class InterpretationTypePresenter: BasePresenter, InterpretationTypePresen var wireFrame: InterpretationTypeWireFrameProtocol! weak var delegate: SelectInterpretationTypeDelegate? - func showed() { } - func selectItem(type: InterpretationType) { self.delegate?.typeSelected(type) self.wireFrame.dismiss() } + + func dismiss() { + self.wireFrame.dismiss() + } } extension InterpretationTypePresenter: SetInjectable { diff --git a/Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift b/Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift index 9ad5b38de..03e291930 100644 --- a/Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift +++ b/Nynja/Modules/InterpretationType/View/InterpretationTypeViewController.swift @@ -17,10 +17,6 @@ final class InterpretationTypeViewController: BaseVC, InterpretationTypeViewProt } } - lazy var swipeBackHelper: SwipeBackHelper = { - return SwipeBackHelper(with: self, gestureCompletion: nil) - }() - private var tableDataSource: InterpretationTypeTableDataSource! private var tableDelegate: InterpretationTypeTableDelegate! @@ -45,13 +41,17 @@ final class InterpretationTypeViewController: BaseVC, InterpretationTypeViewProt override func viewDidLoad() { super.viewDidLoad() setupUI() - presenter.showed() } // MARK: - UI Setup private func setupUI() { - swipeBackHelper.addGesture() + self.navigationView.configure(config: NavigationView.Config( + isVisibleSeparator: navigationView.isSeparatorVisible ?? false, + isVisibleBackButton: true, + title: title, + navigationHandler: self, + backButtonImage: UIImage(named:"ic_back_navigation"))) self.screenTitle = "interpretation_type".localized.uppercased() self.tableView.dataSource = self.tableDataSource self.tableView.delegate = self.tableDelegate @@ -62,6 +62,12 @@ final class InterpretationTypeViewController: BaseVC, InterpretationTypeViewProt } } +extension InterpretationTypeViewController: NavigationProtocol { + func back() { + self.presenter.dismiss() + } +} + extension InterpretationTypeViewController: SetInjectable { func inject(dependencies: InterpretationTypeViewController.Dependencies) { self.presenter = dependencies.presenter diff --git a/Nynja/Modules/Marketplace/MarketplaceViewController.swift b/Nynja/Modules/Marketplace/MarketplaceViewController.swift index b895c45c4..c630d082a 100644 --- a/Nynja/Modules/Marketplace/MarketplaceViewController.swift +++ b/Nynja/Modules/Marketplace/MarketplaceViewController.swift @@ -57,7 +57,6 @@ final class MarketplaceViewController: BaseVC, MarketplaceViewProtocol { private func setupUI() { self.circleMenu.isHidden = false } - } extension MarketplaceViewController: SetInjectable { diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 22c30b9c5..977facc63 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -869,3 +869,5 @@ "asigning_interpreter_description" = "NYNJA is finding the perfect interpreter for you. Go ahead and continue using the app and we will let you know when they’re ready to start the call."; "coming_soon" = "Coming soon"; "assigning_interpreter" = "Assigning Interpreter"; + +"enter_interpretation_time" = "Enter interpretation time (min)"; diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings index 06d65900c..e022b8400 100644 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ b/Nynja/Resources/ru.lproj/Localizable.strings @@ -787,3 +787,5 @@ "asigning_interpreter_description" = "NYNJA is finding the perfect interpreter for you. Go ahead and continue using the app and we will let you know when they’re ready to start the call."; "coming_soon" = "Coming soon"; "assigning_interpreter" = "Assigning Interpreter"; + +"enter_interpretation_time" = "Enter interpretation time (min)"; -- GitLab From 84093aa88fe43653ec68851748c727e7804aeed2 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Thu, 9 Aug 2018 17:45:37 +0300 Subject: [PATCH 08/32] Marketplace bugfix NY: 2270, 2263, 2261, 2262, 2266, 2267, 2275, 2417, 2289 --- Nynja.xcodeproj/project.pbxproj | 20 +++- Nynja/CircleMenuControl/Core/CircleMenu.swift | 1 + .../Model/CircleMenuFactory.swift | 2 +- ...ynjaContextMenuItemsFactory+Messages.swift | 9 +- .../Material/MaterialTextField.swift | 4 + .../CircleLoadingView/CircleLoadingView.swift | 15 +++ .../Interpretation/InterpretationModel.swift | 6 +- .../InterpretationProtocols.swift | 3 +- .../Presenter/InterpretationPresenter.swift | 18 +++- .../AlertTextFieldViewController.swift | 101 +++++++++--------- .../AlertTextFieldViewControllerLayout.swift | 26 +++-- .../View/InterpretationViewController.swift | 13 ++- .../Wireframe/InterpretationWireFrame.swift | 5 +- .../InterpretationType.swift | 10 +- .../Cell/InterpretationTypeCell.swift | 2 +- Nynja/Resources/en.lproj/Localizable.strings | 6 +- Nynja/Resources/ru.lproj/Localizable.strings | 6 +- .../ServiceFactory/ServiceFactory.swift | 4 + Nynja/Utils/Money/CryptoMoney.swift | 2 + .../TextInputValidationService.swift | 11 ++ .../Validation/UseCaseValidationService.swift | 40 +++++++ 21 files changed, 216 insertions(+), 88 deletions(-) rename Nynja/Modules/Interpretation/View/{TimeFieldViewController => AlertTextFieldViewController}/AlertTextFieldViewController.swift (68%) rename Nynja/Modules/Interpretation/View/{TimeFieldViewController => AlertTextFieldViewController}/AlertTextFieldViewControllerLayout.swift (62%) rename Nynja/{ => Validation}/TextInputValidationService.swift (80%) create mode 100644 Nynja/Validation/UseCaseValidationService.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b4e168473..60d2e6b60 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1626,6 +1626,7 @@ B7EF8ED9210C71E800E0E981 /* InterpretationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8ED8210C71E800E0E981 /* InterpretationType.swift */; }; B7EF8EDB210C759400E0E981 /* InterpretationTypeCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8EDA210C759300E0E981 /* InterpretationTypeCellModel.swift */; }; B7EF8EDD210CB0A200E0E981 /* InterpretationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EF8EDC210CB0A200E0E981 /* InterpretationModel.swift */; }; + B7F4C2AC211995D500E48A98 /* UseCaseValidationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F4C2AB211995D500E48A98 /* UseCaseValidationService.swift */; }; B7F505192061158800C28FA1 /* SettingsArrowCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */; }; B7F5051B20611A0900C28FA1 /* DownloadSettingsArrowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */; }; B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */; }; @@ -3490,6 +3491,7 @@ B7EF8ED8210C71E800E0E981 /* InterpretationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationType.swift; sourceTree = ""; }; B7EF8EDA210C759300E0E981 /* InterpretationTypeCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationTypeCellModel.swift; sourceTree = ""; }; B7EF8EDC210CB0A200E0E981 /* InterpretationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpretationModel.swift; sourceTree = ""; }; + B7F4C2AB211995D500E48A98 /* UseCaseValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCaseValidationService.swift; sourceTree = ""; }; B7F505182061158800C28FA1 /* SettingsArrowCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsArrowCellViewModel.swift; sourceTree = ""; }; B7F5051A20611A0900C28FA1 /* DownloadSettingsArrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadSettingsArrowModel.swift; sourceTree = ""; }; B7F5051C2061252100C28FA1 /* DataAndStorageItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAndStorageItemsFactory.swift; sourceTree = ""; }; @@ -5550,7 +5552,7 @@ 8509AC61206A54420089089B /* ResponseResult.swift */, 26D35AB71FD0EFA800A5D513 /* AudioPlayer.swift */, 2631C511207A4C0C00F9AA55 /* AudioRecorder.swift */, - C921738120BADAFC00519A2D /* TextInputValidationService.swift */, + B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, ); name = Services; @@ -6334,13 +6336,13 @@ path = WireFrame; sourceTree = ""; }; - 5A48446021178E6B000657ED /* TimeFieldViewController */ = { + 5A48446021178E6B000657ED /* AlertTextFieldViewController */ = { isa = PBXGroup; children = ( B7B546B2210DD1EC002DCA55 /* AlertTextFieldViewController.swift */, 5A48445E21178E33000657ED /* AlertTextFieldViewControllerLayout.swift */, ); - path = TimeFieldViewController; + path = AlertTextFieldViewController; sourceTree = ""; }; 5B2B64C658BEC3CCC90B0DEF /* WireFrame */ = { @@ -10043,7 +10045,7 @@ B79FA03221091ED000F286BF /* InterpretationViewController.swift */, B745F2E52109BB0100488A91 /* InterpretationLayout.swift */, B745F2E32109B9E500488A91 /* LanguagePickerDelegate.swift */, - 5A48446021178E6B000657ED /* TimeFieldViewController */, + 5A48446021178E6B000657ED /* AlertTextFieldViewController */, ); path = View; sourceTree = ""; @@ -10281,6 +10283,15 @@ path = Wireframe; sourceTree = ""; }; + B7F4C2AA211995A500E48A98 /* Validation */ = { + isa = PBXGroup; + children = ( + C921738120BADAFC00519A2D /* TextInputValidationService.swift */, + B7F4C2AB211995D500E48A98 /* UseCaseValidationService.swift */, + ); + path = Validation; + sourceTree = ""; + }; B8DCBB4ACE8A650987F2D234 /* Presenter */ = { isa = PBXGroup; children = ( @@ -14081,6 +14092,7 @@ 26DCB25420692237001EF0AB /* Array+Feature.swift in Sources */, F1607B2E20B2DE8A00BDF60A /* CameraQRPreviewInteractor.swift in Sources */, B77C11EA2109254800CCB42E /* InterpretationTypeWireFrame.swift in Sources */, + B7F4C2AC211995D500E48A98 /* UseCaseValidationService.swift in Sources */, 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, diff --git a/Nynja/CircleMenuControl/Core/CircleMenu.swift b/Nynja/CircleMenuControl/Core/CircleMenu.swift index 9b9c22bbc..7dc1e98d7 100644 --- a/Nynja/CircleMenuControl/Core/CircleMenu.swift +++ b/Nynja/CircleMenuControl/Core/CircleMenu.swift @@ -75,6 +75,7 @@ class CircleMenu: UIControl { //MARK: - Override override func draw(_ rect: CGRect) { + guard self.container == nil else { return } guard let items = self.dataSource?.items else { return } self.container = UIView(frame: rect) self.container.backgroundColor = .clear diff --git a/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift b/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift index b2da07cb9..5f76e7c61 100644 --- a/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift +++ b/Nynja/CircleMenuControl/Model/CircleMenuFactory.swift @@ -54,7 +54,7 @@ class CircleMenuFactory { var virtualGoods: CircleMenuSet { let stickers = CircleMenuItem(type: .stickers, isEnabled: false) let mediaContent = CircleMenuItem(type: .mediaContent, isEnabled: false) - return CircleMenuSet(title: CircleMenuItemType.virtualGoods.localized.uppercased(), items: [stickers, mediaContent]) + return CircleMenuSet(title: CircleMenuItemType.virtualGoods.localized.uppercased(), items: [mediaContent, stickers]) } var accessMarketPlace: CircleMenuSet { diff --git a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift index 02f893488..f4339719a 100644 --- a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift +++ b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift @@ -180,7 +180,8 @@ extension NynjaContextMenuItemsFactory { } private static func audioCallMessageItems(for model: BaseChatCellModel) -> [ContextMenuRow] { - return [ ContextMenuRow(items: [marketPlace()])] + return [ ContextMenuRow(items: [ + starItem(for: model), delete(), marketplace()])] } private static func translatedMessageItems(for model: BaseChatCellModel) -> [ContextMenuRow] { @@ -256,8 +257,8 @@ extension NynjaContextMenuItemsFactory { layout: layout) } - static func marketPlace(with layout: ContextMenuItem.LayoutRepresentation = .default) -> ContextMenuItem { - return ContextMenuItem(content: .item(title: Strings.marketPlace.localized, icon: #imageLiteral(resourceName: "ic_marketplace_context_menu"), action: .marketPlace), + static func marketplace(with layout: ContextMenuItem.LayoutRepresentation = .double) -> ContextMenuItem { + return ContextMenuItem(content: .item(title: Strings.marketplace.localized, icon: #imageLiteral(resourceName: "ic_marketplace_context_menu"), action: .marketPlace), layout: layout) } @@ -362,7 +363,7 @@ private enum Strings: String { case share = "share" case saveToGallery = "save_to_gallery" case saveToDownloads = "save_to_downloads" - case marketPlace = "Market Place" + case marketplace = "marketplace" var localized: String { return rawValue.localized diff --git a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift index 21b13e4d4..22015ccb3 100644 --- a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift +++ b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift @@ -62,6 +62,10 @@ class MaterialTextField: MaterialTextContainer { textField.accessibilityIdentifier = "material_text_field" } + override func becomeFirstResponder() -> Bool { + return self.textField.becomeFirstResponder() + } + // MARK: - Actions diff --git a/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift b/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift index ecdf11786..43c328950 100644 --- a/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift +++ b/Nynja/Modules/AssigningInterpreter/View/CircleLoadingView/CircleLoadingView.swift @@ -24,12 +24,17 @@ class CircleLoadingView: UIView { self.color = color self.lineWidth = lineWidth self.backgroundColor = .clear + self.addNotifications() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + //MARK: - Override override func layoutSubviews() { @@ -86,4 +91,14 @@ class CircleLoadingView: UIView { view.alpha = 0.3 }) } + + private func addNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(self.applicationDidBecomeActive), name: Notification.Name.UIApplicationDidBecomeActive, object: nil) + } + + @objc + private func applicationDidBecomeActive() { + self.setNeedsLayout() + self.layoutIfNeeded() + } } diff --git a/Nynja/Modules/Interpretation/InterpretationModel.swift b/Nynja/Modules/Interpretation/InterpretationModel.swift index 5a8da2eb2..7554b732a 100644 --- a/Nynja/Modules/Interpretation/InterpretationModel.swift +++ b/Nynja/Modules/Interpretation/InterpretationModel.swift @@ -13,5 +13,9 @@ class InterpretationModel { var fromLang: Language = Language.empty_default var toLang: Language = Language.current var time: Int = 30 - var price: CGFloat = 12 + var price: NYNMoney { + let result = Double(time) * NSDecimalNumber(decimal: type.price.amount).doubleValue + let decimalResult = Decimal(result) + return NYNMoney(decimalResult) + } } diff --git a/Nynja/Modules/Interpretation/InterpretationProtocols.swift b/Nynja/Modules/Interpretation/InterpretationProtocols.swift index a351a5836..c813bfa88 100644 --- a/Nynja/Modules/Interpretation/InterpretationProtocols.swift +++ b/Nynja/Modules/Interpretation/InterpretationProtocols.swift @@ -40,10 +40,9 @@ protocol InterpretationPresenterProtocol: BasePresenterProtocol { /** * Add here your methods for communication VIEW -> PRESENTER */ - func openAssigningInterpreter() + func openAssigningInterpreter(with model: InterpretationModel) func openInterpretationType() func dismiss() - func showed() } protocol InterpretationInteractorOutputProtocol: class { diff --git a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift index e1e0b5cea..ec5531759 100644 --- a/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift +++ b/Nynja/Modules/Interpretation/Presenter/InterpretationPresenter.swift @@ -13,20 +13,28 @@ final class InterpretationPresenter: BasePresenter, InterpretationPresenterProto weak var view: InterpretationViewProtocol! var interactor: InterpretationInteractorInputProtocol! var wireFrame: InterpretationWireFrameProtocol! - - func showed() { } + private var useCaseValidationService: UseCaseValidationServiceProtocol! func openInterpretationType() { self.wireFrame.openInterpretationType(delegate: self) } - func openAssigningInterpreter() { - self.wireFrame.openAssigningInterpreter() + func openAssigningInterpreter(with model: InterpretationModel) { + self.validateInterpretationConditions(model: model) } func dismiss() { self.wireFrame.dismiss() } + + private func validateInterpretationConditions(model: InterpretationModel) { + do { + try self.useCaseValidationService.validateInterpretationLanguages(model.fromLang, toLanguage: model.toLang) + self.wireFrame.openAssigningInterpreter() + } catch { + AlertManager.sharedInstance.showAlertOk(message: error.localizedDescription) + } + } } extension InterpretationPresenter: SelectInterpretationTypeDelegate { @@ -40,11 +48,13 @@ extension InterpretationPresenter: SetInjectable { self.view = dependencies.view self.interactor = dependencies.interactor self.wireFrame = dependencies.wireFrame + self.useCaseValidationService = dependencies.useCaseValidationService } struct Dependencies { let view: InterpretationViewProtocol let interactor: InterpretationInteractorInputProtocol let wireFrame: InterpretationWireFrameProtocol + let useCaseValidationService: UseCaseValidationServiceProtocol } } diff --git a/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewController.swift b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewController.swift similarity index 68% rename from Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewController.swift rename to Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewController.swift index c8ac74d93..b374e1ff4 100644 --- a/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewController.swift +++ b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewController.swift @@ -9,11 +9,13 @@ import UIKit typealias TimeCallback = (Int) -> () +typealias EmptyCallBack = ()->() class AlertTextFieldViewController: BaseVC { var completion: TimeCallback? private var initTimeValue: Int! + private let validationService = ServiceFactory().makeTextInputValidationService() // MARK: - Views @@ -36,16 +38,16 @@ class AlertTextFieldViewController: BaseVC { view.snp.makeConstraints({ (make) in make.centerX.centerY.equalToSuperview() + make.width.equalTo(Constraints.titleLabel.width) }) return view }() lazy var titleLabel: UILabel = { let label = UILabel() - label.text = "enter_interpretation_time".localized - label.font = Texts.dafaultWhite.font - label.textColor = Texts.dafaultWhite.color - + label.text = Strings.titleLabelText + label.font = Texts.titleLabel.font + label.textColor = Texts.titleLabel.color self.containerView.addSubview(label) label.snp.makeConstraints({ (make) in @@ -53,50 +55,24 @@ class AlertTextFieldViewController: BaseVC { make.height.equalTo(Constraints.titleLabel.height) make.left.equalToSuperview().offset(Constraints.sidePadding) make.right.equalToSuperview().inset(Constraints.sidePadding) - make.bottom.equalTo(self.inputField.snp.top) + make.bottom.equalTo(inputField.snp.top) }) return label }() - lazy var inputField: TextField = { - let field = TextField() - field.keyboardType = .numberPad - field.shouldResetAfterBackground = true - field.prohibitedOptions = .all - field.textAlignment = .left - field.tintColor = Constants.colors.red.getColor() + lazy var inputField: MaterialTextField = { + let field = MaterialTextField() field.keyboardType = .numberPad - field.autocorrectionType = .no - - field.font = Texts.dafaultWhite.font - field.textColor = Texts.dafaultWhite.color - field.delegate = self self.containerView.addSubview(field) field.snp.makeConstraints { (make) in - make.height.equalTo(Constraints.inputField.height) make.left.equalToSuperview().offset(Constraints.sidePadding) make.right.equalToSuperview().offset(-Constraints.sidePadding) - make.bottom.equalTo(separator.snp.top) + make.bottom.equalTo(okButton.snp.top) } return field }() - lazy var separator: UIView = { - let separator = UIView() - - separator.backgroundColor = Constants.colors.red.getColor() - - self.containerView.addSubview(separator) - separator.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.separator.height) - make.left.equalToSuperview().offset(Constraints.sidePadding) - make.right.equalToSuperview().inset(Constraints.sidePadding) - make.bottom.equalTo(okButton.snp.top).offset(-Constraints.separator.bottom) - }) - return separator - }() - lazy var okButton: UIButton = { let button = UIButton() self.containerView.addSubview(button) @@ -110,7 +86,7 @@ class AlertTextFieldViewController: BaseVC { make.width.equalTo(Constraints.okButton.width) make.height.equalTo(Constraints.okButton.height) make.right.equalToSuperview().inset(Constraints.sidePadding) - make.left.equalTo(cancelButton.snp.right).offset(Constraints.sidePadding) + make.left.equalTo(cancelButton.snp.right).offset(Constraints.cancelButton.rightPadding) make.centerY.equalTo(cancelButton) make.bottom.equalToSuperview().offset(-Constraints.okButton.bottom) }) @@ -120,9 +96,10 @@ class AlertTextFieldViewController: BaseVC { lazy var cancelButton: UIButton = { let button = UIButton() self.containerView.addSubview(button) - let title = "cancel".localized.capitalized + let title = "cancel".localized.uppercased() button.setTitle(title, for: .normal) button.titleLabel?.font = Texts.buttons.font + button.titleLabel?.textAlignment = .right button.setTitleColor(Texts.buttons.color, for: .normal) button.addTarget(self, action: #selector(cancelButtonTapped(_:)), for: .touchUpInside) @@ -132,7 +109,7 @@ class AlertTextFieldViewController: BaseVC { }) return button }() - + // MARK: - Init @@ -151,7 +128,7 @@ class AlertTextFieldViewController: BaseVC { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.inputField.becomeFirstResponder() + let _ = self.inputField.becomeFirstResponder() } private func baseSetup() { @@ -160,30 +137,56 @@ class AlertTextFieldViewController: BaseVC { self.view.isOpaque = false self.containerView.isHidden = false self.titleLabel.isHidden = false + self.inputField.text = String(self.initTimeValue) + + self.inputField.textChanged = { [unowned self] input in + if input.text.isEmpty { + self.validate(0) + } else if let num = Int(input.text) { + self.validate(num) + } + } + self.inputField.shouldTextChanged = { (input, range, string) in + if string == "0" && range == NSRange(location: 0, length: 0) { return false } + let allowedCharacters = CharacterSet(charactersIn:"0123456789") + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) + } } // MARK: - Actions @objc func okButtonTapped(_ sender: UIButton) { - guard let text = self.inputField.text else { return } - if let num = Int(text) { - self.completion?(num) + if let number = Int(self.inputField.text) { + self.completion?(number) + self.dismiss(animated: true) } - self.dismiss(animated: true) } @objc func cancelButtonTapped(_ sender: UIButton) { self.dismiss(animated: true) } -} - -extension AlertTextFieldViewController: UITextFieldDelegate { - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - let allowedCharacters = CharacterSet(charactersIn:"0123456789") - let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) + + private func validate(_ number: Int) { + do { + let _ = try self.validationService.validateInterpretationTime(number) + self.updateInfo(with: "") + self.okButton.isEnabled = true + } catch { + self.updateInfo(with: error.localizedDescription) + self.okButton.isEnabled = false + } + } + + func updateInfo(with message: String?) { + guard let message = message else { + inputField.info = nil + return + } + let inputInfo = InputInfo(text: message, kind: .warning) + inputField.info = inputInfo } } diff --git a/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewControllerLayout.swift b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewControllerLayout.swift similarity index 62% rename from Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewControllerLayout.swift rename to Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewControllerLayout.swift index 29871f6c7..22b7088dd 100644 --- a/Nynja/Modules/Interpretation/View/TimeFieldViewController/AlertTextFieldViewControllerLayout.swift +++ b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewControllerLayout.swift @@ -10,12 +10,17 @@ import UIKit extension AlertTextFieldViewController { + struct Strings { + static let titleLabelText = "enter_interpretation_time".localized + } + struct Constraints { static let sidePadding = 24.adjustedByWidth struct titleLabel { - static let height = AlertTextFieldViewController.Texts.dafaultWhite.height + static let height = Texts.titleLabel.height + static let width = 275.adjustedByWidth static let top = 16.adjustedByWidth } @@ -23,32 +28,33 @@ extension AlertTextFieldViewController { static let height = 30.adjustedByWidth } - struct separator { - static let height = 2 - static let bottom = 8.adjustedByWidth - } - struct okButton { static let height = 32.adjustedByWidth - static let width = 44.adjustedByWidth + static let width = 32.adjustedByWidth static let bottom = 8.adjustedByWidth } struct cancelButton { static let height = 32.adjustedByWidth - static let width = 44.adjustedByWidth + static let width = 52.adjustedByWidth + static let rightPadding = 8.adjustedByWidth } } struct Texts { struct dafaultWhite { - static let height = 22.adjustedByWidth + static let height = 20.adjustedByWidth static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(height)) static let color = Constants.colors.white.getColor() } + struct titleLabel { + static let height = 17.adjustedByWidth + static let font = UIFont(fontName: Constants.fonts.regular, height: CGFloat(height)) + static let color = Constants.colors.darkGray.getColor() + } struct buttons { - private static let titleHeight = 22.adjustedByWidth + static let titleHeight = 22.adjustedByWidth static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(titleHeight)) static let color = Constants.colors.red.getColor() } diff --git a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift index a4b699f81..f6745652f 100644 --- a/Nynja/Modules/Interpretation/View/InterpretationViewController.swift +++ b/Nynja/Modules/Interpretation/View/InterpretationViewController.swift @@ -10,7 +10,7 @@ import UIKit import SnapKit final class InterpretationViewController: BaseVC, InterpretationViewProtocol { - + var presenter: InterpretationPresenterProtocol! { didSet { _presenter = presenter @@ -310,7 +310,6 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { override func viewDidLoad() { super.viewDidLoad() setupUI() - presenter.showed() } @@ -329,7 +328,7 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { self.setupPickers() self.interpretationTimeButton.setTitle("\(self.model.time) min", for: .normal) self.interpretationTypeButton.setTitle(self.model.type.localized, for: .normal) - self.totalPriceValueLabel.text = "\(self.model.price) NYN" + self.updateTotalPrice() } private func setupPickers() { @@ -381,17 +380,23 @@ final class InterpretationViewController: BaseVC, InterpretationViewProtocol { @objc private func searchButtonTapped() { - self.presenter.openAssigningInterpreter() + self.presenter.openAssigningInterpreter(with: self.model) } func setInterpretationType(_ type: InterpretationType) { self.model.type = type self.interpretationTypeButton.setTitle(type.localized, for: .normal) + self.updateTotalPrice() } private func setInterpretationTime(_ time: Int) { self.model.time = time self.interpretationTimeButton.setTitle("\(time) min", for: .normal) + self.updateTotalPrice() + } + + func updateTotalPrice() { + self.totalPriceValueLabel.text = self.model.price.formattedCurrency } } diff --git a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift index 1daa6e20b..4c02b27e3 100644 --- a/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift +++ b/Nynja/Modules/Interpretation/Wireframe/InterpretationWireFrame.swift @@ -22,7 +22,10 @@ final class InterpretationWireFrame: InterpretationWireFrameProtocol { let viewDependencies = InterpretationViewController.Dependencies(presenter: presenter, fromLangDelegate: LanguagePickerDelegate(defaultEmptyLanguage: true), toLangDelegate: LanguagePickerDelegate(defaultEmptyLanguage: false)) view.inject(dependencies: viewDependencies) - let presenterDependencies = InterpretationPresenter.Dependencies(view: view, interactor: interactor, wireFrame: self) + + let serviceFactory = ServiceFactory() + let useCaseValidationService = serviceFactory.makeUseCaseValidationServise() + let presenterDependencies = InterpretationPresenter.Dependencies(view: view, interactor: interactor, wireFrame: self, useCaseValidationService: useCaseValidationService) presenter.inject(dependencies: presenterDependencies) let interactorDependencies = InterpretationInteractor.Dependencies(presenter: presenter) interactor.inject(dependencies: interactorDependencies) diff --git a/Nynja/Modules/InterpretationType/InterpretationType.swift b/Nynja/Modules/InterpretationType/InterpretationType.swift index da752e2ad..f85ac5b13 100644 --- a/Nynja/Modules/InterpretationType/InterpretationType.swift +++ b/Nynja/Modules/InterpretationType/InterpretationType.swift @@ -27,16 +27,16 @@ enum InterpretationType: String { } } - var price: CGFloat { + var price: NYNMoney { switch self { case .general: - return 0.40 + return NYNMoney(0.40) case .technology: - return 0.50 + return NYNMoney(0.50) case .legal: - return 0.63 + return NYNMoney(0.63) case .medical: - return 0.75 + return NYNMoney(0.75) } } } diff --git a/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift index 0ee69af2d..730cf08b9 100644 --- a/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift +++ b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCell.swift @@ -116,6 +116,6 @@ class InterpretationTypeCell: UITableViewCell { func setup(with type: InterpretationType) { self.titleLabel.text = type.localized self.descriptionLabel.text = type.description - self.priceValueLabel.text = "\(type.price) NYN" + self.priceValueLabel.text = type.price.formattedCurrency } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 977facc63..2d5f6c7ef 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -870,4 +870,8 @@ "coming_soon" = "Coming soon"; "assigning_interpreter" = "Assigning Interpreter"; -"enter_interpretation_time" = "Enter interpretation time (min)"; +"enter_interpretation_time" = "Enter interpretation time (1-300)"; +"interpretation_time_out_of_range" = "Time is ranged from 1 to 300"; + +"interpretation_language_not_selected" = "Please, choose the language you want to interpret from."; +"interpretation_languages_are_equal" = "Please, choose different languages."; diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings index e022b8400..badf79152 100644 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ b/Nynja/Resources/ru.lproj/Localizable.strings @@ -788,4 +788,8 @@ "coming_soon" = "Coming soon"; "assigning_interpreter" = "Assigning Interpreter"; -"enter_interpretation_time" = "Enter interpretation time (min)"; +"enter_interpretation_time" = "Enter interpretation time (1-300)"; +"interpretation_time_out_of_range" = "Time is ranged from 1 to 300"; + +"interpretation_language_not_selected" = "Please, choose the language you want to interpret from."; +"interpretation_languages_are_equal" = "Please, choose different languages."; diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 0bbbfb723..243c17e63 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -43,6 +43,7 @@ protocol ServiceFactoryProtocol { func makeStatusCodeManager() -> StatusCodeManager func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol + func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol } final class ServiceFactory: ServiceFactoryProtocol { @@ -155,4 +156,7 @@ final class ServiceFactory: ServiceFactoryProtocol { return ChatScreenAlertFactory() } + func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol { + return UseCaseValidationService() + } } diff --git a/Nynja/Utils/Money/CryptoMoney.swift b/Nynja/Utils/Money/CryptoMoney.swift index 69db605c5..87f837a6f 100644 --- a/Nynja/Utils/Money/CryptoMoney.swift +++ b/Nynja/Utils/Money/CryptoMoney.swift @@ -32,6 +32,7 @@ extension Money where Currency: CryptoCurrency { return formatter.string(for: amount) } + } fileprivate class CryptoMoneyFormatter { @@ -67,4 +68,5 @@ fileprivate class CryptoMoneyFormatter { formatter.negativeFormat = "#,##0.###" return formatter }() + } diff --git a/Nynja/TextInputValidationService.swift b/Nynja/Validation/TextInputValidationService.swift similarity index 80% rename from Nynja/TextInputValidationService.swift rename to Nynja/Validation/TextInputValidationService.swift index de8c9ff13..b28056ba4 100644 --- a/Nynja/TextInputValidationService.swift +++ b/Nynja/Validation/TextInputValidationService.swift @@ -11,6 +11,7 @@ import Foundation protocol TextInputValidationServiceProtocol { func validateFirstName(_ name: String?) throws -> String func validateLastNameNew(_ name: String?) throws -> String + func validateInterpretationTime(_ number: Int) throws -> Int } final class TextInputValidationService: TextInputValidationServiceProtocol { @@ -38,6 +39,13 @@ final class TextInputValidationService: TextInputValidationServiceProtocol { } return text } + + func validateInterpretationTime(_ number: Int) throws -> Int { + if number < 1 || number > 300 { + throw TextInputValidationError.interpretationTimeIsOutOfRange + } + return number + } } enum TextInputValidationError: LocalizedError { @@ -46,6 +54,7 @@ enum TextInputValidationError: LocalizedError { case firstNameHasNotEnoughSymbols case firstNameHasTooMuchSymbols case lastNameHasTooMuchSymbols + case interpretationTimeIsOutOfRange var errorDescription: String? { switch self { @@ -57,6 +66,8 @@ enum TextInputValidationError: LocalizedError { return "First_Name_must_be_at_more".localized case .lastNameHasTooMuchSymbols: return "Last_Name_must_be_at_more".localized + case .interpretationTimeIsOutOfRange: + return "interpretation_time_out_of_range".localized } } } diff --git a/Nynja/Validation/UseCaseValidationService.swift b/Nynja/Validation/UseCaseValidationService.swift new file mode 100644 index 000000000..f7d868b86 --- /dev/null +++ b/Nynja/Validation/UseCaseValidationService.swift @@ -0,0 +1,40 @@ +// +// UseCaseValidationService.swift +// Nynja +// +// Created by Roman Chopovenko on 8/7/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol UseCaseValidationServiceProtocol { + func validateInterpretationLanguages(_ fromLanguage: Language, toLanguage: Language) throws +} + +final class UseCaseValidationService: UseCaseValidationServiceProtocol { + func validateInterpretationLanguages(_ fromLanguage: Language, toLanguage: Language) throws { + if fromLanguage == .empty_default { + throw UseCaseValidationError.interpretationLanguageNotSelected + } + + if fromLanguage == toLanguage { + throw UseCaseValidationError.interpretationLanguagesAreEqual + } + } +} + +enum UseCaseValidationError: LocalizedError { + + case interpretationLanguageNotSelected + case interpretationLanguagesAreEqual + + var errorDescription: String? { + switch self { + case .interpretationLanguageNotSelected: + return "interpretation_language_not_selected".localized + case .interpretationLanguagesAreEqual: + return "interpretation_languages_are_equal".localized + } + } +} -- GitLab From 5296fb6dfbd39fe101227f335813eed9bb94d9c9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 9 Aug 2018 20:24:54 +0300 Subject: [PATCH 09/32] [NY-2285] History is scrolled up when open chat page after login (#1080) * Refactored MessageDS in order to be able to handle reversed datasource. * Replace tableView with collectionView * Fixed 'reloadIfVisible' * Reversed collectionView * Move CallInfoView to root view hierarchy --- Nynja.xcodeproj/project.pbxproj | 48 +- Nynja/Extensions/UIEdgeInsets+Adjust.swift | 3 + .../Message/Protocols/MessageProtocols.swift | 3 +- .../Message/View/MessageVC+CellDelegate.swift | 6 +- Nynja/Modules/Message/View/MessageVC.swift | 677 +++++++++--------- .../Views/CallInfoView/CallInfoView.swift | 22 +- .../MessageCollectionViewDataSource.swift | 180 +++++ .../MessageCollectionViewDelegate.swift | 149 ++++ .../MessageCollectionViewLayout.swift | 94 +++ .../ChatCells/BaseChatCell/BaseChatCell.swift | 9 +- .../Views/TableView/Cells/SystemCell.swift | 5 +- .../View/Views/TableView/Cells/TimeCell.swift | 3 +- .../Views/TableView/Cells/UnreadCell.swift | 7 +- .../View/Views/TableView/MessageDS.swift | 71 -- .../TableView/MessageTableViewDelegate.swift | 155 ---- .../View/RepliesCollectionViewDelegate.swift | 46 ++ Nynja/Modules/Replies/View/RepliesDS.swift | 24 +- .../View/RepliesTableViewDelegate.swift | 40 -- Nynja/Modules/Replies/View/RepliesVC.swift | 88 +-- 19 files changed, 933 insertions(+), 697 deletions(-) create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift delete mode 100644 Nynja/Modules/Message/View/Views/TableView/MessageDS.swift delete mode 100644 Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift create mode 100644 Nynja/Modules/Replies/View/RepliesCollectionViewDelegate.swift delete mode 100644 Nynja/Modules/Replies/View/RepliesTableViewDelegate.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 60d2e6b60..b4d8be21f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -415,7 +415,7 @@ 26EA201520BECDCA00FBB9CA /* ConversationLanguageSettingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EA201420BECDCA00FBB9CA /* ConversationLanguageSettingService.swift */; }; 26EAA2CB20D2497F005697CB /* TranslationAutoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EAA2CA20D2497F005697CB /* TranslationAutoView.swift */; }; 26ED2C1820042683002DBBE8 /* RepliesDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ED2C1720042683002DBBE8 /* RepliesDS.swift */; }; - 26ED2C1A2004276B002DBBE8 /* RepliesTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ED2C192004276B002DBBE8 /* RepliesTableViewDelegate.swift */; }; + 26ED2C1A2004276B002DBBE8 /* RepliesCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ED2C192004276B002DBBE8 /* RepliesCollectionViewDelegate.swift */; }; 26EEA5472091F84E0066D3B0 /* CollectionsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEA5462091F84E0066D3B0 /* CollectionsExtensions.swift */; }; 26EEA5482091F84E0066D3B0 /* CollectionsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEA5462091F84E0066D3B0 /* CollectionsExtensions.swift */; }; 26F03C0D20698B0000712CB0 /* ChatWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F03C0C20698B0000712CB0 /* ChatWheelItemModel.swift */; }; @@ -793,6 +793,8 @@ 853FB0702049B396000996C5 /* SupportItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853FB06F2049B396000996C5 /* SupportItemsFactory.swift */; }; 853FB0752049B4FF000996C5 /* TextTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853FB0742049B4FF000996C5 /* TextTableViewCell.swift */; }; 853FB0772049B7CA000996C5 /* TextCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853FB0762049B7CA000996C5 /* TextCellViewModel.swift */; }; + 8540A331211B34B4007F65AF /* MessageCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */; }; + 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */; }; 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */; }; @@ -812,6 +814,7 @@ 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */; }; 854A4B312080D6C400759152 /* CellWithImageCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2F2080D6C400759152 /* CellWithImageCellModel.swift */; }; 854CFB08210704AE00FBC133 /* CGRectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */; }; + 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */; }; 854FC1CB204468FC00B12BE5 /* CarouselFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854FC1CA204468FC00B12BE5 /* CarouselFlowLayout.swift */; }; 8557987F2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557987D2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift */; }; 855798802093200D007050B8 /* StickerMenuActionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557987E2093200D007050B8 /* StickerMenuActionCellModel.swift */; }; @@ -1403,8 +1406,6 @@ A45F113220B4218D00F45004 /* BaseChatCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10EF20B4218D00F45004 /* BaseChatCellLayout.swift */; }; A45F113320B4218D00F45004 /* MessageCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10F020B4218D00F45004 /* MessageCellFactory.swift */; }; A45F113420B4218D00F45004 /* UnreadCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10F120B4218D00F45004 /* UnreadCell.swift */; }; - A45F113520B4218D00F45004 /* MessageTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10F220B4218D00F45004 /* MessageTableViewDelegate.swift */; }; - A45F113620B4218D00F45004 /* MessageDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10F320B4218D00F45004 /* MessageDS.swift */; }; A45F113720B4218D00F45004 /* ReplyPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10F520B4218D00F45004 /* ReplyPreview.swift */; }; A45F113820B4218D00F45004 /* AvatarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10F720B4218D00F45004 /* AvatarViewModel.swift */; }; A45F113920B4218D00F45004 /* AvatarViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F10F820B4218D00F45004 /* AvatarViewLayout.swift */; }; @@ -2441,7 +2442,7 @@ 26EA201420BECDCA00FBB9CA /* ConversationLanguageSettingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationLanguageSettingService.swift; sourceTree = ""; }; 26EAA2CA20D2497F005697CB /* TranslationAutoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationAutoView.swift; sourceTree = ""; }; 26ED2C1720042683002DBBE8 /* RepliesDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepliesDS.swift; sourceTree = ""; }; - 26ED2C192004276B002DBBE8 /* RepliesTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepliesTableViewDelegate.swift; sourceTree = ""; }; + 26ED2C192004276B002DBBE8 /* RepliesCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepliesCollectionViewDelegate.swift; sourceTree = ""; }; 26EEA5462091F84E0066D3B0 /* CollectionsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionsExtensions.swift; sourceTree = ""; }; 26F03C0C20698B0000712CB0 /* ChatWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWheelItemModel.swift; sourceTree = ""; }; 26F47051201B7248005D3192 /* ReturnToCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnToCallView.swift; sourceTree = ""; }; @@ -2808,6 +2809,8 @@ 853FB06F2049B396000996C5 /* SupportItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportItemsFactory.swift; sourceTree = ""; }; 853FB0742049B4FF000996C5 /* TextTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextTableViewCell.swift; sourceTree = ""; }; 853FB0762049B7CA000996C5 /* TextCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCellViewModel.swift; sourceTree = ""; }; + 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDataSource.swift; sourceTree = ""; }; + 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDelegate.swift; sourceTree = ""; }; 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenPresenter.swift; sourceTree = ""; }; @@ -2827,6 +2830,7 @@ 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; 854A4B2F2080D6C400759152 /* CellWithImageCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageCellModel.swift; sourceTree = ""; }; 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectExtensions.swift; sourceTree = ""; }; + 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewLayout.swift; sourceTree = ""; }; 854FC1CA204468FC00B12BE5 /* CarouselFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselFlowLayout.swift; sourceTree = ""; }; 8557987D2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerMenuActionCollectionViewCell.swift; sourceTree = ""; }; 8557987E2093200D007050B8 /* StickerMenuActionCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerMenuActionCellModel.swift; sourceTree = ""; }; @@ -3292,8 +3296,6 @@ A45F10EF20B4218D00F45004 /* BaseChatCellLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatCellLayout.swift; sourceTree = ""; }; A45F10F020B4218D00F45004 /* MessageCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellFactory.swift; sourceTree = ""; }; A45F10F120B4218D00F45004 /* UnreadCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCell.swift; sourceTree = ""; }; - A45F10F220B4218D00F45004 /* MessageTableViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTableViewDelegate.swift; sourceTree = ""; }; - A45F10F320B4218D00F45004 /* MessageDS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDS.swift; sourceTree = ""; }; A45F10F520B4218D00F45004 /* ReplyPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyPreview.swift; sourceTree = ""; }; A45F10F720B4218D00F45004 /* AvatarViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarViewModel.swift; sourceTree = ""; }; A45F10F820B4218D00F45004 /* AvatarViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarViewLayout.swift; sourceTree = ""; }; @@ -4609,7 +4611,7 @@ children = ( 264638211FFFE253002590E6 /* RepliesHeaderView */, 26ED2C1720042683002DBBE8 /* RepliesDS.swift */, - 26ED2C192004276B002DBBE8 /* RepliesTableViewDelegate.swift */, + 26ED2C192004276B002DBBE8 /* RepliesCollectionViewDelegate.swift */, 2646381F1FFFD6A2002590E6 /* RepliesVC.swift */, 261F2E2D200EB0AD007D0813 /* RepliesVC+CellDelegate.swift */, ); @@ -7506,6 +7508,16 @@ path = Image; sourceTree = ""; }; + 854D13D6211B2E6200E139FC /* CollectionView */ = { + isa = PBXGroup; + children = ( + 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */, + 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */, + 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */, + ); + path = CollectionView; + sourceTree = ""; + }; 8557987C20931FE8007050B8 /* Menu */ = { isa = PBXGroup; children = ( @@ -9313,6 +9325,7 @@ A45F10C820B4218D00F45004 /* Views */ = { isa = PBXGroup; children = ( + 854D13D6211B2E6200E139FC /* CollectionView */, 5BC1D37E20D3B54B002A44B3 /* CallInfoView */, A45F10C920B4218D00F45004 /* TableView */, A45F10F420B4218D00F45004 /* ReplyPreview */, @@ -9327,8 +9340,6 @@ isa = PBXGroup; children = ( A45F10CA20B4218D00F45004 /* Cells */, - A45F10F220B4218D00F45004 /* MessageTableViewDelegate.swift */, - A45F10F320B4218D00F45004 /* MessageDS.swift */, ); path = TableView; sourceTree = ""; @@ -13607,7 +13618,6 @@ 2648C41E2069B5B300863614 /* ChangeNumberItemsFactory.swift in Sources */, 260313AC20A0A4BA009AC66D /* LanguageSettings+Helper.swift in Sources */, A45F112D20B4218D00F45004 /* MessageContentView.swift in Sources */, - A45F113520B4218D00F45004 /* MessageTableViewDelegate.swift in Sources */, A42D52C3206A53AA00EEB952 /* Star_Spec.swift in Sources */, 26D35AB81FD0EFA800A5D513 /* AudioPlayer.swift in Sources */, 853FB0702049B396000996C5 /* SupportItemsFactory.swift in Sources */, @@ -13654,6 +13664,7 @@ FBCE83D520E52397003B7558 /* MQTTServiceWallet.swift in Sources */, 853D55B820CE72C60080659F /* StickerContentDataSource.swift in Sources */, 85CB25DC20D723D300D5E565 /* StickerPackDAOProtocol.swift in Sources */, + 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */, 4B06D3152028A537003B275B /* ContactsItemsFactory.swift in Sources */, A48C154420EF7A15002DA994 /* LinkHandler.swift in Sources */, A45F113320B4218D00F45004 /* MessageCellFactory.swift in Sources */, @@ -13921,7 +13932,6 @@ FBCE841420E525A6003B7558 /* NetworkService.swift in Sources */, A409B1CF2108D48E0051C20B /* QueryFactory.swift in Sources */, A42D52B7206A53AA00EEB952 /* reader_Spec.swift in Sources */, - A45F113620B4218D00F45004 /* MessageDS.swift in Sources */, F119E66A20D24B960043A532 /* MultiplePreviewProtocols.swift in Sources */, 850FC611203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift in Sources */, 0062D93D2062EC4100B915AC /* InviteFriendsWireframe.swift in Sources */, @@ -14165,7 +14175,7 @@ 3A1AAFCE1F3DF0470098780A /* DateExtensions.swift in Sources */, 26C061C01FEAA04A00A2EBE4 /* FeatureExtension+BERT.swift in Sources */, 2651093F20ADB81100F1B38B /* NotificationSettingProtocol.swift in Sources */, - 26ED2C1A2004276B002DBBE8 /* RepliesTableViewDelegate.swift in Sources */, + 26ED2C1A2004276B002DBBE8 /* RepliesCollectionViewDelegate.swift in Sources */, 8514D52220EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift in Sources */, A45F112820B4218D00F45004 /* MessageViewFactory.swift in Sources */, F117871D20ACF1D0007A9A1B /* CameraSettingsService.swift in Sources */, @@ -14629,6 +14639,7 @@ 54FFFD58388E2B660C1E5A05 /* MapPresenter.swift in Sources */, A42D52D6206A53AB00EEB952 /* serviceTask_Spec.swift in Sources */, 0AB08BA89A51118248FA3233 /* MapInteractor.swift in Sources */, + 8540A331211B34B4007F65AF /* MessageCollectionViewDataSource.swift in Sources */, A48C154220EF76EE002DA994 /* LinkExtension.swift in Sources */, A42D52AD206A53AA00EEB952 /* log_Spec.swift in Sources */, F6150A15F8A3E399EEB2C724 /* MapWireframe.swift in Sources */, @@ -14806,6 +14817,7 @@ 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */, A4569873060C49904EF8C555 /* EditGroupPhotoViewController.swift in Sources */, 8502DB502061030000613C8C /* WheelPositionPickerWireFrame.swift in Sources */, + 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */, 0062D9452062EC4100B915AC /* InviteFriendsDS.swift in Sources */, F117872620ACF2DB007A9A1B /* VideoQuality.swift in Sources */, FBDA34E920921079009F4FB6 /* KeyboardLayoutGuide.swift in Sources */, @@ -15484,7 +15496,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -15496,8 +15508,8 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "99725127-a6e8-4f22-98eb-18164dfae6db"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_AdHocExt; + PROVISIONING_PROFILE = "0ac45157-141b-4e67-9a87-602efcd8cb35"; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; SKIP_INSTALL = YES; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -15659,7 +15671,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -15669,8 +15681,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "50dd2a41-0a8c-4a0a-a8b9-744fa4bf3bb4"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_adhoc; + PROVISIONING_PROFILE = "e9cc21bd-73cb-4b39-92ab-097127d12162"; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; diff --git a/Nynja/Extensions/UIEdgeInsets+Adjust.swift b/Nynja/Extensions/UIEdgeInsets+Adjust.swift index f1b8ecc2d..b507e9d7d 100644 --- a/Nynja/Extensions/UIEdgeInsets+Adjust.swift +++ b/Nynja/Extensions/UIEdgeInsets+Adjust.swift @@ -43,4 +43,7 @@ extension UIEdgeInsets { right: lhs.right + rhs.right) } + func flippedVertically() -> UIEdgeInsets { + return UIEdgeInsets(top: bottom, left: left, bottom: top, right: right) + } } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 22823aeb0..bd4dd59bd 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -265,10 +265,9 @@ protocol MessageViewProtocol: class { func showContextMenu(fromCell cell: BaseChatCell, convertingModel: ConvertionMessageModel?, targetView: UIView?) - func scrollToBottomIfNeeded() func scrollToMessage(with localId: String?) - func scrollToUnreadMessage(with index: Int) func scrollToMessage(serverId: Int64) + func scrollToBottomIfNeeded() func scrollToBottom() func updateHeaderStatus(_ status: String) diff --git a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift index 78ff22af8..afe12aea7 100644 --- a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift +++ b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift @@ -12,11 +12,11 @@ extension MessageVC: BaseChatCellDelegate, AudioManagerDelegate, ProximitySensor // MARK: - BaseChatCellDelegate func didCellTapped(_ cell: BaseChatCell) { - guard let model = cell.model else { + guard let model = cell.model, let type = model.type else { return } - - switch model.type! { + + switch type { case .image: if let url = model.fileUrl { let contentImageView = cell.transitionImageView diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index ba19708d1..60bea51c5 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -18,7 +18,7 @@ fileprivate enum ButtonState { case none } -final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSwipable { +final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSwipable, CallInfoViewDelegate { var presenter: MessagePresenterProtocol! { didSet { @@ -29,11 +29,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw var progressDictionary = [String: [(ProgressModel)->Void]]() var loadingStatus = false - var isHideKeyboard = true - var verticalOffset: CGFloat = 0.0 - var messageDS: MessageDS! - var messageTVDelegate: MessageTableViewDelegate! + var messageDS: MessageCollectionViewDataSource! + var messageTVDelegate: MessageCollectionViewDelegate! var contextMenuPresented = false @@ -67,6 +65,12 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw static let convertionModel = "convertionModel" static let starId = "starID" } + + private let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "YYYY-MM-dd" + return dateFormatter + }() // MARK: - Views private var bottomInset = Constraints.replyPreview.bottomInset @@ -94,33 +98,35 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw make.left.right.equalToSuperview() } } - - private(set) lazy var tableView: UITableView = { - let tv = UITableView() - - tv.separatorStyle = .none - tv.backgroundColor = UIColor.clear - - registerCells(for: tv) - - tv.estimatedRowHeight = 0 // NOTE: It is important for scrolling behavior. - tv.estimatedSectionHeaderHeight = 0 + + private let collectionViewLayout: UICollectionViewFlowLayout = { + let layout = MessageCollectionViewLayout() + layout.minimumLineSpacing = 0 + return layout + }() + + private let defaultCollectionViewInsets = UIEdgeInsets(top: Constraints.tableView.defaultVerticalInset, + left: 0, + bottom: Constraints.tableView.bottomInset, + right: 0).flippedVertically() + + private(set) lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.backgroundColor = .clear + collectionView.alwaysBounceVertical = true + collectionView.transform = CGAffineTransform(rotationAngle: .pi) - - tv.contentInset = UIEdgeInsets( - top: Constraints.tableView.defaultVerticalInset, - left: 0, - bottom: Constraints.tableView.bottomInset, - right: 0 - ) - - self.view.addSubview(tv) - tv.snp.makeConstraints { make in + collectionView.contentInset = defaultCollectionViewInsets + + view.addSubview(collectionView) + collectionView.snp.makeConstraints { make in makeDefaultTableConstraint(make) make.bottom.equalTo(staticFooterView.snp.top) } - - return tv + + registerCells(for: collectionView) + + return collectionView }() private(set) lazy var inputBar: InputBar = { @@ -165,11 +171,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw self?.presenter.sendTyping(status) } - inputBar.changesHeightHandler = { [weak self] diff in - guard let `self` = self else { return } - self.tableView.contentInset.top -= diff - self.tableView.contentOffset.y += diff - } inputBar.inputTypeChangeHandler = { [weak self] inputType in self?.stickerInputState.performUpdates { state in state.inputType = inputType.toggled() @@ -229,7 +230,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw view.snp.makeConstraints({ (make) in make.height.equalTo(gradientHeight) make.left.right.equalToSuperview() - make.bottom.equalTo(tableView.snp.bottom) + make.bottom.equalTo(collectionView.snp.bottom) }) return view @@ -294,7 +295,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw view.addSubview(searchView) searchView.snp.makeConstraints { maker in maker.left.right.equalToSuperview() - maker.bottom.equalTo(tableView.snp.bottom) + maker.bottom.equalTo(collectionView.snp.bottom) } searchView.setupContentInset(to: gradientView) @@ -346,7 +347,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw view.addSubview(mentionPanelView) mentionPanelView.snp.makeConstraints { maker in - maker.edges.equalTo(tableView) + maker.edges.equalTo(collectionView) } return mentionPanelView @@ -369,6 +370,17 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw return counterView }() + + + // MARK: - Calls + + private weak var callInfoView: CallInfoView? { + didSet { + if callInfoView == nil { + oldValue?.removeFromSuperview() + } + } + } // MARK: - Initialize @@ -381,11 +393,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw audioManager.delegate = self proximityManager.delegate = self - messageDS = MessageDS(view: self) - tableView.dataSource = messageDS + messageDS = MessageCollectionViewDataSource(view: self) + collectionView.dataSource = messageDS - messageTVDelegate = MessageTableViewDelegate(view: self) - tableView.delegate = messageTVDelegate + messageTVDelegate = MessageCollectionViewDelegate(view: self) + collectionView.delegate = messageTVDelegate replyPreview.delegate = self gradientView.isHidden = false @@ -407,7 +419,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw mentionCounterView.isHidden = true stickerSearchResultView.isHidden = true - messageTVDelegate.rejoinBannerDisplayed = presenter.hasRunningCall() + displayRejoinBanner(display: presenter.hasRunningCall(), count: 0) } deinit { @@ -418,21 +430,22 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw super.viewDidAppear(animated) viewVisible = true - NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationWillEnterForeground, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: .UIApplicationDidEnterBackground, object: nil) + let center = NotificationCenter.default + center.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationWillEnterForeground, object: nil) + center.addObserver(self, selector: #selector(didEnterBackground), name: .UIApplicationDidEnterBackground, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.willResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + center.addObserver(self, selector: #selector(willResignActive), name: .UIApplicationWillResignActive, object: nil) + center.addObserver(self, selector: #selector(didBecomeActive), name: .UIApplicationDidBecomeActive, object: nil) - tableView.layoutIfNeeded() + collectionView.layoutIfNeeded() presenter.viewDidAppear() } - @objc func willResignActive() { + @objc private func willResignActive() { goAway() } - @objc func didBecomeActive() { + @objc private func didBecomeActive() { goAway() } @@ -458,13 +471,20 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + + updateScrollIndicatorInsets() - let expandedHeight = self.tableView.bounds.height + CGFloat(FooterViewLayout.collapsedHeight) + let expandedHeight = collectionView.bounds.height + CGFloat(FooterViewLayout.collapsedHeight) footerView?.update(constraintLayout: CollapsedView.ConstraintLayout(availableHeight: [CGFloat(FooterViewLayout.collapsedHeight), CGFloat(FooterViewLayout.middleHeight), expandedHeight])) } + + private func updateScrollIndicatorInsets() { + let indicatorInset = collectionView.bounds.width - collectionView.scrollIndicatorInsets.left - 8 + collectionView.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: indicatorInset) + } // MARK: - BaseVC @@ -480,38 +500,18 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } override func keyboardNotified(endFrame: CGRect) { - let currentTableBounds = tableView.bounds - if endFrame.origin.y >= UIScreen.main.bounds.size.height { - if !isHideKeyboard { - let newTableBounds = currentTableBounds.updating(height: currentTableBounds.height + verticalOffset) - adjustTableTopInset(for: newTableBounds) - - tableView.contentOffset = CGPoint(x: 0, y: tableView.contentOffset.y - verticalOffset) - verticalOffset = 0 - } - isHideKeyboard = true - updateToHide() - + updateToHide(view: inputBar, offset: 0) } else { - if isHideKeyboard { - verticalOffset = CGFloat(endFrame.height) - safeAreaBottomInset() - let newTableBounds = currentTableBounds.updating(height: currentTableBounds.height - verticalOffset) - adjustTableTopInset(for: newTableBounds) - - tableView.contentOffset = CGPoint(x: 0, y: tableView.contentOffset.y + verticalOffset) - } - isHideKeyboard = false - updateToShow(endFrame: endFrame) + updateToShow(view: inputBar, offset: -endFrame.height) } } - + @objc private func wheelDidOpen() { endInputBarInteraction() } - //MARK: - Translation private func handleUpdateForTranslation(in textView: UITextView) { let inputText = textView.text ?? "" @@ -670,9 +670,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw // MARK: - Mentions func setupUnreadMentions(after index: Int) { - var serverId = messageDS.cells[index].serverID + var serverId = messageDS.cellModel(at: index).serverID - if serverId == nil, let previousModel = messageDS.cells[.. CGPoint { - return CGPoint(x: tableView.bounds.width / 2, - y: tableView.contentOffset.y + tableView.bounds.height - tableView.contentInset.bottom) - } - func lastVisibleCellIndex() -> Int? { - let point = lastMessagePosition() - return tableView.indexPathForRow(at: point)?.row + let offset = messageDS.isReversed + ? collectionView.contentInset.top + : collectionView.bounds.height - collectionView.contentInset.bottom + + let lastMessagePosition = CGPoint( + x: collectionView.bounds.width / 2, + y: collectionView.contentOffset.y + offset + ) + return collectionView.indexPathForItem(at: lastMessagePosition)?.item } @@ -823,12 +825,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateData(with configuration: DisplayChatConfiguration) { - let isLastMessageVisible = self.isLastMessageVisible - - messageDS.cells = configuration.cells - tableView.reloadData() - adjustTableTopInset() - + messageDS.update(configuration.cells) + collectionView.reloadData() + hasUnread = configuration.isUnreadShown adjustPosition(with: configuration, isLastMessageVisible: isLastMessageVisible) adjustButtonState(configuration) @@ -836,13 +835,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw updateUnreadMentions() } - func updateNewData(_ cells: [BaseChatCellModel]) { - var result = cells - + func updateNewData(_ history: [BaseChatCellModel]) { var lastTimeShowed: Date? - for i in (0.. cells.count { - let indexPath = IndexPath(row: cells.count, section: 0) - let rect = tableView.rectForRow(at: indexPath) - tableView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell + if !messageDS.isReversed, messageDS.count > history.count { + let indexPath = IndexPath(row: history.count, section: 0) + guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else { + return + } + let rect = attributes.frame + collectionView.contentOffset.y = rect.minY + initialOffset.y - offsetForDeletedTimeCell } loadingStatus = false @@ -904,7 +899,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw case .message(let localId): scrollToMessage(with: localId) case .unread(let index): - scrollToUnreadMessage(with: Int(index)) + let index = messageDS.presentationIndex(from: index) + scrollToUnreadMessage(with: index) case .checkpoint(let checkpoint): scrollToCheckpoint(checkpoint) case .lastMessage: @@ -944,7 +940,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func newMessage(_ cell: BaseChatCellModel) { - let isLastMessageVisible = self.isLastMessageVisible if let messageDS = self.messageDS { var unreadCellIndex: Int? @@ -952,33 +947,28 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw hasUnread = false unreadCellIndex = messageDS.unreadCellIndex } - + readMessage(cell) - tableView.performUpdates(animated: false) { + collectionView.performBatchUpdates({ if let unrIndex = unreadCellIndex { - messageDS.cells.remove(at: unrIndex) - tableView.deleteRows(at: [IndexPath(row: unrIndex, section: 0)], with: .none) + messageDS.remove(at: unrIndex) + collectionView.deleteItems(at: [IndexPath(row: unrIndex, section: 0)]) } - - messageDS.cells.append(cell) - tableView.insertRows(at: [IndexPath(row: messageDS.cells.count - 1, section: 0)], with: .none) - } - adjustTableTopInset() - - if isLastMessageVisible || cell.isOwner { - dispatchAsyncMainAfter(0.01, block: { - self.scrollToBottom(animated: false) - }) - } else { - buttonState = .newMessages - } + messageDS.addNewMessage(cell) + collectionView.insertItems(at: [IndexPath(row: messageDS.bottomIndex, section: 0)]) + + }, completion: { _ in + if self.isLastMessageVisible || cell.isOwner { + dispatchAsyncMainAfter(0.01) { + self.scrollToBottom(animated: false) + } + } else { + self.buttonState = .newMessages + } + }) } } - - private var unreadCellIndex: Int? { - return messageDS.cells.index(where: { $0.unread == true }) - } func updateProgress(progressModel: ProgressModel?) { DispatchQueue.main.async { @@ -992,30 +982,38 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func updateTranslationProgress(progressModel: ConvertionProgressModel?) { DispatchQueue.main.async { let id = progressModel?.id - let list = self.messageDS.cells.filter { $0.id == id } + let list = self.messageDS.filter { $0.id == id } list.forEach { $0.translationProgressModel = progressModel } self.reloadIfVisible(models: list) } } func updateMessage(_ model: BaseChatCellModel) { - if let messageId = model.id, let index = messageDS.cells.index(where: { $0.id == messageId }) { - model.deliveryStatus = messageDS.cells[index].deliveryStatus // TODO: castil - messageDS.cells[index] = model - //TODO: I think we need to do it outside this class - var shouldReload = [model] - messageDS.cells.forEach { (mod) in - switch mod.messageType { - case .reply(let replyModel, _): - if replyModel.id == messageId && replyModel.type.rawValue == "text" { - replyModel.text = model.text ?? "" - shouldReload.append(mod) - } - default: break + guard let messageId = model.id, let index = messageDS.index(where: { $0.id == messageId }) else { + return + } + model.deliveryStatus = messageDS.cellModel(at: index).deliveryStatus // TODO: castil + messageDS.set(model, at: index) + + //TODO: I think we need to do it outside this class + var shouldReload = [model] + + for idx in messageDS.indices { + let cellModel = messageDS.cellModel(at: idx) + + switch cellModel.messageType { + case let .reply(replyModel, _): + if replyModel.id == messageId && replyModel.type == .text { + // TODO: in this case we don'a actually need to reload cell, because height won't be changed. + // It would be better to update only cells layout. + replyModel.text = model.text ?? "" + shouldReload.append(cellModel) } + default: + break } - reloadIfVisible(models: shouldReload) } + reloadIfVisible(models: shouldReload) } func showContextMenu(fromCell cell: BaseChatCell, convertingModel: ConvertionMessageModel? = nil, targetView: UIView? = nil) { @@ -1041,7 +1039,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw design: NynjaContextMenuItemsFactory.defaultDesign, userInfo: itemsFactory.userInfo, targetView: targetView, - targetMask: tableView, + targetMask: collectionView, containerViewMaskRect: menuMask) let menu = NynjaContextMenu() @@ -1100,18 +1098,15 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateDeliveryStatus(_ status: DeliveryStatus, messageId: String) { - let cells = self.messageDS.cells - - guard let index = cells.index(where: { $0.id == messageId }) else { + guard let index = messageDS.index(where: { $0.id == messageId }) else { return } - if status != .read { - let model = cells[index] + let model = messageDS.cellModel(at: index) model.deliveryStatus = status reloadIfVisible(models: [model]) } else { - markMesssagesAsRead(from: index, cells: cells) + markMesssagesAsRead(from: index) } } @@ -1124,35 +1119,30 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateStar(starID: String?, messageId: String) { - let cells = self.messageDS.cells - - guard let index = cells.index(where: { $0.id == messageId }) else { + guard let model = messageDS.first(where: { $0.id == messageId }) else { return } - let model = cells[index] model.starID = starID reloadIfVisible(models: [model]) } func updateUnreadTitle(_ count: Int) { - let index = min(messageDS.cells.count, count) + let index = messageDS.presentationIndex(from: min(messageDS.count, count)) let unreadIndex = messageDS.unreadCellIndex - - tableView.performUpdates { + + collectionView.performBatchUpdates({ if let unreadIndex = unreadIndex { - messageDS.cells.swapAt(unreadIndex, index) - tableView.moveRow(at: makeIndexPathForUpdate(with: unreadIndex), - to: makeIndexPathForUpdate(with: index)) + messageDS.swapAt(unreadIndex, index) + collectionView.moveItem(at: makeIndexPathForUpdate(with: unreadIndex), + to: makeIndexPathForUpdate(with: index)) } else { - messageDS.cells.insert(BaseChatCellModel.unreadModel(), at: index) - tableView.insertRows(at: [makeIndexPathForUpdate(with: index)], with: .none) + messageDS.insert(.unreadModel(), at: index) + collectionView.insertItems(at: [makeIndexPathForUpdate(with: index)]) } - } - adjustTableTopInset() - scrollToUnreadMessage(with: index + 1) - - let lastIndex = messageDS.cells.count - 1 - readMessage(messageDS.cells[lastIndex]) + }, completion: { _ in + self.scrollToUnreadMessage(with: index + 1) + self.messageDS.bottomModel.map { self.readMessage($0) } + }) } private func makeIndexPathForUpdate(with row: Int) -> IndexPath { @@ -1160,39 +1150,56 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } // MARK: Scroll to message + func scrollToMessage(with localId: String?) { - if let id = localId, let index = messageDS.cells.index(where: { $0.id == id }) { - scrollToMessage(with: index) - } else if !messageDS.cells.isEmpty { + scrollToMessage(with: localId, scrollPosition: .bottom) + } + + func scrollToMessage(with localId: String?, scrollPosition: UICollectionViewScrollPosition) { + if let id = localId, let index = messageDS.index(where: { $0.id == id }) { + scrollToMessage(with: index, scrollPosition: scrollPosition) + } else if !messageDS.isEmpty { scrollToBottom() } } private func scrollToCheckpoint(_ checkpoint: ChatCheckpoint) { - if canScroll { - scrollToMessage(with: checkpoint.localId) - if checkpoint.topOffset > 0 { - var contentOffset = tableView.contentOffset.y + CGFloat(checkpoint.topOffset) - contentOffset = min(contentOffset, tableView.contentSize.height - tableView.bounds.height) - tableView.contentOffset.y = contentOffset + (gradientHeight - tableView.contentInset.bottom) - } + collectionView.layoutIfNeeded() + guard let index = messageDS.index(where: { $0.id == checkpoint.localId }) else { + return } + let indexPath = IndexPath(item: index, section: 0) + + guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else { + return + } + + var contentOffset = attributes.frame.minY + contentOffset = contentOffset - CGFloat(checkpoint.topOffset) + contentOffset = max(-collectionView.contentInset.top, + min(contentOffset, collectionView.contentSize.height - collectionView.bounds.height)) + + collectionView.contentOffset.y = contentOffset } - func scrollToMessage(with index: Int) { + private func scrollToMessage(with index: Int, scrollPosition: UICollectionViewScrollPosition = .bottom) { let indexPath = IndexPath(row: index, section: 0) - scroll(to: indexPath, at: .top, animated: false) + scroll(to: indexPath, at: scrollPosition, animated: false) } - func scrollToUnreadMessage(with index: Int) { - let prevInd = index - 1 - let cellModel = messageDS.cells[safe: prevInd] - guard cellModel?.unread == true else { return } + private func scrollToUnreadMessage(with index: Int) { + guard case let prevInd = index - 1, messageDS.indices.contains(prevInd) else { + return + } + let cellModel = messageDS.cellModel(at: prevInd) + guard cellModel.unread == true else { + return + } scrollToMessage(with: prevInd) } func scrollToMessage(serverId: Int64) { - guard let index = messageDS.cells.index(where: { $0.serverID == serverId }) else { + guard let index = messageDS.index(where: { $0.serverID == serverId }) else { return } scrollToMessage(with: index) @@ -1252,24 +1259,30 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func displayRejoinBanner(display: Bool, count: Int) { - self.messageTVDelegate.rejoinBannerDisplayed = display - self.messageTVDelegate.membersCount = count - self.tableView.reloadData() - adjustTableTopInset() + // Use bottom inset, because collectionView has reversed transform: + // collectionView.transform = CGAffineTransform(rotationAngle: .pi) + var inset = defaultCollectionViewInsets.bottom + + if display { + let callInfoView = self.callInfoView ?? makeRejoinBar() + callInfoView.membersCount = count + callInfoView.delegate = self + self.callInfoView = callInfoView + + inset += CallInfoView.Constraints.baseSizes.height + } else { + callInfoView = nil + } + + if collectionView.contentInset.bottom != inset { + collectionView.contentInset.bottom = inset + } } private func toggleReplyPreview(_ shouldShow: Bool) { let isShown = !replyPreview.isHidden guard shouldShow != isShown else { return } - let replyPreviewHeight = CGFloat(Constraints.replyPreview.height) - if shouldShow { - tableView.contentOffset.y += replyPreviewHeight - tableView.contentInset.top -= replyPreviewHeight - } else { - tableView.contentInset.top += replyPreviewHeight - } - replyPreview.isHidden = !shouldShow if shouldShow { @@ -1356,63 +1369,71 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw }) } - private func registerCells(for tableView: UITableView) { + private func registerCells(for collectionView: UICollectionView) { MessageCellFactory.selfIdentifiers.forEach { - tableView.register(BaseChatCell.self, forCellReuseIdentifier: $0) + collectionView.register(BaseChatCell.self, forCellWithReuseIdentifier: $0) } MessageCellFactory.opponenetIdentifiers.forEach { - tableView.register(OponentChatCell.self, forCellReuseIdentifier: $0) + collectionView.register(OponentChatCell.self, forCellWithReuseIdentifier: $0) } - tableView.register(SystemCell.self, forCellReuseIdentifier: "system") - tableView.register(TimeCell.self, forCellReuseIdentifier: "time") - tableView.register(UnreadCell.self, forCellReuseIdentifier: "unread") + collectionView.register(SystemCell.self, forCellWithReuseIdentifier: "system") + collectionView.register(TimeCell.self, forCellWithReuseIdentifier: "time") + collectionView.register(UnreadCell.self, forCellWithReuseIdentifier: "unread") } func reloadIfVisible(models: [BaseChatCellModel]) { - tableView.performUpdates { + collectionView.performBatchUpdates({ var indexPaths: [IndexPath] = [] - tableView.visibleCells.forEach { (cell) in - if let model = (cell as? BaseChatCell)?.model { - if let index = models.index(where: { (mod) -> Bool in - if mod.id != nil { - return mod.id == model.id - } - - if mod.text != nil { - return mod.text == model.text - } - - return false - }) { - (cell as? BaseChatCell)?.updateData(models[index]) - indexPaths.append(tableView.indexPath(for: cell)!) + collectionView.visibleCells.forEach { (cell) in + guard let cell = cell as? BaseChatCell, let model = cell.model else { + return + } + if models.contains(where: { (mod) -> Bool in + if mod.id != nil { + return mod.id == model.id + } + + if mod.text != nil { + return mod.text == model.text + } + + return false + }) { + if let indexPath = collectionView.indexPath(for: cell) { + indexPaths.append(indexPath) } } } - tableView.reloadRows(at: indexPaths, with: .none) - } - adjustTableTopInset() + collectionView.reloadItems(at: indexPaths) + }, completion: nil) } - private func markMesssagesAsRead(from index: Int, cells: [BaseChatCellModel]) { + private func markMesssagesAsRead(from index: Int) { var updatedCells: [BaseChatCellModel] = [] - for i in (0...index).reversed() where cells[i].isOwner { - let model = cells[i] - if model.deliveryStatus == .read { + + for i in (0...index).reversed() { + let i = messageDS.presentationIndex(from: i) + guard case let model = messageDS.cellModel(at: i), model.isOwner else { + continue + } + + guard model.deliveryStatus != .read else { break - } else { - model.deliveryStatus = .read - updatedCells.append(model) } - reloadIfVisible(models: updatedCells) + + model.deliveryStatus = .read + updatedCells.append(model) } + reloadIfVisible(models: updatedCells) } func scrollToBottomIfNeeded() { - guard let model = messageDS.cells.last, bottomGap < messageTVDelegate.height(for: model) else { return } - dispatchAsyncMainAfter(0.01, block: { + guard let model = messageDS.bottomModel, bottomGap < messageTVDelegate.height(for: model) else { + return + } + dispatchAsyncMainAfter(0.01) { self.scrollToBottom(animated: true) - }) + } } func scrollToBottom() { @@ -1420,58 +1441,56 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func scrollToBottom(animated: Bool) { - let indexPath = IndexPath(row: messageDS.cells.count - 1, section: 0) - scroll(to: indexPath, at: .bottom, animated: animated) + guard case let index = messageDS.bottomIndex, messageDS.indices.contains(index) else { + return + } + let indexPath = IndexPath(row: index, section: 0) + scroll(to: indexPath, at: .top, animated: animated) + } + + func scrollToTop() { + guard case let index = messageDS.topIndex, messageDS.indices.contains(index) else { + return + } + let indexPath = IndexPath(row: index, section: 0) + scroll(to: indexPath, at: .bottom, animated: true) } - private func scroll(to indexPath: IndexPath, at position: UITableViewScrollPosition, animated: Bool) { + private func scroll(to indexPath: IndexPath, at position: UICollectionViewScrollPosition, animated: Bool) { guard !contextMenuPresented, canScroll else { return } - tableView.scrollToRow(at: indexPath, at: position, animated: animated) + collectionView.scrollToItem(at: indexPath, at: position, animated: animated) } private var canScroll: Bool { - return canScroll(with: tableView.bounds) + return canScroll(with: collectionView.bounds) } private func canScroll(with bounds: CGRect) -> Bool { - return tableView.contentSize.height > bounds.height + return collectionView.contentSize.height > bounds.height } private var checkpoint: ChatCheckpoint? { - let visibleCells = tableView.visibleCells - guard let index = visibleCells.index(where: { $0 is BaseChatCell }), - let cell = visibleCells[index] as? BaseChatCell, - let model = cell.model, let localId = model.id else { - return nil + let visibleCells = collectionView.visibleCells + let bottomCell = visibleCells + .sorted(by: { $0.frame.minY < $1.frame.minY }) + .first(where: { $0 is BaseChatCell }) + + guard let cell = bottomCell as? BaseChatCell, let model = cell.model, let localId = model.id else { + return nil } - let rect = tableView.convert(cell.frame, to: tableView.superview) - let offset = tableView.frame.origin.y - rect.origin.y + let rect = collectionView.convert(cell.frame, to: collectionView.superview) + let offset = collectionView.frame.maxY - rect.maxY return ChatCheckpoint(localId: localId, topOffset: Double(offset)) } - private var localIdOfFirstVisibleMessage: String? { - var messageCell: BaseChatCell? - - for cell in tableView.visibleCells { - if let chatCell = cell as? BaseChatCell { - messageCell = chatCell - break - } - } - - if let cell = messageCell, let model = cell.model { - return model.id - } - - return nil - } - private var bottomGap: CGFloat { - return tableView.contentSize.height - (tableView.contentOffset.y + tableView.bounds.height) + return messageDS.isReversed + ? collectionView.contentOffset.y + : collectionView.contentSize.height - (collectionView.contentOffset.y + collectionView.bounds.height) } private var isLastMessageVisible: Bool { @@ -1499,22 +1518,18 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func removeMessage(_ messageId: String) { - guard let messageDS = self.messageDS, let index = messageDS.cells.index(where: { $0.id == "\(messageId)" }) else { + guard let messageDS = self.messageDS, let index = messageDS.index(where: { $0.id == messageId }) else { return } - tableView.performUpdates { - messageDS.cells.remove(at: index) - tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .none) - } - adjustTableTopInset() + collectionView.performBatchUpdates({ + messageDS.remove(at: index) + collectionView.deleteItems(at: [IndexPath(row: index, section: 0)]) + }, completion: nil) } func getNextPage() { - for i in 0.. CallInfoView { + let callInfoView = CallInfoView() + + view.addSubview(callInfoView) + callInfoView.snp.makeConstraints { maker in + maker.top.equalTo(collectionView.snp.top) + maker.left.right.equalToSuperview() + maker.height.equalTo(CallInfoView.Constraints.baseSizes.height) } - } - - //MARK: - Rejoin bar - func rejoinRunningCall() { - presenter.rejoinRunningCall() + + return callInfoView } } -//MARK: - Translation +// MARK: - Translation extension MessageVC { func showTranslationPreview(_ selectedLang: SelectedLang, isAuto: Bool) { guard case .lang(let language) = selectedLang, !language.isNone else { @@ -1612,7 +1613,7 @@ extension MessageVC { make.height.equalTo(0) } - tableView.snp.remakeConstraints { make in + collectionView.snp.remakeConstraints { make in makeDefaultTableConstraint(make) make.bottom.equalTo(staticFooterView.snp.top) } @@ -1625,7 +1626,7 @@ extension MessageVC { staticFooterView.snp.updateConstraints { make in make.height.equalTo(gradientHeight) } - tableView.snp.remakeConstraints { make in + collectionView.snp.remakeConstraints { make in makeDefaultTableConstraint(make) make.bottom.equalTo(staticFooterView.snp.top).offset(-FooterViewLayout.collapsedHeight) } @@ -1647,7 +1648,7 @@ extension MessageVC { } } -//MARK: - UI configuration +// MARK: - Translation UI configuration private extension MessageVC { func makeFooterView(on view: UIView, bottom: UIView) -> CollapsedView { let footerView = CollapsedView() diff --git a/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift b/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift index f3e197a0d..420a2d787 100644 --- a/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift +++ b/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift @@ -16,9 +16,15 @@ class CallInfoView: UIView { weak var delegate: CallInfoViewDelegate? - var membersCount: Int = 0 + var membersCount: Int = 0 { + didSet { + participantsCount.text = text(for: membersCount) + } + } + // MARK: - Views + lazy var headsetImage: UIImageView = { let img = UIImageView() img.contentMode = .scaleAspectFill @@ -78,7 +84,7 @@ class CallInfoView: UIView { pc.backgroundColor = Constants.colors.sectionBackgroundColor.getColor() pc.textAlignment = .left pc.numberOfLines = 1 - pc.text = String(format: "call_info_banner_members".localized, self.membersCount) + pc.text = text(for: membersCount) self.labelsView.addSubview(pc) pc.snp.makeConstraints({ (make) in @@ -119,16 +125,22 @@ class CallInfoView: UIView { baseSetup() } + // MARK: - Setup + private func baseSetup() { durationLabel.isHidden = false self.backgroundColor = Constants.colors.sectionBackgroundColor.getColor() } - //MARK: actions + private func text(for membersCount: Int) -> String { + return String(format: "call_info_banner_members".localized, membersCount) + } - @objc func joinButtonPressed() { - + + // MARK: - Actions + + @objc private func joinButtonPressed() { self.delegate?.didPressButtonJoinIn(callInfoView: self) } } diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift new file mode 100644 index 000000000..46b415a86 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift @@ -0,0 +1,180 @@ +// +// MessageCollectionViewDataSource.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class MessageCollectionViewDataSource: NSObject { + + unowned var view: MessageVC + + private var cells = [BaseChatCellModel]() + + init(view: MessageVC) { + self.view = view + super.init() + } + + + // MARK: - Data + + let isReversed = true + + var isEmpty: Bool { + return cells.isEmpty + } + + var count: Int { + return cells.count + } + + var indices: CountableRange { + return 0.. Int { + return isReversed ? count : cells.count - count + } + + func index(where predicate: (BaseChatCellModel) -> Bool) -> Int? { + return isReversed ? cells.reversed().index(where: predicate) : cells.index(where: predicate) + } + + func first(where predicate: (BaseChatCellModel) -> Bool) -> BaseChatCellModel? { + return isReversed ? cells.reversed().first(where: predicate) : cells.first(where: predicate) + } + + func filter(where predicate: (BaseChatCellModel) -> Bool) -> [BaseChatCellModel] { + return isReversed ? cells.reversed().filter(predicate) : cells.filter(predicate) + } + + func cellModel(at indexPath: IndexPath) -> BaseChatCellModel { + return cellModel(at: indexPath.row) + } + + func cellModel(at index: Int) -> BaseChatCellModel { + return cells[dataIndex(from: index)] + } + + func cellModel(before index: Int, where predicate: (BaseChatCellModel) -> Bool) -> BaseChatCellModel? { + let index = dataIndex(from: index) + return cells[.. Int64? { + return cells.first { $0.serverID != nil }.flatMap { $0.serverID } + } + + private func dataIndex(from presentationIndex: Int) -> Int { + return cells.count - presentationIndex - 1 + } + + func presentationIndex(from dataIndex: Int) -> Int { + return cells.count - dataIndex - 1 + } +} + +// MARK: - UICollectionViewDataSource + +extension MessageCollectionViewDataSource: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let model = cellModel(at: indexPath) + + if model.unread != nil { + if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "unread", for: indexPath) as? UnreadCell { + cell.setup() + cell.accessibilityIdentifier = "chat_cell_unread_\(indexPath.section)" + return cell + } + } + + if model.messageType == .system { + if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "system", for: indexPath) as? SystemCell { + cell.setup(message: model.text!) + cell.accessibilityIdentifier = "system_cell_unread_\(indexPath.section)" + return cell + } + } else if model.time == nil { + var cell: BaseChatCell! + + if let identifier = MessageCellFactory.identifier(for: model) { + cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as? BaseChatCell + cell.accessibilityIdentifier = "message_cell_\(identifier)_\(indexPath.section)" + } + + if view.messageTVDelegate != nil { + cell.delegate = view + } + cell.setup(model: model) + return cell + } else { + if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "time", for: indexPath) as? TimeCell { + cell.setup(date: model.time!) + cell.accessibilityIdentifier = "time_cell_unread_\(indexPath.section)" + return cell + } + } + + return UICollectionViewCell() + } +} diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift new file mode 100644 index 000000000..10275391e --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift @@ -0,0 +1,149 @@ +// +// MessageCollectionViewDelegate.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +enum ScrollDirection { + case top + case bottom +} + +final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { + + unowned var view: MessageVC + + init(view: MessageVC) { + self.view = view + super.init() + } + + + // MARK: - Collection View + + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + setupProgress(cell: cell) + setupMentions(cell: cell, indexPath: indexPath) + } + + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + return + } + view.progressDictionary.removeValue(forKey: url) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: view.view.bounds.width, height: heightForItem(at: indexPath)) + } + + private func heightForItem(at indexPath: IndexPath) -> CGFloat { + let model = view.messageDS.cellModel(at: indexPath) + + if model.messageType == .system { + return 40 + } + + if let _ = model.type { + return height(for: model) + } + + if model.unread != nil { + return 30 + } + + if model.time == nil { + return height(for: model) + } else { + return 30 + } + } + + func height(for model: BaseChatCellModel) -> CGFloat { + return model.isOwner ? BaseChatCell.size(for: model).height : OponentChatCell.size(for: model).height + } + + + // MARK: - Scroll View + + private var scrollOffset: CGFloat = 0 + + var lastVisibleIndex: Int? + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let offset = view.messageDS.isReversed + ? scrollView.contentSize.height - scrollView.bounds.height - scrollView.contentOffset.y + : scrollView.contentOffset.y + + let absoluteTop: CGFloat = 0 + let absoluteBottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height + + let isTop = scrollOffset - offset > 0 && offset > absoluteTop + let isBottom = scrollOffset - offset < 0 && offset < absoluteBottom + + if isTop { + view.isScrollingTo(.top) + } else if isBottom { + view.isScrollingTo(.bottom) + } + + if offset >= absoluteBottom { + view.didReachBottom() + } + + let contentHeight = scrollView.contentSize.height + + if offset < 0 && scrollView.frame.size.height < contentHeight { + loadData() + } + scrollOffset = offset + } + + func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + view.scrollToTop() + return false + } + + + // MARK: - Private + + private func setupMentions(cell: UICollectionViewCell, indexPath: IndexPath) { + func setup() { + lastVisibleIndex = indexPath.row + view.setupUnreadMentions(after: indexPath.row) + } + guard let index = lastVisibleIndex ?? view.lastVisibleCellIndex() else { + setup() + return + } + guard view.messageDS.isReversed ? indexPath.row < index : indexPath.row > index else { + return + } + setup() + } + + private func setupProgress(cell: UICollectionViewCell) { + guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + return + } + let block: (ProgressModel) -> Void = { model in + cell.updateProgressClosure(model: model) + } + if view.progressDictionary[url] == nil { + view.progressDictionary[url] = [block] + } else { + view.progressDictionary[url]?.append(block) + } + } + + private func loadData() { + if !view.loadingStatus { + view.loadingStatus = true + view.getNextPage() + } + } +} diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift new file mode 100644 index 000000000..4072e4ca6 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift @@ -0,0 +1,94 @@ +// +// MessageCollectionViewLayout.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class MessageCollectionViewLayout: UICollectionViewFlowLayout { + + private var offset: CGFloat = 0 + + override init() { + super.init() + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + private func setup() {} + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if let currentBounds = collectionView?.bounds { + return currentBounds.width != newBounds.width + } else { + return false + } + } + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + guard let attributes = super.layoutAttributesForElements(in: rect) else { + return nil + } + for attr in attributes { + attr.transform = CGAffineTransform(rotationAngle: .pi) + } + return attributes + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + let attributes = super.layoutAttributesForItem(at: indexPath) + attributes?.transform = CGAffineTransform(rotationAngle: .pi) + return attributes + } + + override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { + guard let collectionView = collectionView else { return } + + offset = 0 + + super.prepare(forCollectionViewUpdates: updateItems) + + let currentOffset = collectionView.contentOffset.y + collectionView.contentInset.top + + for item in updateItems { + switch item.updateAction { + case .insert: + guard let path = item.indexPathAfterUpdate, let attributes = layoutAttributesForItem(at: path) else { + continue + } + + guard attributes.frame.maxY <= currentOffset else { continue } + + offset += attributes.size.height + minimumLineSpacing + case .delete: + guard let path = item.indexPathBeforeUpdate, let attributes = layoutAttributesForItem(at: path) else { + continue + } + + guard attributes.frame.maxY <= currentOffset else { continue } + + offset -= (attributes.size.height + minimumLineSpacing) + default: + break + } + } + } + + override func finalizeCollectionViewUpdates() { + super.finalizeCollectionViewUpdates() + + guard let collectionView = collectionView else { return } + + guard collectionView.contentSize.height + offset >= collectionView.frame.height - collectionView.contentInset.bottom else { + return + } + collectionView.contentOffset.y += offset + } +} diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift index 7fd838eba..e1314166c 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift @@ -9,7 +9,7 @@ import Foundation import SnapKit -class BaseChatCell: UITableViewCell, MessageBaseImageViewDataSource, MessageContentAppearance, MessageVoiceViewDelegate, ReplyCounterDelegate, MessageRepliedViewDelegate, MessageTextViewDelegate, MessageForwardViewDelegate, MessageStickerRepliedViewDelegate, MessageConvertionViewDelegate { +class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, MessageContentAppearance, MessageVoiceViewDelegate, ReplyCounterDelegate, MessageRepliedViewDelegate, MessageTextViewDelegate, MessageForwardViewDelegate, MessageStickerRepliedViewDelegate, MessageConvertionViewDelegate { private static let senderFont = UIFont(fontName: Constants.fonts.medium, height: CGFloat(22.0.adjustedByWidth)) private static let translatingFont = UIFont(name: Constants.fonts.regular, size: 12) @@ -233,9 +233,11 @@ class BaseChatCell: UITableViewCell, MessageBaseImageViewDataSource, MessageCont model?.resetHandlers() } + // MARK: - Init - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) + + override init(frame: CGRect) { + super.init(frame: frame) initialSetup() } @@ -246,7 +248,6 @@ class BaseChatCell: UITableViewCell, MessageBaseImageViewDataSource, MessageCont // MARK: - Setup func initialSetup() { - selectionStyle = .none backgroundColor = .clear setupRecognizers() } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift index 57e3daf63..b0b0ec9c2 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -final class SystemCell: UITableViewCell { +final class SystemCell: UICollectionViewCell { lazy var systemLabel: UILabel = { let label = UILabel(height: Constraints.Label.height, @@ -31,11 +31,9 @@ final class SystemCell: UITableViewCell { // MARK: - Setup func setup(message: String) { - self.selectionStyle = .none self.backgroundColor = UIColor.clear systemLabel.text = message } - } @@ -49,5 +47,4 @@ extension SystemCell { static let horizontalInset = 16.0.adjustedByWidth } } - } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift index 640d4bdb0..19cc21e05 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift @@ -8,7 +8,7 @@ import Foundation -class TimeCell: UITableViewCell { +final class TimeCell: UICollectionViewCell { lazy var timeStamp: UILabel = { let v = UILabel() @@ -22,7 +22,6 @@ class TimeCell: UITableViewCell { }() func setup(date: Date) { - self.selectionStyle = .none self.backgroundColor = UIColor.clear timeStamp.text = self.getText(fromDate: date).uppercased() } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift index ad379285e..c048a7cb4 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift @@ -6,7 +6,9 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -final class UnreadCell: UITableViewCell { +import Foundation + +final class UnreadCell: UICollectionViewCell { lazy var timeStamp: UILabel = { let v = UILabel() @@ -56,8 +58,7 @@ final class UnreadCell: UITableViewCell { }() func setup() { - self.selectionStyle = .none - self.backgroundColor = UIColor.clear + backgroundColor = UIColor.clear timeStamp.isHidden = false line1.isHidden = false line2.isHidden = false diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift b/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift deleted file mode 100644 index f4e24b3a4..000000000 --- a/Nynja/Modules/Message/View/Views/TableView/MessageDS.swift +++ /dev/null @@ -1,71 +0,0 @@ -// MessageDS.swift -// Nynja -// -// Created by Anton Makarov on 27.08.2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import Foundation - -class MessageDS: NSObject, UITableViewDataSource { - - weak var view: MessageVC! - - var cells = [BaseChatCellModel]() - - var unreadCellIndex: Int? { - return cells.index(where: { $0.unread == true }) - } - - // MARK: Init - init(view: MessageVC) { - self.view = view - } - - // MARK: UITableViewDataSource - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return cells.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = cells[indexPath.row] - - if model.unread != nil { - if let cell = tableView.dequeueReusableCell(withIdentifier: "unread", for: indexPath) as? UnreadCell { - cell.setup() - cell.accessibilityIdentifier = "chat_cell_unread_\(indexPath.section)" - return cell - } - } - - if model.messageType == .system { - if let cell = tableView.dequeueReusableCell(withIdentifier: "system", for: indexPath) as? SystemCell { - cell.setup(message: model.text!) - cell.accessibilityIdentifier = "system_cell_unread_\(indexPath.section)" - return cell - } - } else if model.time == nil { - var cell: BaseChatCell! - - if let identifier = MessageCellFactory.identifier(for: model) { - cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? BaseChatCell - cell.accessibilityIdentifier = "message_cell_\(identifier)_\(indexPath.section)" - } - - if view.messageTVDelegate != nil { - cell.delegate = view - } - cell.setup(model: model) - return cell - } else { - if let cell = tableView.dequeueReusableCell(withIdentifier: "time", for: indexPath) as? TimeCell { - cell.setup(date: model.time!) - cell.accessibilityIdentifier = "time_cell_unread_\(indexPath.section)" - - return cell - } - } - - return UITableViewCell() - } -} diff --git a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift b/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift deleted file mode 100644 index cad450d33..000000000 --- a/Nynja/Modules/Message/View/Views/TableView/MessageTableViewDelegate.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// MessageTableViewDelegate.swift -// Nynja -// -// Created by Volodymyr Hryhoriev on 10/17/17. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -enum ScrollDirection { - case top - case bottom -} - -class MessageTableViewDelegate: NSObject, UITableViewDelegate, CallInfoViewDelegate { - - weak var view: MessageVC? - - var rejoinBannerDisplayed: Bool = false - var membersCount: Int = 0 - - // MARK: Init - init(view: MessageVC) { - self.view = view - } - - // MARK: UITableViewDelegate - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - - var callInfoView:CallInfoView? = nil - - if rejoinBannerDisplayed { - - callInfoView = CallInfoView() - - if let civ = callInfoView { - civ.membersCount = membersCount - civ.delegate = self - } - } - - return callInfoView - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return rejoinBannerDisplayed ? CallInfoView.Constraints.baseSizes.height : 0 - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard let model = view?.messageDS.cells[indexPath.row] else { return 0 } - - if model.messageType == .system { - return 40 - } - - if let _ = model.type { - return height(for: model) - } - - if model.unread != nil { - return 30 - } - - if model.time == nil { - return height(for: model) - } else { - return 30 - } - } - - func height(for model: BaseChatCellModel) -> CGFloat { - return model.isOwner ? BaseChatCell.size(for: model).height : OponentChatCell.size(for: model).height - } - - // MARK: UIScrollViewDelegate - var scrollOffset: CGFloat = 0 - var lastVisibleIndex: Int? - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let offset = scrollView.contentOffset.y - - let absoluteTop: CGFloat = 0 - let absoluteBottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height - - let isTop = scrollOffset - offset > 0 && offset > absoluteTop - let isBottom = scrollOffset - offset < 0 && offset < absoluteBottom - - if isTop { - view?.isScrollingTo(.top) - } else if isBottom { - view?.isScrollingTo(.bottom) - } - - if offset >= absoluteBottom { - view?.didReachBottom() - } - - let contentHeight = scrollView.contentSize.height - - if offset < 0 && scrollView.frame.size.height < contentHeight { - loadData() - } - scrollOffset = offset - } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - setupProgress(cell: cell) - setupMentions(cell: cell, indexPath: indexPath) - } - - func setupMentions(cell: UITableViewCell, indexPath: IndexPath) { - guard let index = lastVisibleIndex ?? view?.lastVisibleCellIndex() else { - lastVisibleIndex = indexPath.row - view?.setupUnreadMentions(after: indexPath.row) - return - } - guard indexPath.row > index else { - return - } - lastVisibleIndex = indexPath.row - view?.setupUnreadMentions(after: indexPath.row) - } - - func setupProgress(cell: UITableViewCell) { - guard let bcc = cell as? BaseChatCell else { return } - guard let url = bcc.model?.progressModel?.url.absoluteString else { return } - let block : (ProgressModel)-> Void = { model in - (cell as? BaseChatCell)?.updateProgressClosure(model: model) - } - if view?.progressDictionary[url] == nil { - view?.progressDictionary[url] = [block] - } else { - view?.progressDictionary[url]?.append(block) - } - } - - func loadData() { - guard let v = self.view else { return } - if !v.loadingStatus{ - v.loadingStatus = true - v.getNextPage() - } - } - - func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let bcc = cell as? BaseChatCell else { return } - guard let url = bcc.model?.fileUrl?.absoluteString else { return } - view?.progressDictionary.removeValue(forKey: url) - } - - // MARK: CallInfoVIewDelegate - func didPressButtonJoinIn(callInfoView: CallInfoView) { - self.view?.rejoinRunningCall() - } -} diff --git a/Nynja/Modules/Replies/View/RepliesCollectionViewDelegate.swift b/Nynja/Modules/Replies/View/RepliesCollectionViewDelegate.swift new file mode 100644 index 000000000..fd89cff62 --- /dev/null +++ b/Nynja/Modules/Replies/View/RepliesCollectionViewDelegate.swift @@ -0,0 +1,46 @@ +// +// RepliesCollectionViewDelegate.swift +// Nynja +// +// Created by Andrey Reznik on 09.01.18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class RepliesCollectionViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { + + unowned var collectionView: UICollectionView + weak var dataSource: RepliesDS! + + + // MARK: - Init + + init(collectionView: UICollectionView, dataSource: RepliesDS) { + self.collectionView = collectionView + self.dataSource = dataSource + } + + + // MARK: - UICollectionViewDelegate + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: collectionView.bounds.width, height: heightForItem(at: indexPath)) + } + + private func heightForItem(at indexPath: IndexPath) -> CGFloat { + let model = dataSource.cells[indexPath.item] + + if model.type != nil { + return height(for: model) + } else if model.unread != nil { + return 30 + } else if model.time == nil { + return height(for: model) + } else { + return 30 + } + } + + private func height(for model: BaseChatCellModel) -> CGFloat { + return model.isOwner ? BaseChatCell.size(for: model).height : OponentChatCell.size(for: model).height + } +} diff --git a/Nynja/Modules/Replies/View/RepliesDS.swift b/Nynja/Modules/Replies/View/RepliesDS.swift index 28ccc1d02..cd65ab844 100644 --- a/Nynja/Modules/Replies/View/RepliesDS.swift +++ b/Nynja/Modules/Replies/View/RepliesDS.swift @@ -6,43 +6,47 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class RepliesDS: NSObject, UITableViewDataSource { +final class RepliesDS: NSObject, UICollectionViewDataSource { weak var view: RepliesVC! var cells = [BaseChatCellModel]() - // MARK: Init + + // MARK: - Init + init(view: RepliesVC) { self.view = view } - // MARK: UITableViewDataSource - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + + // MARK: - UITableViewDataSource + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return cells.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let model = cells[indexPath.row] if model.time == nil { var cell: BaseChatCell! if let identifier = MessageCellFactory.identifier(for: model) { - cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? BaseChatCell + cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as? BaseChatCell } - if view.tableViewDelegate != nil { + if view.collectionViewDelegate != nil { cell.delegate = view } - + cell.setup(model: model) -// view.cellDisplayed(at: indexPath) + // view.cellDisplayed(at: indexPath) return cell } - return UITableViewCell() + return UICollectionViewCell() } } diff --git a/Nynja/Modules/Replies/View/RepliesTableViewDelegate.swift b/Nynja/Modules/Replies/View/RepliesTableViewDelegate.swift deleted file mode 100644 index a7d4b8334..000000000 --- a/Nynja/Modules/Replies/View/RepliesTableViewDelegate.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// RepliesTableViewDelegate.swift -// Nynja -// -// Created by Andrey Reznik on 09.01.18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -class RepliesTableViewDelegate: NSObject, UITableViewDelegate { - - weak var dataSource: RepliesDS! - - // MARK: Init - init(dataSource: RepliesDS) { - self.dataSource = dataSource - } - - // MARK: UITableViewDelegate - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = dataSource.cells[indexPath.row] - - if let _ = model.type { - return height(for: model) - } - - if model.unread != nil { - return 30 - } - - if model.time == nil { - return height(for: model) - } else { - return 30 - } - } - - private func height(for model: BaseChatCellModel) -> CGFloat { - return model.isOwner ? BaseChatCell.size(for: model).height : OponentChatCell.size(for: model).height - } -} diff --git a/Nynja/Modules/Replies/View/RepliesVC.swift b/Nynja/Modules/Replies/View/RepliesVC.swift index 1a660f683..00f85889a 100644 --- a/Nynja/Modules/Replies/View/RepliesVC.swift +++ b/Nynja/Modules/Replies/View/RepliesVC.swift @@ -11,7 +11,7 @@ import CoreLocation.CLLocation import Photos -class RepliesVC: BaseVC, RepliesViewProtocol { +final class RepliesVC: BaseVC, RepliesViewProtocol { var presenter: RepliesPresenterProtocol! { didSet { @@ -19,12 +19,13 @@ class RepliesVC: BaseVC, RepliesViewProtocol { } } - var tableViewDataSource: RepliesDS! - var tableViewDelegate: RepliesTableViewDelegate! + var collectionViewDataSource: RepliesDS! + var collectionViewDelegate: RepliesCollectionViewDelegate! + //MARK: - Subviews - lazy var repliesNumberLabel: UILabel = { + private lazy var repliesNumberLabel: UILabel = { let label = UILabel(height: Constraints.repliesNumberLabel.height, color: Constants.colors.white.getColor(), fontName: Constants.fonts.regular, @@ -42,30 +43,29 @@ class RepliesVC: BaseVC, RepliesViewProtocol { return label }() - lazy var tableView: UITableView = { - let tv = UITableView() - - tv.separatorStyle = .none - tv.backgroundColor = UIColor.clear - tv.contentInset = UIEdgeInsetsMake(Constraints.tableView.topInset, 0, 0, 0) - tv.contentOffset = CGPoint(x: 0, y: -Constraints.tableView.topInset) - - registerCells(for: tv) + private lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + collectionView.backgroundColor = .clear + collectionView.alwaysBounceVertical = true - tv.estimatedRowHeight = 0 // NOTE: It is important for scrolling behavior. + collectionView.contentInset = UIEdgeInsetsMake(Constraints.tableView.topInset, 0, 0, 0) + collectionView.contentOffset = CGPoint(x: 0, y: -Constraints.tableView.topInset) let bottomPadding = UIScreen.main.bounds.height * 0.11 - self.view.addSubview(tv) - tv.snp.makeConstraints({ (make) in - make.left.right.equalTo(self.view) + view.addSubview(collectionView) + collectionView.snp.makeConstraints { (make) in + make.left.right.equalToSuperview() make.top.equalTo(self.navigationView.snp.bottom) adjustVerticalInset(.bottom, make: make, offset: -bottomPadding) - }) - return tv + } + + registerCells(for: collectionView) + + return collectionView }() - lazy var closeButton: NynjaCloseButton = { + private lazy var closeButton: NynjaCloseButton = { let button = NynjaCloseButton() button.contentMode = .scaleToFill @@ -95,11 +95,12 @@ class RepliesVC: BaseVC, RepliesViewProtocol { closeButton.isHidden = false - tableViewDataSource = RepliesDS(view: self) - tableViewDelegate = RepliesTableViewDelegate(dataSource: tableViewDataSource) + collectionViewDataSource = RepliesDS(view: self) + collectionViewDelegate = RepliesCollectionViewDelegate(collectionView: collectionView, + dataSource: collectionViewDataSource) - tableView.dataSource = tableViewDataSource - tableView.delegate = tableViewDelegate + collectionView.dataSource = collectionViewDataSource + collectionView.delegate = collectionViewDelegate } //MARK: - RepliesViewProtocol @@ -109,26 +110,27 @@ class RepliesVC: BaseVC, RepliesViewProtocol { } func updateTableViewDataSource(_ cells: [BaseChatCellModel]) { - tableViewDataSource.cells = cells - tableView.reloadData() - tableView.layoutIfNeeded() + collectionViewDataSource.cells = cells + collectionView.reloadData() + collectionView.layoutIfNeeded() } func removeReplies(_ ids: [String]) { - let filtred = self.tableViewDataSource.cells.filter { model -> Bool in + let filtred = self.collectionViewDataSource.cells.filter { model -> Bool in ids.contains(where: { $0 == model.id }) } let indexes = filtred.map { model -> Int in - return self.tableViewDataSource.cells.index(where: { $0.id == model.id })! + return self.collectionViewDataSource.cells.index(where: { $0.id == model.id })! } guard !indexes.isEmpty else { return } - indexes.forEach { self.tableViewDataSource.cells.remove(at: $0) } - tableView.beginUpdates() - tableView.deleteRows(at: indexes.map { IndexPath(row: $0, section: 0) }, with: .none) - tableView.endUpdates() + indexes.forEach { self.collectionViewDataSource.cells.remove(at: $0) } + + collectionView.performBatchUpdates({ + collectionView.deleteItems(at: indexes.map { IndexPath(row: $0, section: 0) }) + }, completion: nil) } func insertReplies(_ cells: [BaseChatCellModel]) { @@ -137,7 +139,7 @@ class RepliesVC: BaseVC, RepliesViewProtocol { func updateProgress(progressModel: ProgressModel?) { let url = progressModel?.url - let list = tableViewDataSource.cells.filter({ (model) -> Bool in + let list = collectionViewDataSource.cells.filter({ (model) -> Bool in model.progressModel?.url == url }) list.forEach { (model) in @@ -156,17 +158,17 @@ class RepliesVC: BaseVC, RepliesViewProtocol { //MARK: - Private methods - private func registerCells(for tableView: UITableView) { + private func registerCells(for collectionView: UICollectionView) { MessageCellFactory.selfIdentifiers.forEach { - tableView.register(BaseChatCell.self, forCellReuseIdentifier: $0) + collectionView.register(BaseChatCell.self, forCellWithReuseIdentifier: $0) } MessageCellFactory.opponenetIdentifiers.forEach { - tableView.register(OponentChatCell.self, forCellReuseIdentifier: $0) + collectionView.register(OponentChatCell.self, forCellWithReuseIdentifier: $0) } } func reloadIfVisible(models: [BaseChatCellModel]) { - tableView.visibleCells.forEach { (cell) in + collectionView.visibleCells.forEach { (cell) in if let model = (cell as? BaseChatCell)?.model { if let index = models.index(where: { (mod) -> Bool in if mod.id != nil { @@ -186,14 +188,16 @@ class RepliesVC: BaseVC, RepliesViewProtocol { } } +// MARK: - Layout + extension RepliesVC { - struct Constraints { - struct tableView { + enum Constraints { + enum tableView { static let topInset = CGFloat(8.0.adjustedByWidth) } - struct closeButton { + enum closeButton { static let height: CGFloat = 44.0 static let width: CGFloat = 80.0 @@ -201,7 +205,7 @@ extension RepliesVC { static let bottomInset: CGFloat = 26.0 } - struct repliesNumberLabel { + enum repliesNumberLabel { static let height = CGFloat(17.adjustedByWidth) static let rightInset = 16.adjustedByWidth static let leftOffset = 8.adjustedByWidth -- GitLab From 9d0cab46a905fb6b94b997ed9c60281ac36d86dd Mon Sep 17 00:00:00 2001 From: reznik94 Date: Thu, 9 Aug 2018 23:59:11 +0300 Subject: [PATCH 10/32] [NY-2427] Keyboard closes if user taps on microphone icon - fixed (#1064) --- Nynja.xcodeproj/project.pbxproj | 4 + .../Extensions/InputBar+ContentType.swift | 9 ++ .../UI/TextInput/InputBar/InputBar.swift | 82 +++++++++++++------ .../InputContent/RecordContainer.swift | 20 +++++ 4 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 Nynja/Library/UI/TextInput/InputBar/InputContent/RecordContainer.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b4d8be21f..83c256125 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -158,6 +158,7 @@ 2632139120D797F500C31144 /* TranslationViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2632139020D797F500C31144 /* TranslationViewProtocol.swift */; }; 2633EF6E205212F700DB3868 /* MemberDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2633EF6D205212F700DB3868 /* MemberDAOProtocol.swift */; }; 2633EF702052130400DB3868 /* MemberDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2633EF6F2052130400DB3868 /* MemberDAO.swift */; }; + 263409342119CFE2002F8D8F /* RecordContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263409332119CFE2002F8D8F /* RecordContainer.swift */; }; 26342CA020ECAA0700D2196B /* TranscribeNetworkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342C9F20ECAA0700D2196B /* TranscribeNetworkRouter.swift */; }; 26342CA920ECBAEF00D2196B /* TranscribeNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CA820ECBAEE00D2196B /* TranscribeNetworkClient.swift */; }; 26342CAB20ECBB0100D2196B /* TranscribeNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CAA20ECBB0100D2196B /* TranscribeNetworkService.swift */; }; @@ -2258,6 +2259,7 @@ 2632139220D7B71200C31144 /* TranslateConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TranslateConfig.xcconfig; sourceTree = ""; }; 2633EF6D205212F700DB3868 /* MemberDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDAOProtocol.swift; sourceTree = ""; }; 2633EF6F2052130400DB3868 /* MemberDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDAO.swift; sourceTree = ""; }; + 263409332119CFE2002F8D8F /* RecordContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordContainer.swift; sourceTree = ""; }; 26342C9F20ECAA0700D2196B /* TranscribeNetworkRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeNetworkRouter.swift; sourceTree = ""; }; 26342CA820ECBAEE00D2196B /* TranscribeNetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeNetworkClient.swift; sourceTree = ""; }; 26342CAA20ECBB0100D2196B /* TranscribeNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeNetworkService.swift; sourceTree = ""; }; @@ -8984,6 +8986,7 @@ 8580BAE020BD99D200239D9D /* InputContent.swift */, A43B257520AB1DFA00FF8107 /* InputContentProtocol.swift */, A43B257820AB1DFA00FF8107 /* TextInputContent.swift */, + 263409332119CFE2002F8D8F /* RecordContainer.swift */, A43B257620AB1DFA00FF8107 /* RecordDisplayInputContent.swift */, A43B257720AB1DFA00FF8107 /* RecordingInputContent.swift */, A458FABA20EB87BF0075D55E /* ActionContainerContent.swift */, @@ -13959,6 +13962,7 @@ A45F112120B4218D00F45004 /* InfoDateView.swift in Sources */, 9BD8E3F820EF8874001384EC /* CallInProgressNavigationController.swift in Sources */, A4688DFA20650FF50013660D /* DBObserver.swift in Sources */, + 263409342119CFE2002F8D8F /* RecordContainer.swift in Sources */, 853FB0682049B193000996C5 /* SupportProtocols.swift in Sources */, A42D51B1206A361400EEB952 /* Typing.swift in Sources */, A42D52D3206A53AB00EEB952 /* Test_Spec.swift in Sources */, diff --git a/Nynja/Library/UI/TextInput/InputBar/Extensions/InputBar+ContentType.swift b/Nynja/Library/UI/TextInput/InputBar/Extensions/InputBar+ContentType.swift index cf2101d03..bf773eafc 100644 --- a/Nynja/Library/UI/TextInput/InputBar/Extensions/InputBar+ContentType.swift +++ b/Nynja/Library/UI/TextInput/InputBar/Extensions/InputBar+ContentType.swift @@ -41,6 +41,15 @@ extension InputBar { static func ==(lhs: ContentType, rhs: ContentType) -> Bool { return lhs.hashValue == rhs.hashValue } + + var shouldRemovePreviousContent: Bool { + switch self { + case .recording, .recordDisplay: + return false + default: + return true + } + } } } diff --git a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift index 825a03733..645d2669c 100644 --- a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift +++ b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift @@ -86,7 +86,7 @@ final class InputBar: UIView { private var contentType: ContentType = .text(nil) { didSet { if oldValue != contentType { - setupContent(contentType) + setupContent(contentType, oldType: oldValue) setupTestingKeys() } else { updateContent(contentType) @@ -135,6 +135,8 @@ final class InputBar: UIView { return view }() + private var recordingContainer: RecordContainer? + private lazy var rightButton: ScheduleButton = { let width = Constraints.Main.RightButton.width let height = Constraints.Main.RightButton.height @@ -203,16 +205,39 @@ final class InputBar: UIView { backgroundColor = .clear setupButton(.voice) - setupContent(.text(nil)) + setupContent(.text(nil), oldType: .text(nil)) + setupTestingKeys() } // MARK: - Setup // MARK: - Content - private func setupContent(_ type: ContentType) { - inputContentContainer.subviews.removeFromSuperview() - + private func setupContent(_ type: ContentType, oldType: ContentType) { + //TODO: - Keep calm and don't read that shit + if type.shouldRemovePreviousContent { + if oldType.shouldRemovePreviousContent { + inputContentContainer.subviews.removeFromSuperview() + } else { + recordingContainer?.removeFromSuperview() + recordingContainer = nil + + if let textInput = inputContentContainer.subviews.first as? TextInputContent { + textInput.isHidden = false + textInput.delegate = self + textInput.textView.textViewDidChange() + inputContent = textInput + inputContent?.startInteraction() + return + } + } + } else { + if let textInput = inputContent as? TextInputContent { + textInput.isHidden = true + textInput.delegate = nil + } + } + guard let inputContentView = makeInputContentView(for: type) else { return } @@ -260,6 +285,28 @@ final class InputBar: UIView { return content } + private func makeRecordingContainer(_ view: InputContentView) -> RecordContainer { + let containerMaker: () -> RecordContainer = { + let container = RecordContainer() + container.content = view + return container + } + return recordingContainer ?? containerMaker() + } + + private func wrapRecordingContentToContainer(_ view: InputContentView) -> RecordContainer { + let container = makeRecordingContainer(view) + container.subviews.removeFromSuperview() + container.addSubview(view) + view.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + recordingContainer = container + + return container + } + private func makeRecordingContent() -> InputContentView? { guard let content = RecordingInputContent(recordingStatusHandler: { [weak self] status in self?.sendTypingHandler?(status) @@ -281,13 +328,7 @@ final class InputBar: UIView { return nil } - inputContentContainer.addSubview(content) - content.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - inputContent = content - - return content + return wrapRecordingContentToContainer(content) } private func makeRecordDisplayContent(with url: URL) -> InputContentView { @@ -299,7 +340,7 @@ final class InputBar: UIView { self?.buttonType = .voice } - return content + return wrapRecordingContentToContainer(content) } private func makeNynjaAction(with info: DisplayMode.ActionInfo) -> InputContentView { @@ -622,12 +663,10 @@ final class InputBar: UIView { rightButton.backgroundColor = Constants.colors.red.getColor() rightButton.isEnabled = true } - } - - - // MARK: - ALTextInputBarDelegate +} - extension InputBar: ALTextInputBarDelegate { +//MARK: - ALTextInputBarDelegate +extension InputBar: ALTextInputBarDelegate { func textViewDidChange(textView: ALTextView) { if case .edit(let text) = displayMode { @@ -704,8 +743,7 @@ final class InputBar: UIView { } -// MARK: - Layout - +//MARK: - Layout extension InputBar { enum Constraints { enum Main { @@ -732,8 +770,7 @@ extension InputBar { } -// MARK: - Testable - +//MARK: - Testable extension InputBar: TestableViewProtocol { private enum Keys: String { case sendButton = "input_bar_send_button" @@ -753,4 +790,3 @@ extension InputBar: TestableViewProtocol { actionButton?.accessibilityIdentifier = Keys.actionButton.rawValue } } - diff --git a/Nynja/Library/UI/TextInput/InputBar/InputContent/RecordContainer.swift b/Nynja/Library/UI/TextInput/InputBar/InputContent/RecordContainer.swift new file mode 100644 index 000000000..5e29e56cb --- /dev/null +++ b/Nynja/Library/UI/TextInput/InputBar/InputContent/RecordContainer.swift @@ -0,0 +1,20 @@ +// +// RecordContainer.swift +// Nynja +// +// Created by Andrey Reznik on 07.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class RecordContainer: UIView, InputContentProtocol { + var content: InputContentProtocol? + + func startInteraction() { + content?.startInteraction() + } + func cancelInteraction() { + content?.cancelInteraction() + } +} -- GitLab From efebbd4fce775709809bb48a93a480c16389778a Mon Sep 17 00:00:00 2001 From: reznik94 Date: Thu, 9 Aug 2018 23:59:39 +0300 Subject: [PATCH 12/32] [NY-1944] Incorrect updating of 'Group Participants' screen - fixed (#1067) --- .../Participants/Interactor/ParticipantsInteractor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift index 6b8f30a77..babd3b0c4 100644 --- a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift +++ b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift @@ -38,7 +38,7 @@ class ParticipantsInteractor: BaseInteractor, ParticipantsInteractorInputProtoco override func update(with changes: [StorageChange], type: SubscribeType) { if case .room = type { guard let dbRoom = changes.first?.entity as? DBRoom else { return } - guard let updatedMembers = Room(room: dbRoom).members else { return } + guard let updatedMembers = Room(room: dbRoom).allMembers else { return } self.includedMembers = updatedMembers self.loadContacts() } -- GitLab From 3d4f584f25efa678f085c8568044c442d556b940 Mon Sep 17 00:00:00 2001 From: reznik94 Date: Thu, 9 Aug 2018 23:59:51 +0300 Subject: [PATCH 13/32] [NY-2478] Incorrect alert on the 'By Contacts' screen when permission to contacts is denied - fixed (#1068) --- Nynja/Modules/Main/WireFrame/MainWireframe.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 6ae0efd79..9b919e521 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -73,7 +73,12 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato } func showContacts() { - ContactsWireFrame().presentContacts(navigation: contentNavigation, mainWireFrame: self) + PermissionManager().requestContactsPermission { [weak self] status in + guard let `self` = self, status == .authorized else { + return + } + ContactsWireFrame().presentContacts(navigation: self.contentNavigation, mainWireFrame: self) + } } func showCreateConferenceCall() { -- GitLab From 3027a03c2fc36b15c0bc271088c02345af62993e Mon Sep 17 00:00:00 2001 From: reznik94 Date: Fri, 10 Aug 2018 00:00:08 +0300 Subject: [PATCH 14/32] [NY-1210] Can't create Group after app goes to background - fixed (#1069) --- .../Interactor/NewChannelInteractor.swift | 31 +++++++--- .../NewChannel/NewChannelProtocols.swift | 2 + .../Presenter/NewChannelPresenter.swift | 8 +++ .../CreateGroup/CreateGroupProtocols.swift | 2 + .../Interactor/CreateGroupInteractor.swift | 62 ++++++++++++++----- .../Presenter/CreateGroupPresenter.swift | 16 ++++- Nynja/Services/MQTT/MQTTService.swift | 3 +- 7 files changed, 98 insertions(+), 26 deletions(-) diff --git a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift index 55f7c177e..a0d5d7a88 100644 --- a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift +++ b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift @@ -18,6 +18,7 @@ final class NewChannelInteractor: BaseInteractor, NewChannelInteractorInputProto weak var presenter: NewChannelInteractorOutputProtocol! + private var createChannelAction: (() -> Void)? // MARK: - Dependencies @@ -105,7 +106,12 @@ final class NewChannelInteractor: BaseInteractor, NewChannelInteractorInputProto private func sendCreateChannel(avatarUrl: URL?, info: CreateChannelInfo) { let room = makeRoom(with: avatarUrl, info: info) - mqttService.createRoom(room, kind: .channel) + if mqttService.isConnectedSuccess { + mqttService.createRoom(room, kind: .channel) + } + createChannelAction = { [weak self] in + self?.mqttService.createRoom(room, kind: .channel) + } } private func makeRoom(with avatarUrl: URL?, info: CreateChannelInfo) -> Room { @@ -139,15 +145,16 @@ final class NewChannelInteractor: BaseInteractor, NewChannelInteractorInputProto // MARK: - StorageSubscriber override func update(with changes: [StorageChange], type: SubscribeType) { - if case .room = type, + guard case .room = type, let change = changes.first, change.kind == .insert, let dbRoom = change.entity as? DBRoom, - dbRoom.id == localId { - - let room = Room(room: dbRoom) - presenter?.channelCreated(room) + dbRoom.id == localId else { + return } + createChannelAction = nil + let room = Room(room: dbRoom) + presenter?.channelCreated(room) } } @@ -173,7 +180,6 @@ extension NewChannelInteractor { private func isAbleToHandleLink(_ link: Link) -> Bool { return link.room_id == localId } - } @@ -182,9 +188,18 @@ extension NewChannelInteractor { extension NewChannelInteractor { func didConnect(_ mqttService: MQTTService) { - presenter.didConnectToServer() + presenter?.didConnectToServer() + + guard let action = createChannelAction else { + return + } + presenter?.showHUD() + action() } + func didDisconnect(_ mqttService: MQTTService) { + presenter?.hideHUD() + } } diff --git a/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift b/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift index f722ad10d..81fe46802 100644 --- a/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift +++ b/Nynja/Modules/Channel/NewChannel/NewChannelProtocols.swift @@ -82,6 +82,8 @@ protocol NewChannelInteractorOutputProtocol: class { func checkedLinkIsAvailable(_ link: String) func somethingWrongWithLink(_ link: String, code: StatusCode) + func showHUD() + func hideHUD() func didConnectToServer() } diff --git a/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift b/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift index 4b5bfe6aa..04dc2f264 100644 --- a/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift +++ b/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift @@ -232,6 +232,14 @@ final class NewChannelPresenter: BasePresenter, NewChannelPresenterProtocol, New } } + func showHUD() { + view.shouldShowSpinner = true + } + + func hideHUD() { + view.shouldShowSpinner = false + } + func didConnectToServer() { if shouldShowGeneratedLink { interactor.generateLink() diff --git a/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift b/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift index 7694d0428..0ce0d54b6 100644 --- a/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift +++ b/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift @@ -59,6 +59,8 @@ protocol CreateGroupInteractorOutputProtocol: class { /** * Add here your methods for communication INTERACTOR -> PRESENTER */ + func showHUD() + func hideHUD() func created(room: Room) } diff --git a/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift b/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift index 4631716e7..ef479e969 100644 --- a/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift +++ b/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift @@ -17,25 +17,44 @@ class CreateGroupInteractor: BaseInteractor, CreateGroupInteractorInputProtocol return ContactDAO.currentContact } + private let mqttService = MQTTService.sharedInstance + private var avatarUrl: URL? private let localId = IdBuilder(format: .defaultId).build() + private var sendRoomAction: (() -> Void)? + + override init() { + super.init() + mqttService.addSubscriber(self) + } + + deinit { + mqttService.removeSubscriber(self) + } func createRoom(name: String, avatar: UIImage?, members: [Member]) { - if let url = self.avatarUrl { - let sync = SyncFileManager.sharedInstance - sync.downloader = AmazonManager.shared - SyncFileManager.sharedInstance.saveExternalFileLink(localUrl: url.path) { (ext, progress, request) in - if let extUrl = ext { - self.sendRoom(with: name, avatar: String(describing: extUrl), members: members) - } - } - } else { + guard let url = self.avatarUrl else { sendRoom(with: name, avatar: nil, members: members) + return + } + let sync = SyncFileManager.sharedInstance + sync.downloader = AmazonManager.shared + SyncFileManager.sharedInstance.saveExternalFileLink(localUrl: url.path) { (ext, progress, request) in + guard let extUrl = ext else { + return + } + self.sendRoom(with: name, avatar: String(describing: extUrl), members: members) } } func sendRoom(with name: String, avatar: String?, members: [Member]) { - MQTTService.sharedInstance.createRoom(id: localId, name: name, avatar: avatar, members: members) + if mqttService.isConnectedSuccess { + mqttService.createRoom(id: localId, name: name, avatar: avatar, members: members) + } + let roomId = localId + sendRoomAction = { [weak self] in + self?.mqttService.createRoom(id: roomId, name: name, avatar: avatar, members: members) + } } func avatarChanged(with url: URL) { @@ -44,11 +63,26 @@ class CreateGroupInteractor: BaseInteractor, CreateGroupInteractorInputProtocol // MARK: - StorageSubscriber override func update(with changes: [StorageChange], type: SubscribeType) { - if case .room = type, let dbRoom = changes.first?.entity as? DBRoom, dbRoom.id == localId { - let room = Room(room: dbRoom) - presenter?.created(room: room) + guard case .room = type, let dbRoom = changes.first?.entity as? DBRoom, dbRoom.id == localId else { + return } + sendRoomAction = nil + let room = Room(room: dbRoom) + presenter?.created(room: room) } - } +extension CreateGroupInteractor: MQTTServiceDelegate { + + func didConnect(_ mqttService: MQTTService) { + guard let action = sendRoomAction else { + return + } + presenter?.showHUD() + action() + } + + func didDisconnect(_ mqttService: MQTTService) { + presenter?.hideHUD() + } +} diff --git a/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift b/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift index 8fbc78735..2105d1f1c 100644 --- a/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift +++ b/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift @@ -55,11 +55,11 @@ class CreateGroupPresenter: BasePresenter, CreateGroupPresenterProtocol, CreateG return } if room.name != "" { - (wireFrame as? CreateGroupWireFrame)?.mainWireFrame?.view?.showSpinner() + showHUD() interactor.createRoom(name: room.name!, avatar: avatar, members: room.members!) disableAction = true } else { - (wireFrame as? CreateGroupWireFrame)?.mainWireFrame?.view?.hideSpinner() + hideHUD() AlertManager.sharedInstance.showAlertOk(message: "Group_Name_Empty_Message".localized) } } @@ -113,11 +113,21 @@ class CreateGroupPresenter: BasePresenter, CreateGroupPresenterProtocol, CreateG self.view.setup(room: room, avatar: avatar, myAlias: myAlias, badgeNumber: contacts.count) } + //MARK: CreateGroupInteractorOutputProtocol + func created(room: Room) { - (wireFrame as? CreateGroupWireFrame)?.mainWireFrame?.view?.hideSpinner() + hideHUD() wireFrame.showGroupChat(room: room) } + func showHUD() { + (wireFrame as? CreateGroupWireFrame)?.mainWireFrame?.view?.showSpinner() + } + + func hideHUD() { + (wireFrame as? CreateGroupWireFrame)?.mainWireFrame?.view?.hideSpinner() + } + // MARK: - EditGroupPhotoDelegate func photoSavedAtUrl(url: URL) { if let data = try? Data(contentsOf: url) { diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index 78ac98ea2..251618b11 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -256,7 +256,8 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve if let error = err as NSError?, error.code == 7 { LogService.log(topic: .MQTT, text: "Something went wrong") - } + } + isConnectedSuccess = false } func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopic topic: String) {} -- GitLab From 2af832227e38a09a7d979615e9b7e0345ae69c22 Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Fri, 10 Aug 2018 02:12:22 +0300 Subject: [PATCH 15/32] [NY-1083] IOS: Incorrect state of 2rd level of wheel on the Settings section --- Nynja-Share/Resources/Info.plist | 2 +- Nynja/OptionsItemsFactory.swift | 5 +++-- Nynja/Resources/Info.plist | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index c11f81732..5fff27344 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.135 + 0.2.136 Config $(Config) ModelsVersion diff --git a/Nynja/OptionsItemsFactory.swift b/Nynja/OptionsItemsFactory.swift index 6da1b98a5..6f0e2d9a6 100644 --- a/Nynja/OptionsItemsFactory.swift +++ b/Nynja/OptionsItemsFactory.swift @@ -17,9 +17,10 @@ class OptionsItemsFactory: WCBaseItemsFactory { // MARK: - Second lvl override var secondLevelItems: ItemModels { - return [logout, notifications, changeNumber, wheelPosition, buildNumber, support, languageSettings, theme, dataAndStorage, security, privacy, about, deleteAccount] +// return [logout, notifications, changeNumber, wheelPosition, buildNumber, support, languageSettings, theme, dataAndStorage, security, privacy, about, deleteAccount] + return [languageSettings, theme, dataAndStorage, security, privacy, notifications, changeNumber, wheelPosition, buildNumber, support] } - +//Language, Theme, Data and Storage, Security, Privacy, notification, change number, Wheel position, Build number, support // MARK: - Items var logout: ImageActionItemModel { let item = ImageActionItemModel(navItem: .logOut, action: { [weak navigateDelegate] (item, indexPath) in diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 8ee31ca43..06840e9ce 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.135 + 0.2.136 ConfServerAddress $(ConfServerAddress) ConfServerPort -- GitLab From 59ac57651b12e8d62b4843eb5327e0e14848401c Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Fri, 10 Aug 2018 04:49:39 +0300 Subject: [PATCH 16/32] Add Logout button --- Nynja/OptionsItemsFactory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nynja/OptionsItemsFactory.swift b/Nynja/OptionsItemsFactory.swift index 6f0e2d9a6..09089073b 100644 --- a/Nynja/OptionsItemsFactory.swift +++ b/Nynja/OptionsItemsFactory.swift @@ -18,7 +18,7 @@ class OptionsItemsFactory: WCBaseItemsFactory { // MARK: - Second lvl override var secondLevelItems: ItemModels { // return [logout, notifications, changeNumber, wheelPosition, buildNumber, support, languageSettings, theme, dataAndStorage, security, privacy, about, deleteAccount] - return [languageSettings, theme, dataAndStorage, security, privacy, notifications, changeNumber, wheelPosition, buildNumber, support] + return [languageSettings, theme, dataAndStorage, security, privacy, notifications, changeNumber, wheelPosition, buildNumber, support, logout] } //Language, Theme, Data and Storage, Security, Privacy, notification, change number, Wheel position, Build number, support // MARK: - Items -- GitLab From 8df9774a5a8f54278a39858372972531767e1edf Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Fri, 10 Aug 2018 09:09:55 +0300 Subject: [PATCH 17/32] 0.2.137 (#1084) --- Nynja-Share/Resources/Info.plist | 2 +- Nynja.xcodeproj/project.pbxproj | 36 +- .../AddParticipantsProtocols.swift | 2 +- .../Presenter/AddParticipantsPresenter.swift | 6 + .../View/AddParticipantsViewController.swift | 40 +- .../Call/CallInProgressProtocols.swift | 15 + Nynja/Modules/Call/CallProtocols.swift | 118 ---- .../Interactor/CallInProgressInteractor.swift | 96 ++- .../Presenter/CallInProgressPresenter.swift | 30 +- .../View/CallInProgressViewController.swift | 566 ++++++++------- .../WireFrame/CallInProgressWireframe.swift | 26 +- .../Call/Interactor/CallInteractor.swift | 300 -------- .../Call/Presenter/CallPresenter.swift | 170 ----- .../Call/View/CallViewController.swift | 655 ------------------ .../Call/WireFrame/CallWireframe.swift | 190 ----- .../Main/Interactor/MainInteractor.swift | 2 +- Nynja/Modules/Main/MainProtocols.swift | 5 +- .../Main/Presenter/MainPresenter.swift | 141 ++-- .../MainViewController+NavigateProtocol.swift | 1 - .../Main/WireFrame/MainWireframe.swift | 73 +- Nynja/Modules/Message/View/MessageVC.swift | 10 +- Nynja/OptionsItemsFactory.swift | 2 +- Nynja/Resources/Info.plist | 2 +- Nynja/Resources/en.lproj/Localizable.strings | 2 + Nynja/Resources/ru.lproj/Localizable.strings | 2 + Nynja/Services/NynjaCommunicatorService.swift | 109 ++- Podfile | 14 +- 27 files changed, 685 insertions(+), 1930 deletions(-) delete mode 100644 Nynja/Modules/Call/CallProtocols.swift delete mode 100644 Nynja/Modules/Call/Interactor/CallInteractor.swift delete mode 100644 Nynja/Modules/Call/Presenter/CallPresenter.swift delete mode 100644 Nynja/Modules/Call/View/CallViewController.swift delete mode 100644 Nynja/Modules/Call/WireFrame/CallWireframe.swift diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index 5fff27344..3b50d6f5d 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.136 + 0.2.137 Config $(Config) ModelsVersion diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 83c256125..214ccdab4 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -51,7 +51,6 @@ 0062D94E2062EDB000B915AC /* InviteFriendsItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0062D94D2062EDAF00B915AC /* InviteFriendsItemsFactory.swift */; }; 0070FB0004EFB510C3409746 /* AddContactByUsernamePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505C687860C446A37E2FE4FF /* AddContactByUsernamePresenter.swift */; }; 00772A49F4B53A5EB669E8F2 /* AddParticipantsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BA5392968EF1C9E844C927 /* AddParticipantsInteractor.swift */; }; - 00BB79AE09C68C716BF81645 /* CallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE839D7AC2C332E0234BC166 /* CallViewController.swift */; }; 00D7B5C720285BA7004B0E2B /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D7B5C620285BA7004B0E2B /* ScheduleView.swift */; }; 00E4A65F201A287100CEC61F /* MapSearchDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E4A65E201A287100CEC61F /* MapSearchDS.swift */; }; 00E8513B2021E96E007DC792 /* GApiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E8513A2021E96E007DC792 /* GApiResponse.swift */; }; @@ -437,7 +436,6 @@ 2CB54DD94DA23D7160F36472 /* SecurityWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48276F2EE408C27334B2894C /* SecurityWireframe.swift */; }; 2F2A5C12A7202E7834F923DC /* GroupRulesWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520422E90094C6C267AECE7E /* GroupRulesWireframe.swift */; }; 2F7C7F7837BDE6F5767A3A8C /* GroupStorageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D5302025583482829BBF2E /* GroupStorageViewController.swift */; }; - 314D0EEBF8C227FD046D43BA /* CallWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0974A63763BCB33814FC7A /* CallWireframe.swift */; }; 3219C8F242591BA17953FF33 /* SecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F509C0C8B9C738DBC7ABE07 /* SecurityViewController.swift */; }; 32868DD51F31CADF0028B260 /* ChatsListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32868DD41F31CADF0028B260 /* ChatsListProtocols.swift */; }; 32868DDB1F31CB500028B260 /* ChatsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32868DDA1F31CB500028B260 /* ChatsListViewController.swift */; }; @@ -575,7 +573,6 @@ 4B4266C1204D917800194BC1 /* ActionsView+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4266C0204D917800194BC1 /* ActionsView+Layout.swift */; }; 4B4266C3204D923400194BC1 /* Array+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4266C2204D923400194BC1 /* Array+UIView.swift */; }; 4B5A714D204F069000A551F5 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5A714C204F069000A551F5 /* ChatService.swift */; }; - 4B6AA4B1F92A45DB56BDC44C /* CallProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12456F574C62670B98C16E4B /* CallProtocols.swift */; }; 4B736D4720237C140028F2CB /* CGSizeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269D9DEF1FC3AF0D00324263 /* CGSizeExtension.swift */; }; 4B736D4920238FA40028F2CB /* ThumbnailGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B736D4820238FA40028F2CB /* ThumbnailGenerator.swift */; }; 4B7B81C62044790700C2EFCF /* TimeZoneLocal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7B81C52044790700C2EFCF /* TimeZoneLocal.swift */; }; @@ -606,7 +603,6 @@ 4D53FE7454959323B1CCFD96 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270F638DBB2D8FC1BDEB633 /* ProfileViewController.swift */; }; 4DAEBCF361B86B0AD3C98749 /* EditUsernameInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE3BAC9B7EA418FB463EF04 /* EditUsernameInteractor.swift */; }; 50960A9A3A3E544A494B4642 /* EditPhotoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7F09BDC006C92CF84A481E /* EditPhotoProtocols.swift */; }; - 52720EA907F60135673F1A46 /* CallPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5373874E1CD82B33214ED96 /* CallPresenter.swift */; }; 54FFFD58388E2B660C1E5A05 /* MapPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E04C696850BAF082139AAD /* MapPresenter.swift */; }; 553819525871F7D28AB90364 /* GroupRulesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705B62097A99515B3C778F35 /* GroupRulesPresenter.swift */; }; 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BA66D21FFC1A74CFD2F63C4 /* ProfileWireframe.swift */; }; @@ -648,7 +644,6 @@ 6D5157D01F30B36A002A27DB /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5157CF1F30B36A002A27DB /* ChatView.swift */; }; 6D5157D21F30B822002A27DB /* MicrophoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5157D11F30B822002A27DB /* MicrophoneView.swift */; }; 6D5168A21F30430900DA3728 /* SpeakerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5168A11F30430900DA3728 /* SpeakerView.swift */; }; - 6D5168A41F30638400DA3728 /* CallInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5168A31F30638400DA3728 /* CallInteractor.swift */; }; 6D56F2101F39FC6A00CBF56D /* AmazonManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D56F20F1F39FC6A00CBF56D /* AmazonManager.swift */; }; 6D6234F61F1E150600EF375F /* HistoryTableDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6234F51F1E150600EF375F /* HistoryTableDS.swift */; }; 6D6234F81F1E158600EF375F /* HistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6234F71F1E158600EF375F /* HistoryCell.swift */; }; @@ -2187,7 +2182,6 @@ 0E7F09BDC006C92CF84A481E /* EditPhotoProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditPhotoProtocols.swift; sourceTree = ""; }; 0FF56F6F8D90FB98A6B42971 /* EditGroupNameInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupNameInteractor.swift; sourceTree = ""; }; 10E04C696850BAF082139AAD /* MapPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapPresenter.swift; sourceTree = ""; }; - 12456F574C62670B98C16E4B /* CallProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CallProtocols.swift; sourceTree = ""; }; 1457809A715A3526EBF39205 /* MainViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 1746BDC1030434814FE63E0A /* DateTimePickerPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerPresenter.swift; sourceTree = ""; }; 17B34E74A0246B17348E9597 /* Pods-Nynja.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.debug.xcconfig"; sourceTree = ""; }; @@ -2664,7 +2658,6 @@ 6D5157CF1F30B36A002A27DB /* ChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; 6D5157D11F30B822002A27DB /* MicrophoneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MicrophoneView.swift; sourceTree = ""; }; 6D5168A11F30430900DA3728 /* SpeakerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeakerView.swift; sourceTree = ""; }; - 6D5168A31F30638400DA3728 /* CallInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInteractor.swift; sourceTree = ""; }; 6D56F20F1F39FC6A00CBF56D /* AmazonManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmazonManager.swift; sourceTree = ""; }; 6D5A57913B84E0665E3ABC0E /* EditGroupPhotoPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupPhotoPresenter.swift; sourceTree = ""; }; 6D6234F51F1E150600EF375F /* HistoryTableDS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryTableDS.swift; sourceTree = ""; }; @@ -2682,7 +2675,6 @@ 718EF22D86A9656BB6ED89D5 /* Pods-Nynja.translate.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.translate.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.translate.xcconfig"; sourceTree = ""; }; 7625A2CFF245BC8A47701724 /* AddParticipantsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsPresenter.swift; sourceTree = ""; }; 762BA232B5D027BD943DFA18 /* SecurityPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SecurityPresenter.swift; sourceTree = ""; }; - 7C0974A63763BCB33814FC7A /* CallWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CallWireframe.swift; sourceTree = ""; }; 7C19AFE8E64821851F4112EE /* ProfileProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileProtocols.swift; sourceTree = ""; }; 7C2CBB5F32D209160D00F744 /* CreateGroupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupViewController.swift; sourceTree = ""; }; 7CFD3063186FFCB048E843FD /* SelectCountryViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryViewController.swift; sourceTree = ""; }; @@ -3811,7 +3803,6 @@ F1EED41420C57C30001060C4 /* PhotoPreviewSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreviewSource.swift; sourceTree = ""; }; F1F219FC7966064C555AC2A4 /* TopUpAccountViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TopUpAccountViewController.swift; sourceTree = ""; }; F46A5D92A279FA0A509DA508 /* Pods-NynjaUnitTests.translate.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.translate.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.translate.xcconfig"; sourceTree = ""; }; - F5373874E1CD82B33214ED96 /* CallPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CallPresenter.swift; sourceTree = ""; }; F56141F2CF85255940EA304F /* EditPhotoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditPhotoWireframe.swift; sourceTree = ""; }; F79C9355E1AA4B373567F765 /* LanguageSettingsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsInteractor.swift; sourceTree = ""; }; F96FD91024D36848A4A4277C /* AddContactViaPhoneProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddContactViaPhoneProtocols.swift; sourceTree = ""; }; @@ -3865,7 +3856,6 @@ FE2D7CCC211C71AD00520D78 /* WalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletService.swift; sourceTree = ""; }; FE58F9B0208F00FE004AFDD3 /* MessageEditActionTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEditActionTable.swift; sourceTree = ""; }; FE58F9B2208F0583004AFDD3 /* DBMessageEditAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMessageEditAction.swift; sourceTree = ""; }; - FE839D7AC2C332E0234BC166 /* CallViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CallViewController.swift; sourceTree = ""; }; FE9E70CF21175DDC0034067A /* ChatScreenAlertFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatScreenAlertFactory.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -8490,7 +8480,6 @@ A33FA59FE338E9660AB10CD1 /* Presenter */ = { isa = PBXGroup; children = ( - F5373874E1CD82B33214ED96 /* CallPresenter.swift */, ); path = Presenter; sourceTree = ""; @@ -9885,7 +9874,6 @@ A4D0787972A19641165C28B6 /* WireFrame */ = { isa = PBXGroup; children = ( - 7C0974A63763BCB33814FC7A /* CallWireframe.swift */, ); path = WireFrame; sourceTree = ""; @@ -9914,7 +9902,6 @@ A895793051E246613AC4F30F /* View */ = { isa = PBXGroup; children = ( - FE839D7AC2C332E0234BC166 /* CallViewController.swift */, 6D67310F1F29E1F4003E8F8F /* BottomCallView.swift */, 6D5168A11F30430900DA3728 /* SpeakerView.swift */, 6D5157CF1F30B36A002A27DB /* ChatView.swift */, @@ -10320,7 +10307,6 @@ 9BD8E3EC20EF7776001384EC /* CallScreens */, 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */, 9BC9657520FF042D00052AE1 /* CallInProgressProtocols.swift */, - 12456F574C62670B98C16E4B /* CallProtocols.swift */, A895793051E246613AC4F30F /* View */, A33FA59FE338E9660AB10CD1 /* Presenter */, D6365C3D94F0150AFA59F586 /* Interactor */, @@ -10585,7 +10571,6 @@ D6365C3D94F0150AFA59F586 /* Interactor */ = { isa = PBXGroup; children = ( - 6D5168A31F30638400DA3728 /* CallInteractor.swift */, ); path = Interactor; sourceTree = ""; @@ -14019,7 +14004,6 @@ C9C695032022306D00A57297 /* SelectCountryTableDataSource.swift in Sources */, 8584C90F20920F3C001A0BBB /* StickerGridCellModel.swift in Sources */, A42D51A4206A361400EEB952 /* Feature.swift in Sources */, - 6D5168A41F30638400DA3728 /* CallInteractor.swift in Sources */, 260552A61F9E1CD100D68DE6 /* SearchHandler.swift in Sources */, F1607B1F20B21A9D00BDF60A /* CameraViewController.swift in Sources */, 853E595B20D71E6C007799B9 /* StickerPack+DB.swift in Sources */, @@ -14437,7 +14421,6 @@ 4BAB9CE02035CAE700385520 /* ScheduleInfo.swift in Sources */, 8ECC067E1FC5BCC6002CF225 /* TransferManager.swift in Sources */, A42D52B2206A53AA00EEB952 /* Cursor_Spec.swift in Sources */, - 4B6AA4B1F92A45DB56BDC44C /* CallProtocols.swift in Sources */, E76D13311FA35F3500B07F0E /* TextCellModel.swift in Sources */, E734831C1F9F53050090A4DB /* ProfileViewSectionDelegate.swift in Sources */, F10AFEB720F7B1B000C7CE83 /* WheelMediaFullItemPreview.swift in Sources */, @@ -14445,14 +14428,12 @@ F105C69D209F71BF0091786A /* CameraWireframe.swift in Sources */, A458FABF20EB8BB50075D55E /* MessageInteractor+ChannelActions.swift in Sources */, 26CD3FDD2104D1DD00597E62 /* AudioConvertionOperation.swift in Sources */, - 00BB79AE09C68C716BF81645 /* CallViewController.swift in Sources */, 8514F17C20EA219F00883513 /* ContextMenuConfiguration.swift in Sources */, FBCE840D20E525A6003B7558 /* HTTPResponseResult.swift in Sources */, A43B259F20AB1DFA00FF8107 /* PickerCell.swift in Sources */, 2661D12E1F373D1700F3E125 /* BadgeView.swift in Sources */, F10B0E1F20B4CC5400528E7A /* CameraSettingsCoordinator.swift in Sources */, F119E67920D27EA50043A532 /* VideoPreviewCVCell.swift in Sources */, - 52720EA907F60135673F1A46 /* CallPresenter.swift in Sources */, 8502DB542061030100613C8C /* WheelPositionPickerViewController.swift in Sources */, A45F116120B422AF00F45004 /* Message+System.swift in Sources */, FBCE83E020E52496003B7558 /* ContactServices.swift in Sources */, @@ -14463,7 +14444,6 @@ 85433F2C204D5AA500B373A7 /* NynjaCloseButton.swift in Sources */, A44B4D5A20CE9BDF00CA700A /* SwitchCell.swift in Sources */, F1607B3020B2FD5A00BDF60A /* QRNotificationVIew.swift in Sources */, - 314D0EEBF8C227FD046D43BA /* CallWireframe.swift in Sources */, 260313A420A0A4BA009AC66D /* DirectableActionCellViewModel.swift in Sources */, 859773232087965700B03B4A /* NynjaControlContainerView.swift in Sources */, E743B58A1FB0911200F72F92 /* ParticipantsContactCell.swift in Sources */, @@ -16358,8 +16338,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRC; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -16370,8 +16350,8 @@ MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "d22f9bd2-3299-4c7a-ac33-6a12c4c3cdaa"; - PROVISIONING_PROFILE_SPECIFIER = NynjaRC_dev; + PROVISIONING_PROFILE = "3d9e361e-de0d-4189-be64-4bea82318684"; + PROVISIONING_PROFILE_SPECIFIER = NynjaRC_adhoc; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -16389,8 +16369,8 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -16402,8 +16382,8 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "1fc6c35e-7400-4bfc-8831-e2bba8e2d5e0"; - PROVISIONING_PROFILE_SPECIFIER = NynjaRC_devExt; + PROVISIONING_PROFILE = "f232c367-7000-49c5-b32c-0efb36c5f2e3"; + PROVISIONING_PROFILE_SPECIFIER = NynjaRC_adhocExt; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; diff --git a/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift b/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift index 00dd6bad4..98cd7d640 100644 --- a/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift +++ b/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift @@ -40,7 +40,7 @@ protocol AddParticipantsViewProtocol: class { func updateParticipantsList(_ participants: GroupedParticipants) } -protocol AddParticipantsPresenterProtocol: BasePresenterProtocol { +protocol AddParticipantsPresenterProtocol: BasePresenterProtocol, NavigationProtocol { var view: AddParticipantsViewProtocol! { get set } var interactor: AddParticipantsInteractorInputProtocol! { get set } diff --git a/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift b/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift index 66579585c..bfca0fb1c 100644 --- a/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift +++ b/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift @@ -117,6 +117,12 @@ class AddParticipantsPresenter: BasePresenter, AddParticipantsPresenterProtocol, } } } + + // MARK: NavigationProtocol + func back() { + let arr:[Contact] = [] + hide(with: arr) + } // MARK: - AddParticipantsInteractorOutputProtocol diff --git a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift index 312e41406..3fba25e68 100644 --- a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift +++ b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift @@ -8,7 +8,7 @@ import UIKit -class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, NavigationProtocol { +class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { var presenter: AddParticipantsPresenterProtocol! { didSet { @@ -148,7 +148,6 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga var title:String = "" var backBtnImage:UIImage? = nil - var selectAllBtnImage:UIImage? = nil var navHandler:NavigationProtocol? = nil if presenter.participantsMode == .delete { @@ -156,21 +155,18 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga } else if presenter.participantsMode == .createGroupCall { title = "add_members_to_call".localized.uppercased() backBtnImage = UIImage(named:"ic_close_clear") - selectAllBtnImage = UIImage(named:"ic_unchecked") - navHandler = self + navHandler = presenter self.selectAllButtonVisible = true } else if presenter.participantsMode == .updateGroupCall { title = "add_members_to_call".localized.uppercased() backBtnImage = UIImage(named:"ic_close_clear") - selectAllBtnImage = UIImage(named:"ic_unchecked") - navHandler = self + navHandler = presenter self.selectAllButtonVisible = true } else if presenter.participantsMode == .createConferenceCall { - title = "add_members_to_call".localized.uppercased() title = "add_members_to_call".localized.uppercased() backBtnImage = UIImage(named:"ic_close_clear") - selectAllBtnImage = UIImage(named:"ic_unchecked") - navHandler = self + navHandler = presenter + self.selectAllButtonVisible = true } else if presenter.participantsMode == .admins { title = "admins".localized.uppercased() } else { @@ -203,10 +199,13 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga doneButton.setTitle("delete".localized.uppercased(), for: .normal) } else if presenter.participantsMode == .createGroupCall { doneButton.setTitle("call".localized.uppercased(), for: .normal) + doneButton.setTitle("call".localized.uppercased(), for: .disabled) } else if presenter.participantsMode == .createConferenceCall { doneButton.setTitle("call".localized.uppercased(), for: .normal) + doneButton.setTitle("call".localized.uppercased(), for: .disabled) } else if presenter.participantsMode == .updateGroupCall { doneButton.setTitle("done".localized.uppercased(), for: .normal) + doneButton.setTitle("done".localized.uppercased(), for: .disabled) } else { doneButton.setTitle("done".localized.uppercased(), for: .normal) } @@ -214,6 +213,8 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga doneButton.addTarget(self, action: #selector(doneTapped(_:)), for: .touchUpInside) setupTestingKeysInSubviews() + + updateDoneButtonState() } private func configureLists() { @@ -254,6 +255,13 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga return layout } + + private func updateDoneButtonState() { + + if presenter.participantsMode == .createGroupCall || presenter.participantsMode == .updateGroupCall || presenter.participantsMode == .createConferenceCall { + doneButton.isEnabled = (participantsDataSource.selectedParticipants.count > 0) + } + } // MARK: - Actions @objc private func doneTapped(_ button: UIButton) { @@ -305,7 +313,7 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga if sender.isSelected { participantsDataSource.selectedParticipants.append(contentsOf: participants) - + } } } @@ -338,6 +346,8 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga avatarsView.reloadData() tableView.reloadData() + + updateDoneButtonState() } // MARK: - Keyboard @@ -376,12 +386,6 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, Naviga tableView.reloadData() avatarsView.reloadData() } - - // MARK: NavigationProtocol - func back() { - let arr:[Contact] = [] - presenter.hide(with: arr) - } } // MARK: - AddParticipantsActionsDelegate @@ -395,6 +399,8 @@ extension AddParticipantsViewController: ParticipantsActionsDelegate { deselectParticipant(participant) tableView.reloadData() avatarsView.reloadData() + + updateDoneButtonState() } func participantTapped(_ participant: Participant) { @@ -421,6 +427,8 @@ extension AddParticipantsViewController: ParticipantsActionsDelegate { } avatarsView.reloadData() + + updateDoneButtonState() } private func deselectParticipant(_ participant: Participant) { diff --git a/Nynja/Modules/Call/CallInProgressProtocols.swift b/Nynja/Modules/Call/CallInProgressProtocols.swift index e80506a85..a3e92e39c 100644 --- a/Nynja/Modules/Call/CallInProgressProtocols.swift +++ b/Nynja/Modules/Call/CallInProgressProtocols.swift @@ -20,6 +20,7 @@ protocol CallInProgressWireFrameProtocol: class { func messageActionWith(room:Room, isVideo: Bool) func updateCallParticipants() func showMenuWith(participant: NYNCallParticipant?, delegate: ManageCallInProgressParticipantsProtocol) + func callClosed() } protocol CallInProgressViewProtocol: class { @@ -37,6 +38,9 @@ protocol CallInProgressViewProtocol: class { func updateCallBy(status: CallStatus) func callFailed() func askEndOrLeave() + func didAddRemoteVideoStream() + func didStartLocalCapturer() + func didStopLocalCapturer() } protocol CallInProgressPresenterProtocol: class { @@ -70,6 +74,9 @@ protocol CallInProgressPresenterProtocol: class { func showMenuWith(groupCollectionCell: GroupCollectionViewCell) func endCall() func hangupCall() + func addRemoteVideoRenderer(inView view: UIView) + func attachLocalVideoPreview(inView view: UIView) + func dettachLocalVideoPreview(inView view: UIView) } protocol CallInProgressInteractorOutputProtocol: class { @@ -87,6 +94,9 @@ protocol CallInProgressInteractorOutputProtocol: class { func update(participants: [NYNCallParticipant]) func callFailed() func askEndOrLeave() + func didAddRemoteVideoStream() + func didStartLocalCapturer() + func didStopLocalCapturer() } protocol CallInProgressInteractorInputProtocol: class { @@ -94,6 +104,8 @@ protocol CallInProgressInteractorInputProtocol: class { var presenter: CallInProgressInteractorOutputProtocol! { get set } var call: VICall? { get set } var contact: Contact? { get } + var room: Room? { get } + /** * Add here your methods for communication PRESENTER -> INTERACTOR */ @@ -113,6 +125,9 @@ protocol CallInProgressInteractorInputProtocol: class { func removeCallMember(memberId: String) func endCall() func hangupCall() + func addRemoteVideoRenderer(inView view: UIView) + func attachLocalVideoPreview(inView view: UIView) + func detachLocalVideoPreview(inView view: UIView) } protocol ManageCallInProgressParticipantsProtocol: class { diff --git a/Nynja/Modules/Call/CallProtocols.swift b/Nynja/Modules/Call/CallProtocols.swift deleted file mode 100644 index df276ca62..000000000 --- a/Nynja/Modules/Call/CallProtocols.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// CallCallProtocols.swift -// Nynja -// -// Created by Bohdan Paliychuk on 26/07/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit -import VoxImplant - -protocol CallWireFrameProtocol: class { - - func presentCall(navigation: UINavigationController, callMode: CallMode, contact: Contact, call: VICall?, main: MainWireFrame?) - - /** - * Add here your methods for communication PRESENTER -> WIREFRAME - */ - func messageAction(isVideo: Bool, contact: Contact) - func messageActionWith(room:Room, isVideo: Bool) - func updateCallParticipants() - func showMenuWith(participant: NYNCallParticipant?, delegate: ManageCallParticipantsProtocol) -} - -protocol CallViewProtocol: class { - - var presenter: CallPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ - func setupUI() - func updateTime(text: String) - func remoteVideoStreamStopped() - func changeUIToIncall() - func update(participants: [NYNCallParticipant]) - func updateCallBy(status: CallStatus) - func callFailed() -} - -protocol CallPresenterProtocol: class { - - var view: CallViewProtocol! { get set } - var interactor: CallInteractorInputProtocol! { get set } - var wireFrame: CallWireFrameProtocol! { get set } - var contact: Contact? { get set } - var room: Room? { get set } - var type: CallMode! { get set } - /** - * Add here your methods for communication VIEW -> PRESENTER - */ - - func willShow() - func setupViews(myView: UIView, remoteView: UIView) - - func acceptCall(withVideo: Bool) - func declineCall() - func speakerAction() - func messageAcion(with roomId:String, isVideo: Bool) - func microphoneAction() - func toggleMicrophone() - func isMuted()->Bool - func switchCamera() - func disableVideo() - func viewShowed() - func rejectCall() - func updateCallParticipants() - func showMenuWith(groupCollectionCell: GroupCollectionViewCell) - func endCall() -} - -protocol CallInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ - - func callClosed() - func callConnected(withVideo: Bool) - func setRingingWithoutVideo() - func setRingingStatus() - func remoteVideoStreamStopped() - func updateTime(text: String) - func update(participants: [NYNCallParticipant]) - func callFailed() -} - -protocol CallInteractorInputProtocol: class { - - var presenter: CallInteractorOutputProtocol! { get set } - var call: VICall? { get set } - var contact: Contact? { get } - - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ - - func acceptCall(withVideo: Bool) - func declineCall() - func speakerAction() - func microphoneAction() - func toggleMicrophone() - func isMuted()->Bool - func switchCamera() - func setupViews(myView: UIView, remoteView: UIView) - func disableVideo() - func setupDelegate() - func rejectCall() - func updateGroupCall(contacts: [Contact]) - func removeCallMember(memberId: String) - func endCall() -} - -protocol ManageCallParticipantsProtocol: class { - func remove(participant: NYNCallParticipant?) - func mute(participant: NYNCallParticipant?) - func unmute(participant: NYNCallParticipant?) -} diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift index a23be1b3f..f3a59004d 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift @@ -8,39 +8,74 @@ import VoxImplant -class CallInProgressInteractor: CallInProgressInteractorInputProtocol, VoxServiceDelegate, NynjaCommunicatorServiceDelegate { +class CallInProgressInteractor: CallInProgressInteractorInputProtocol, VoxServiceDelegate, NynjaCallDelegate { weak var presenter: CallInProgressInteractorOutputProtocol! weak var call: VICall? var vox = VoxService.sharedInstance + var callService = NynjaCommunicatorService.sharedInstance var timer: Timer? var contact: Contact? { guard let voxId = call?.voxId else { return nil } return ContactDAO.findContactBy(voxId: voxId) } + + var room: Room? { + guard let roomId = nynCall?.externalInfo else { return nil } + return RoomDAO.findRoom(by: roomId) + } + weak var nynCall: NYNCall? var duration = 0 + let storageService: StorageService = .sharedInstance + let payloadBuilder: MessagePayloadBuilderInput = MessagePayloadBuilder() + private let mqttService: MQTTService = .sharedInstance + let processingManager = DefaultMessagesProcessingManager.shared + private(set) lazy var messageFactory: MessageFactoryProtocol = { + let factory = MessageFactory() + + let dependencies = MessageFactory.Dependencies( + storageService: storageService, + payloadBuilder: payloadBuilder + ) + factory.inject(dependencies: dependencies) + + return factory + }() + + private(set) lazy var messageSendingService: MessageSendingServiceProtocol = { + let service = MessageSendingService() + let dependencies = MessageSendingService.Dependencies( + mqttService: mqttService, + storageService: storageService, + processingManager: processingManager + ) + service.inject(dependencies: dependencies) + + return service + }() + func setupDelegate() { vox.delegate = self - NynjaCommunicatorService.sharedInstance.delegate = self + callService.callDelegate = self } func acceptCall(withVideo: Bool) { if let id = call { vox.answer(call: id, withVideo: withVideo) } else if let ncall = self.nynCall { - NynjaCommunicatorService.sharedInstance.acceptConference(call: ncall) + callService.acceptConference(call: ncall) } } func rejectCall() { if let nc = self.nynCall { - NynjaCommunicatorService.sharedInstance.rejectConference(call: nc) + callService.rejectConference(call: nc) self.presenter.callClosed() } @@ -93,7 +128,11 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, VoxServic } func speakerAction() { - vox.switchSpeaker() + if AudioManager.sharedInstance.speaker == .loud { + AudioManager.sharedInstance.speaker = .soft + } else { + AudioManager.sharedInstance.speaker = .loud + } } func disableVideo() { @@ -140,7 +179,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, VoxServic } func switchCamera() { - vox.switchCamera() + callService.switchCamera() } // MARK: - VoxServiceDelegate @@ -190,18 +229,30 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, VoxServic } } - func dialing(call: NYNCall) { - + func didAddVideoStreamForCall(call: NYNCall) { + self.presenter.didAddRemoteVideoStream() } - - func incomingCallRinging(call: NYNCall) { - + + func didStartLocalCapturerForCall(call: NYNCall) { + self.presenter.didStartLocalCapturer() } - func creatingGroupCall(name: String, call: NYNCall) { - + func didStopLocalCapturerForCall(call: NYNCall) { + self.presenter.didStopLocalCapturer() } - + + func addRemoteVideoRenderer(inView view: UIView) { + callService.attachRemoteVideoRenderer(inView: view) + } + + func attachLocalVideoPreview(inView view: UIView) { + callService.attachLocalVideoPreview(inView: view) + } + + func detachLocalVideoPreview(inView view: UIView) { + callService.detachLocalVideoPreview(inView: view) + } + func stopTimer () { if timer != nil { @@ -260,6 +311,19 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, VoxServic self.presenter.update(participants: ncall.participants) } } + + func conferenceCreated(call: NYNCall) { + sendCall(ncall: call) + } + + func sendCall(ncall: NYNCall) { + let room = Room() + room.id = ncall.externalInfo + let membersIds = ncall.participants.map({ $0.address ?? "" }) + let message = messageFactory.makeCallMessage(members: membersIds, room: room) + try? storageService.perform(action: .save, with: message) + messageSendingService.sendMessage(message) + } func updateGroupCall(contacts: [Contact]) { @@ -268,14 +332,14 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, VoxServic for ctc in contacts { let name = "\(ctc.names ?? "") \(ctc.surnames ?? "")" - NynjaCommunicatorService.sharedInstance.addConferenceMember(conferenceId: ncall.callId, phoneId: ctc.phone_id!, name: name) + callService.addConferenceMember(conferenceId: ncall.callId, phoneId: ctc.phone_id!, name: name) } } } func removeCallMember(memberId: String) { if let ncall = self.nynCall { - NynjaCommunicatorService.sharedInstance.removeConferenceMember(conferenceId: ncall.callId, memberId: memberId) + callService.removeConferenceMember(conferenceId: ncall.callId, memberId: memberId) } } } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift index 5d7dd9c0d..a0ff75729 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift @@ -83,10 +83,7 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn } func callClosed() { - if let navigation = (self.view as? UIViewController)?.navigationController { - (navigation.parent as? MainViewProtocol)?.presenter.wireFrame.hideReturnToCallView() - navigation.popViewController(animated: true) - } + wireFrame.callClosed() } func callConnected(withVideo: Bool) { @@ -108,6 +105,30 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn self.view.askEndOrLeave() } + func didAddRemoteVideoStream() { + self.view.didAddRemoteVideoStream() + } + + func didStartLocalCapturer() { + self.view.didStartLocalCapturer() + } + + func didStopLocalCapturer() { + self.view.didStopLocalCapturer() + } + + func addRemoteVideoRenderer(inView view: UIView) { + self.interactor.addRemoteVideoRenderer(inView: view) + } + + func attachLocalVideoPreview(inView view: UIView) { + self.interactor.attachLocalVideoPreview(inView: view) + } + + func dettachLocalVideoPreview(inView view: UIView) { + self.interactor.detachLocalVideoPreview(inView: view) + } + func remoteVideoStreamStopped() { //self.view.remoteVideoStreamStopped() self.view.setupUI() @@ -143,7 +164,6 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn } func viewShowed() { - self.interactor.setupDelegate() } // MARK: EditParticpantsDelegate diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift index 54439ad56..1688f4407 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift @@ -6,8 +6,29 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // +enum CallMode { + case incamingCall + case incamingGroupCall + case outGoingCall + case outGoingGroupCall + case incamingVideoCall + case incamingVideoGroupCall + case outGoingVideoCall + case outGoingVideoGroupCall +} + +enum CallStatus { + case callStarting + case callIncomingAudio + case callIncomingVideo + case callOutgoingAudio + case callOutgoingVideo + case callConnecting + case callInProgress +} + enum CallInProgressMode { - + case oneToOneAudio case oneToOneVideo case groupAudio @@ -16,7 +37,7 @@ enum CallInProgressMode { import UIKit class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCallViewProtocol, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, GroupCollectionViewCellDelegate, GroupAddParticipantsCollectionViewCellDelegate, SpeakerDelegate { - + var presenter: CallInProgressPresenterProtocol! var contact: Contact? var callInProgressMode: CallInProgressMode! @@ -24,116 +45,119 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa var dataSource: NSMutableArray = [] var moderator: Bool = false var roomId: String = "" - + let bottomViewHeight: Float = 188.0 let labelNameHeight: Float = 40.0 let labelStatusHeight: Float = 30.0 let offset: Float = 10.0 let middleGVHeight: Float = 100.0 let statusBarHeight: Float = Float(UIApplication.shared.statusBarFrame.size.height) - - var collapsed: Bool = false - + var collapsed: Bool = false + var initialized: Bool = false + var needLocalRenderer: Bool = false + var needRemoteRenderer: Bool = false + private struct ConstraintConstants { static let buttonSize: CGFloat = 44.0 - + static let bottomOffset: CGFloat = 16.0 static let labelNameFontSize: CGFloat = 22.0 static let labelStatusFontSize: CGFloat = 16.0 - + static let labelsFontSize: CGFloat = 11.0 static let minSectionHeight: CGFloat = 83.0 - + static let declineButtonSize:CGFloat = 68.0 } - + lazy var expectedRowsInCollectionView: Int = { - + var sections:Int = 0 var topOffset:Float = 165 var bottomOffset = self.bottomViewHeight let screenHeight:Float = Float(UIScreen.main.bounds.size.height) - + let nameFont = UIFont(name: Constants.fonts.medium, size: ConstraintConstants.labelNameFontSize) let statusFont = UIFont(name: Constants.fonts.medium, size: ConstraintConstants.labelStatusFontSize) - + if let nf = nameFont, let sf = statusFont { topOffset = Float(nf.lineHeight) + Float(sf.lineHeight) + Float(8*offset) + Float(UIApplication.shared.statusBarFrame.size.height) } - + let clearSpace:Float = screenHeight - topOffset - bottomOffset - + if 0 > clearSpace { - + sections = 3 - + } else { - + let minSectionHeight:Float = Float(ConstraintConstants.minSectionHeight) - + sections = Int(floor(clearSpace/minSectionHeight)) } - + return sections }() - + lazy var backgroundImage: UIImageView = { - + let img = UIImageView() img.isUserInteractionEnabled = true img.contentMode = .scaleAspectFill img.backgroundColor = .clear self.view.addSubview(img) - + img.snp.makeConstraints({ (make) in make.top.left.right.equalTo(self.view) make.bottom.equalTo(self.bottomView.snp.top).offset(-offset/2) }) return img }() - + lazy var otherVideoView: UIView = { - + let view = UIView() view.isUserInteractionEnabled = true - view.backgroundColor = .yellow + view.backgroundColor = self.view.backgroundColor self.view.addSubview(view) - + view.snp.makeConstraints({ (make) in make.top.left.right.equalTo(self.view) make.bottom.equalTo(self.bottomView.snp.top).offset(-offset/2) }) return view }() - + lazy var myVideoView: UIView = { - + let view = UIView() view.isUserInteractionEnabled = true - view.backgroundColor = .green + view.backgroundColor = .clear self.view.addSubview(view) - + view.snp.makeConstraints({ (make) in make.left.equalTo(self.view).offset(1.5*offset) make.bottom.equalTo(self.otherVideoView.snp.bottom).offset(-1.5*offset) - make.width.equalTo(30) - make.height.equalTo(60) + make.width.equalTo(72.adjustedByWidth) + make.height.equalTo(115.adjustedByHeight) }) return view }() - + lazy var switchCameraButton: UIButton = { - + let btn = UIButton() let img = #imageLiteral(resourceName: "switch_camera") btn.backgroundColor = .clear btn.setImage(img, for: .normal) self.view.addSubview(btn) - btn.addTarget(self, action: #selector(declineButtonPressed), for: .touchUpInside) - + + btn.addTarget(self, action: #selector(switchCameraButtonPressed), for: .touchUpInside) + btn.snp.makeConstraints({ (make) in make.right.equalTo(self.view).offset(-offset) make.bottom.equalTo(self.otherVideoView.snp.bottom).offset(-offset) @@ -142,7 +166,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa return btn }() - + lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical @@ -154,7 +178,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa cv.delegate = self cv.dataSource = self cv.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() - + cv.register(GroupCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: GroupCollectionViewCell.self)) cv.register(GroupAddParticipantsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: GroupAddParticipantsCollectionViewCell.self)) @@ -162,11 +186,11 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa make.top.equalTo(self.statusLabel.snp.bottom).offset(offset) make.left.right.equalTo(self.view) make.bottom.equalTo(self.openCloseCallsButton.snp.top).offset(-offset/2) - + }) return cv }() - + private lazy var nameLabel: UILabel = { let scwidth = UIScreen.main.bounds.width let lbl = UILabel() @@ -177,16 +201,16 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa lbl.baselineAdjustment = .alignCenters lbl.lineBreakMode = .byTruncatingTail self.view.addSubview(lbl) - + lbl.snp.makeConstraints({ (make) in make.top.equalTo(self.view).offset(6*offset) make.left.right.equalTo(self.view).offset(offset) make.height.equalTo(lbl.font.lineHeight) }) - + return lbl }() - + private lazy var statusLabel: UILabel = { let scwidth = UIScreen.main.bounds.width let lbl = UILabel() @@ -199,31 +223,31 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa lbl.baselineAdjustment = .alignCenters lbl.lineBreakMode = .byClipping self.view.addSubview(lbl) - + lbl.snp.makeConstraints({ (make) in make.top.equalTo(self.nameLabel.snp.bottom).offset(offset) make.left.right.equalTo(self.view).offset(offset) make.height.equalTo(lbl.font.lineHeight) }) - + return lbl }() - + lazy var bottomView : UIView = { - + let view = UIView() view.isUserInteractionEnabled = true view.backgroundColor = .clear self.view.addSubview(view) - + view.snp.makeConstraints({ (make) in make.bottom.left.right.equalTo(self.view) make.height.equalTo(self.bottomViewHeight) }) - + return view }() - + lazy var declineButton : UIButton = { let btn = UIButton() let img = UIImage.init(named:"ic_decline_call") @@ -231,7 +255,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa btn.setImage(img, for: .normal) self.bottomView.addSubview(btn) btn.addTarget(self, action: #selector(declineButtonPressed), for: .touchUpInside) - + btn.snp.makeConstraints({ (make) in make.width.height.equalTo(ConstraintConstants.declineButtonSize) make.width.height.equalTo(ConstraintConstants.declineButtonSize) @@ -240,13 +264,13 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa }) return btn }() - + lazy var speakerButton : UIButton = { - + let btn = UIButton() - + AudioManager.sharedInstance.speaker = .soft - + btn.setImage(#imageLiteral(resourceName: "ic_speaker_off"), for: .normal) btn.setImage(#imageLiteral(resourceName: "ic_speaker_on"), for: .selected) btn.isSelected = (AudioManager.sharedInstance.speaker == .loud) @@ -254,10 +278,10 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa btn.layer.masksToBounds = true self.bottomView.addSubview(btn) - + let offsetX = UIScreen.main.bounds.width / 3.0 btn.addTarget(self, action: #selector(speakerButtonPressed), for: .touchUpInside) - + btn.snp.makeConstraints({ (make) in make.width.equalTo(ConstraintConstants.buttonSize) make.height.equalTo(ConstraintConstants.buttonSize) @@ -266,7 +290,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa }) return btn }() - + lazy var speakerLabel: UILabel = { let label = UILabel() label.text = "speakerphone".localized @@ -274,15 +298,15 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .center label.font = UIFont(name: Constants.fonts.medium, size: ConstraintConstants.labelsFontSize) self.bottomView.addSubview(label) - + label.snp.makeConstraints({ (make) in make.top.equalTo(self.speakerButton.snp.bottom).offset(0) make.centerX.equalTo(self.speakerButton) }) - + return label }() - + lazy var cameraLabel: UILabel = { let label = UILabel() label.text = "turn_on_video".localized @@ -290,17 +314,17 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .center label.font = UIFont(name: Constants.fonts.medium, size: ConstraintConstants.labelsFontSize) self.bottomView.addSubview(label) - + let offsetX = UIScreen.main.bounds.width / 3.0 label.snp.makeConstraints({ (make) in make.centerX.equalTo(self.view).offset(-offsetX) make.bottom.equalTo(self.declineButton.snp.top).offset(-2*offset) }) - + return label }() - + lazy var cameraButton: UIButton = { let button = UIButton() button.setImage(#imageLiteral(resourceName: "ic_video_on_voice_call"), for: .normal) @@ -308,17 +332,17 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa button.isEnabled = (.oneToOneVideo == self.callInProgressMode) self.bottomView.addSubview(button) let offsetX = UIScreen.main.bounds.width / 3.0 - + button.snp.makeConstraints({ (make) in make.height.equalTo(ConstraintConstants.buttonSize) make.width.equalTo(ConstraintConstants.buttonSize) make.centerX.equalTo(self.bottomView).offset(-offsetX) make.bottom.equalTo(self.cameraLabel.snp.top).offset(0) }) - + return button }() - + lazy var microphoneButton : UIButton = { let btn = UIButton() let muteImage = (self.isMuted()) ? #imageLiteral(resourceName: "ic_mute_voice_call"): #imageLiteral(resourceName: "ic_unmute_voice_call") @@ -326,7 +350,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa btn.layer.masksToBounds = true self.bottomView.addSubview(btn) btn.addTarget(self, action: #selector(microphoneButtonPressed), for: .touchUpInside) - + btn.snp.makeConstraints({ (make) in make.width.equalTo(ConstraintConstants.buttonSize) make.height.equalTo(ConstraintConstants.buttonSize) @@ -335,7 +359,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa }) return btn }() - + lazy var muteLabel: UILabel = { let label = UILabel() label.text = "mute_call".localized @@ -343,12 +367,12 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .center label.font = UIFont(name: Constants.fonts.medium, size: ConstraintConstants.labelsFontSize) self.bottomView.addSubview(label) - + label.snp.makeConstraints({ (make) in make.centerY.equalTo(self.cameraLabel.snp.centerY).offset(0) make.centerX.equalTo(self.microphoneButton) }) - + return label }() @@ -359,7 +383,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.bottomView.addSubview(btn) let offsetX = UIScreen.main.bounds.width / 3.0 btn.addTarget(self, action: #selector(messageButtonPressed), for: .touchUpInside) - + btn.snp.makeConstraints({ (make) in make.width.equalTo(ConstraintConstants.buttonSize) make.height.equalTo(ConstraintConstants.buttonSize) @@ -368,7 +392,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa }) return btn }() - + lazy var chatLabel: UILabel = { let label = UILabel() label.text = "return_to_chat_from_call".localized @@ -376,16 +400,16 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .center label.font = UIFont(name: Constants.fonts.medium, size: ConstraintConstants.labelsFontSize) self.bottomView.addSubview(label) - + label.snp.makeConstraints({ (make) in make.top.equalTo(self.messageButton.snp.bottom).offset(0) make.centerX.equalTo(self.messageButton) }) - + return label }() - + lazy var moreLabel: UILabel = { let label = UILabel() label.text = "more".localized @@ -393,32 +417,32 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .center label.font = UIFont(name: Constants.fonts.medium, size: ConstraintConstants.labelsFontSize) self.bottomView.addSubview(label) - + let offsetX = UIScreen.main.bounds.width / 3.0 - + label.snp.makeConstraints({ (make) in make.centerX.equalTo(self.bottomView).offset(offsetX) make.bottom.equalTo(self.declineButton.snp.top).offset(-2*offset) }) - + return label }() - + lazy var moreButton: UIButton = { let button = UIButton() button.setImage(#imageLiteral(resourceName: "ic_more_voice_call"), for: .normal) button.addTarget(self, action: #selector(onMoreButtonPressed), for: .touchUpInside) - + self.bottomView.addSubview(button) let offsetX = UIScreen.main.bounds.width / 3.0 - + button.snp.makeConstraints({ (make) in make.height.equalTo(ConstraintConstants.buttonSize) make.width.equalTo(ConstraintConstants.buttonSize) make.centerX.equalTo(self.bottomView).offset(offsetX) make.bottom.equalTo(self.moreLabel.snp.top).offset(0) }) - + return button }() @@ -429,17 +453,17 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .left label.font = UIFont(name: Constants.fonts.regular, size: 13) self.view.addSubview(label) - + let offsetX = UIScreen.main.bounds.width / 3.0 - + label.snp.makeConstraints({ (make) in make.left.equalTo(self.view).offset(offset) - make.bottom.equalTo(self.cameraButton.snp.top).offset(0)//offset(0) + make.bottom.equalTo(self.bottomView.snp.top).offset(0) }) - + return label }() - + lazy var heldCallDescription: UILabel = { let label = UILabel() label.text = "" @@ -447,19 +471,19 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .right label.font = UIFont(name: Constants.fonts.regular, size: 13) self.view.addSubview(label) - + let offsetX = UIScreen.main.bounds.width / 3.0 - + label.snp.makeConstraints({ (make) in make.right.equalTo(self.view).offset(-offset) make.centerY.equalTo(self.heldCallTitle) make.width.lessThanOrEqualTo(UIScreen.main.bounds.width/2 - CGFloat(offset)) make.left.equalTo(self.heldCallTitle.snp.right).offset(offset) }) - + return label }() - + lazy var activeCallTitle: UILabel = { let label = UILabel() label.text = "" @@ -467,17 +491,17 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .left label.font = UIFont(name: Constants.fonts.regular, size: 13) self.view.addSubview(label) - + let offsetX = UIScreen.main.bounds.width / 3.0 - + label.snp.makeConstraints({ (make) in make.left.equalTo(self.view).offset(offset) make.bottom.equalTo(self.heldCallTitle.snp.top).offset(0)//offset(-offset/2) }) - + return label }() - + lazy var activeCallDescription: UILabel = { let label = UILabel() label.text = "" @@ -485,110 +509,108 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa label.textAlignment = .right label.font = UIFont(name: Constants.fonts.regular, size: 13) self.view.addSubview(label) - + let offsetX = UIScreen.main.bounds.width / 3.0 - + label.snp.makeConstraints({ (make) in make.right.equalTo(self.view).offset(-offset) make.centerY.equalTo(self.activeCallTitle) make.width.lessThanOrEqualTo(UIScreen.main.bounds.width/2 - CGFloat(offset)) make.left.equalTo(self.activeCallTitle.snp.right).offset(offset) }) - + return label }() lazy var openCloseCallsButton: UIButton = { let button = UIButton() - + let img = self.collapsed ? UIImage.init(named: "arrow_expand") : UIImage.init(named: "arrow_collapse") button.setImage(img, for: .normal) button.addTarget(self, action: #selector(onOpenCloseCallsButton), for: .touchUpInside) - + self.view.addSubview(button) let offsetX = UIScreen.main.bounds.width / 3.0 - + button.snp.makeConstraints({ (make) in make.height.equalTo(0)//(34) make.width.equalTo(0)//(34) make.centerX.equalTo(self.view) make.bottom.equalTo(self.activeCallTitle.snp.top).offset(0)//offset(-offset/2) }) - + return button }() - - func updateModeVisibility() { - + + func setupVisibility() { + + // for all modes in view self.nameLabel.isHidden = false - self.statusLabel.isHidden = false - self.declineButton.isHidden = false - self.collectionView.isHidden = true + self.heldCallTitle.isHidden = true + self.heldCallDescription.isHidden = true + self.activeCallTitle.isHidden = true + self.activeCallDescription.isHidden = true + self.openCloseCallsButton.isHidden = true + self.openCloseCallsButton.isEnabled = false + + // for all modes in bottom view + self.declineButton.isHidden = false self.speakerButton.isHidden = false self.speakerLabel.isHidden = false - self.cameraButton.isHidden = false self.cameraLabel.isHidden = false - self.microphoneButton.isHidden = false self.muteLabel.isHidden = false - self.messageButton.isHidden = false self.chatLabel.isHidden = false self.messageButton.isEnabled = (nil != self.contact) || (self.roomId.count > 0) - self.moreButton.isHidden = false self.moreLabel.isHidden = false self.moreButton.isEnabled = false - self.heldCallTitle.isHidden = true - self.heldCallDescription.isHidden = true - - self.activeCallTitle.isHidden = true - self.activeCallDescription.isHidden = true - - self.openCloseCallsButton.isHidden = true - self.openCloseCallsButton.isEnabled = false - switch self.callInProgressMode! { - + case .oneToOneAudio: - self.collectionView.isHidden = true self.myVideoView.isHidden = true self.otherVideoView.isHidden = true self.switchCameraButton.isHidden = true self.backgroundImage.isHidden = false self.backgroundImage.setImage(url: self.contact?.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) + self.view.bringSubview(toFront: self.backgroundImage) case .oneToOneVideo: - self.collectionView.isHidden = true - self.myVideoView.isHidden = false + self.switchCameraButton.isHidden = true + self.myVideoView.isHidden = true self.otherVideoView.isHidden = false - self.switchCameraButton.isHidden = false - self.backgroundImage.isHidden = true - self.view.insertSubview(self.myVideoView, aboveSubview:otherVideoView) - self.view.insertSubview(self.switchCameraButton, aboveSubview:otherVideoView) + self.backgroundImage.isHidden = false + self.backgroundImage.setImage(url: self.contact?.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) + self.view.bringSubview(toFront: self.backgroundImage) + self.view.bringSubview(toFront: self.otherVideoView) + self.view.bringSubview(toFront: self.myVideoView) + self.view.bringSubview(toFront: self.switchCameraButton) case .groupAudio: self.collectionView.isHidden = false self.myVideoView.isHidden = true self.otherVideoView.isHidden = true self.switchCameraButton.isHidden = true self.backgroundImage.isHidden = true + self.view.bringSubview(toFront: self.collectionView) } - + + self.view.bringSubview(toFront: self.bottomView) self.view.bringSubview(toFront: self.nameLabel) self.view.bringSubview(toFront: self.statusLabel) } - + func updateTitle() { - + var viewTitle:String = "" - + if let ctct = self.contact { - + viewTitle = "\(ctct.names ?? "") \(ctct.surnames ?? "")" - + } else if self.callInProgressMode == .groupAudio { if self.presenter != nil { if let room = RoomDAO.findRoom(by: self.roomId) { @@ -598,159 +620,157 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa } } } - + nameLabel.text = viewTitle } - + func setupUI() { - - updateModeVisibility() + updateTitle() } - + func updateCallBy(status: CallStatus) { - + } - + func callFailed() { } - + func remoteVideoStreamStopped() { - + // backgtoundVideoImage.isHidden = false // backgtoundVideoImage.image = backgtoundImage.image // // partnerVideoView.isHidden = true -// self.contentVideoView.bringSubview(toFront: self.yourVideoView) } - - + + @objc func resizeButtonAction() { presenter.messageAcion(with: roomId, isVideo: true) } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + // Note: need to prevent weird animation which appears when incoming/outcoming call appears. self.view.layoutIfNeeded() self.presenter.viewShowed() -// self.presenter.setupViews(myView: yourVideoView, remoteView: partnerVideoView) -// -// // Note: need to prevent weird animation with remote and local video views. -// self.partnerVideoView.layoutIfNeeded() -// self.yourVideoView.layoutIfNeeded() + + if self.needLocalRenderer { + self.didStartLocalCapturer() + self.needLocalRenderer = false + } + + if self.needRemoteRenderer { + self.didAddRemoteVideoStream() + self.needRemoteRenderer = false + } } - + override func initialize() { super.initialize() - + self.view.backgroundColor = Constants.colors.grayForCallScreenTop.getColor() - + self.presenter.willShow() - + setupVisibility() + + // mark view as initialized + self.initialized = true } - + //MARK: Collection View Delegates public func numberOfSections(in collectionView: UICollectionView) -> Int { - + return 1 } - + //2 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - + var rows:Int = dataSource.count - + if rows > 0 { rows = dataSource.count + 1 } - + return rows } - + //3 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - + let cell:UICollectionViewCell? - + if 0 == indexPath.row { - + let cellPlus:GroupAddParticipantsCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GroupAddParticipantsCollectionViewCell.self), for: indexPath) as! GroupAddParticipantsCollectionViewCell - + cellPlus.delegate = self - + cell = cellPlus } else { - + let cellPart:GroupCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GroupCollectionViewCell.self), for: indexPath) as! GroupCollectionViewCell - + let callPart:NYNCallParticipant? = dataSource[indexPath.row - 1] as? NYNCallParticipant - + if let cp = callPart { - + cellPart.callPart = cp cellPart.canRemove = self.moderator cellPart.delegate = self cellPart.updateCell() - + } else { - + print("Illegal cell") } - + cell = cellPart } - + return cell! } - + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - + let spacing : CGFloat = (collectionViewLayout as? UICollectionViewFlowLayout)?.minimumInteritemSpacing ?? 0.0 let offsetIn:CGFloat = 5.0 let cellHeight = floor((collectionView.bounds.size.height - ((CGFloat(expectedRowsInCollectionView) - 1)*spacing))/CGFloat(expectedRowsInCollectionView)) let cellWidth = floor((0.75*(cellHeight - 3*offsetIn) + 2*offsetIn)) - + return CGSize(width: cellWidth, height: cellHeight) } - + //MARK: Group Add Participants Collection View Cell delegates - + func didPressAddGroupCollectionCell(groupCollectionCell: GroupAddParticipantsCollectionViewCell) { presenter.updateCallParticipants() } - + //MARK: Group Collection View Cell delegates - + func showMenuWith(groupCollectionCell: GroupCollectionViewCell) { presenter.showMenuWith(groupCollectionCell:groupCollectionCell) } - + //MARK: bottom view delegate - + func acceptButtonPressed() { //presenter.acceptCall(withVideo: (self.callMode == .incamingVideoCall) || (self.callMode == .incamingVideoGroupCall)) } - + @objc func declineButtonPressed() { - + presenter.declineCall() } - + @objc func speakerButtonPressed() { - - let state = AudioManager.sharedInstance.speaker - switch state { - case .loud: - AudioManager.sharedInstance.speaker = .soft - case .soft: - AudioManager.sharedInstance.speaker = .loud - } presenter.speakerAction() } @@ -760,34 +780,34 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa } @objc func messageButtonPressed() { - + if let contact = self.contact { presenter.messageAcion(with: contact, isVideo: false) } else { - + presenter.messageAcion(with: roomId, isVideo: false) } - + } - + @objc func microphoneButtonPressed() { - + toggleMicrophone() } - + func toggleMicrophone() { - + let muteImage = (self.isMuted()) ? #imageLiteral(resourceName: "ic_unmute_voice_call") : #imageLiteral(resourceName: "ic_mute_voice_call") microphoneButton.setImage(muteImage, for: .normal) - + presenter.toggleMicrophone() } - + func isMuted()->Bool { - + return presenter.isMuted() } - + func acceptAudioButtonPressed() { // if self.callMode == .incamingVideoCall || self.callMode == .incamingVideoGroupCall { // presenter.acceptCall(withVideo: false) @@ -795,66 +815,71 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa // presenter.disableVideo() // } } - - func switchCameraButtonPressed() { + + @objc func switchCameraButtonPressed() { presenter.switchCamera() } - + func updateTime(text: String) { - statusLabel.text = "voice_call_status".localized + " - \(text)" + + if .oneToOneVideo == self.callInProgressMode { + statusLabel.text = "video_call_status".localized + " - \(text)" + } else { + statusLabel.text = "voice_call_status".localized + " - \(text)" + } } - + func changeUIToIncall() { - + self.updateCallBy(status: .callInProgress) self.setupUI() } - + func update(participants: [NYNCallParticipant]) { - + dataSource.removeAllObjects() - + for mem in participants { - + if let phoneId = mem.address { - + let contact:Contact? = ContactDAO.findContactBy(phoneId: phoneId) - + if let ct = contact { - + if let urlAvatar = ct.avatarUrl { - + mem.avatarUrl = urlAvatar.absoluteString } } } - + dataSource.add(mem) } - + collectionView.reloadData() } - + @objc func onCameraButtonPressed() { - + if cameraButton.imageView?.image == #imageLiteral(resourceName: "ic_video_on_voice_call") { cameraButton.setImage(#imageLiteral(resourceName: "ic_video_off_voice_call"), for: .normal) } else { cameraButton.setImage(#imageLiteral(resourceName: "ic_video_on_voice_call"), for: .normal) } } - + @objc func onMoreButtonPressed() { - - + + } - + @objc func onOpenCloseCallsButton() { - + self.openCloseCallsButton.snp.removeConstraints() UIView.animate(withDuration: 0.2, animations: { - + let alpha = self.collapsed ? 1.0 : 0.0 self.activeCallTitle.alpha = CGFloat(alpha) self.activeCallDescription.alpha = CGFloat(alpha) @@ -862,14 +887,14 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.heldCallDescription.alpha = CGFloat(alpha) if self.collapsed { - + self.openCloseCallsButton.snp.makeConstraints({ (make) in make.height.equalTo(34) make.width.equalTo(34) make.centerX.equalTo(self.view) make.bottom.equalTo(self.activeCallTitle.snp.top).offset(-self.offset/2) }) - + } else { self.openCloseCallsButton.snp.makeConstraints({ (make) in @@ -879,7 +904,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa make.bottom.equalTo(self.microphoneButton.snp.top).offset(-self.offset/2) }) } - + self.collapsed = !self.collapsed let img = self.collapsed ? UIImage.init(named: "arrow_expand") : UIImage.init(named: "arrow_collapse") self.openCloseCallsButton.setImage(img, for: .normal) @@ -887,20 +912,20 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.view.layoutIfNeeded() }, completion: { _ in - + }) } - + func onPortOutButtonPressed() { let alertVC = UIAlertController(title: "voice_call_the_feature_currently_unavailable".localized, message: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: "ok".localized, style: .cancel) { (action) in alertVC.dismiss(animated: true, completion: nil) } alertVC.addAction(okAction) - + self.present(alertVC, animated: true, completion: nil) } - + func askEndOrLeave() { let alertVC = UIAlertController(title: "question_end_call".localized, message: nil, preferredStyle: .alert) let forMeAction = UIAlertAction(title: "end_call_for_me".localized, style: .default) { (action) in @@ -909,10 +934,63 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa let forAllAction = UIAlertAction(title: "end_call_for_all".localized, style: .default) { (action) in self.presenter.endCall() } - + alertVC.addAction(forMeAction) alertVC.addAction(forAllAction) - + self.present(alertVC, animated: true, completion: nil) } + + func didAddRemoteVideoStream() { + if self.initialized { + handleDidAddRemoteVideoStream() + } else { + self.needRemoteRenderer = true + } + } + + func didStartLocalCapturer() { + if self.initialized { + handleDidStartLocalCapturer() + } else { + self.needLocalRenderer = true + } + } + + func didStopLocalCapturer() { + if self.initialized { + handleDidStopLocalCapturer() + } else { + self.needLocalRenderer = false + } + } + + private func handleDidAddRemoteVideoStream() { + + self.otherVideoView.isHidden = false + self.switchCameraButton.isHidden = false + self.backgroundImage.isHidden = true + self.backgroundImage.image = nil + self.view.sendSubview(toBack: self.backgroundImage) + + self.presenter.addRemoteVideoRenderer(inView: self.otherVideoView); + } + + private func handleDidStartLocalCapturer() { + + self.myVideoView.isHidden = false + self.view.insertSubview(self.myVideoView, aboveSubview: self.otherVideoView) + + self.presenter.attachLocalVideoPreview(inView: self.myVideoView); + } + + private func handleDidStopLocalCapturer() { + + self.myVideoView.isHidden = true + self.view.sendSubview(toBack: self.myVideoView) + + self.presenter.dettachLocalVideoPreview(inView: self.myVideoView); + } + + } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift index 2bc66b6da..53c92b659 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift @@ -45,6 +45,7 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: true) + interactor.setupDelegate() } func presentDialInCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, contact: Contact?, call: NYNCall? = nil, main: MainWireFrame?) { @@ -71,6 +72,7 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: true) + interactor.setupDelegate() } func presentCreateGroupCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, main: MainWireFrame?, call: NYNCall) { @@ -100,6 +102,7 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: true) + interactor.setupDelegate() } func messageActionWith(room: Room, isVideo: Bool) { @@ -134,7 +137,7 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { } } - AddParticipantsWireFrame().presentAddParticipants(navigation: navigation!, main: mainWF, selectedContacts: nil, delegate: external, mode: .updateGroupCall, members: members, room:self.view?.presenter.room) + AddParticipantsWireFrame().presentAddParticipants(navigation: navigation!, main: mainWF, selectedContacts: nil, delegate: external, mode: .updateGroupCall, members: members, room:self.view?.presenter.interactor.room) } func showMenuWith(participant: NYNCallParticipant?, delegate: ManageCallInProgressParticipantsProtocol) { @@ -151,20 +154,6 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { })) } - // if participant.isMuted { - // actionSheet.addAction(UIAlertAction(title: "mute_call".localized, - // style: .default, - // handler: { (action) in - // delegate.mute(participant: participant) - // })) - // } else { - // actionSheet.addAction(UIAlertAction(title: "unmute".localized, - // style: .default, - // handler: { (action) in - // delegate.unmute(participant: participant) - // })) - // } - actionSheet.addAction(UIAlertAction(title: "cancel".localized, style: .cancel, handler: { (action) in self.navigation?.dismiss(animated: true, completion: nil) @@ -172,7 +161,12 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { navigation?.present(actionSheet, animated: true, completion: nil) } - + + func callClosed() { + LogService.log(topic: .callSystem, text: "call view popped to root") + navigation?.popViewController(animated: true) + } + func removeParticipant(participant:NYNCallParticipant?) { } diff --git a/Nynja/Modules/Call/Interactor/CallInteractor.swift b/Nynja/Modules/Call/Interactor/CallInteractor.swift deleted file mode 100644 index 7f93f5a5a..000000000 --- a/Nynja/Modules/Call/Interactor/CallInteractor.swift +++ /dev/null @@ -1,300 +0,0 @@ -// -// CallCallInteractor.swift -// Nynja -// -// Created by Bohdan Paliychuk on 26/07/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// -import VoxImplant - -class CallInteractor: CallInteractorInputProtocol, VoxServiceDelegate, NynjaCommunicatorServiceDelegate { - - weak var presenter: CallInteractorOutputProtocol! - weak var call: VICall? - - var vox = VoxService.sharedInstance - var timer: Timer? - - var contact: Contact? { - guard let voxId = call?.voxId else { return nil } - return ContactDAO.findContactBy(voxId: voxId) - } - weak var nynCall: NYNCall? - - var duration = 0 - - - func setupDelegate() { - vox.delegate = self - NynjaCommunicatorService.sharedInstance.delegate = self - } - - func acceptCall(withVideo: Bool) { - if let id = call { - vox.answer(call: id, withVideo: withVideo) - } else if let ncall = self.nynCall { - NynjaCommunicatorService.sharedInstance.acceptConference(call: ncall) - } - } - - func rejectCall() { - - if let nc = self.nynCall { - NynjaCommunicatorService.sharedInstance.rejectConference(call: nc) - - self.presenter.callClosed() - } - } - - func declineCall() { - if let id = call { - vox.cancelCall(call: id) - } else if let nc = self.nynCall { - if nc.callState == NYNCallState.connected { - nc.hangup() - } else { - self.presenter.callClosed() - } - } else { - self.presenter.callClosed() - } - } - - func speakerAction() { - vox.switchSpeaker() - } - - func disableVideo() { - if let id = call { - vox.disableVideo(call: id) - if id.duration() > 0 { - self.presenter.callConnected(withVideo: false) - } else { - self.presenter.setRingingWithoutVideo() - } - vox.withVideo = false - } - } - - func startRinging() { - presenter.setRingingStatus() - } - - func microphoneAction() { - if let id = call { - vox.switchMicrophone(call: id) - } - if let nc = self.nynCall { - nc.toggleMicrophone() - } - } - - func toggleMicrophone() { - - if let nc = self.nynCall { - nc.toggleMicrophone() - } - } - - func isMuted()->Bool { - - var muted:Bool = false - - if let nc = self.nynCall { - muted = nc.isMuted - } - - return muted - } - - func switchCamera() { - vox.switchCamera() - } - - // MARK: - VoxServiceDelegate - func remoteVideoStreamDeleted () { - presenter.remoteVideoStreamStopped() - } - - func callClosed(call: VICall, isError: Bool) { - self.presenter.callClosed() - } - - func callConnected(call: VICall,withVideo: Bool) { - self.presenter.callConnected(withVideo: withVideo) - self.startTimer() - } - - func setupViews(myView: UIView, remoteView: UIView) { - vox.remoteView = remoteView - vox.myView = myView - } - - func startTimer() { - timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) - } - - @objc func runTimedCode() { - if let call = self.call { - let durationInt = Int(call.duration()) - let minutes = durationInt / 60 - let seconds = durationInt % 60 - - let text = String.localizedStringWithFormat("%u:%02u", minutes,seconds) - if self.presenter != nil { - self.presenter.updateTime(text:text) - } - } else if nil != self.nynCall { - - duration += 1 - let durationInt = Int(duration) - let minutes = durationInt / 60 - let seconds = durationInt % 60 - - let text = String.localizedStringWithFormat("%u:%02u", minutes,seconds) - if self.presenter != nil { - self.presenter.updateTime(text:text) - } - } - } - - func dialing(call: NYNCall) { - - } - - func incomingCallRinging(call: NYNCall) { - - } - - func creatingGroupCall(name: String, call: NYNCall) { - - } - - func stopTimer () { - - if timer != nil { - timer?.invalidate() - timer = nil - duration = 0 - } - } - - func callEnded(call: NYNCall, isError: Bool) { - self.presenter.callClosed() - - stopTimer() - } - - func readyToStart(call:NYNCall) { - - call.start() - - } - - func stateDidChange(call: NYNCall, state: NYNCallState) { - - if let ncall = self.nynCall { - if ncall.callId.elementsEqual(call.callId) { - switch state { - case NYNCallState.new: - break - case NYNCallState.readyToStart: - self.readyToStart(call: ncall) - break; - case NYNCallState.establishing: - break - case NYNCallState.connecting: - break - case NYNCallState.connected: - self.presenter.callConnected(withVideo: false) - self.startTimer() - break - case NYNCallState.failed: - self.presenter.callFailed() - break - case NYNCallState.disconnected: - break - case NYNCallState.closed: - break - case NYNCallState.count: - break - } - } - } - } - - func participantsUpdated(call: NYNCall) { - - if let ncall = self.nynCall, call.callId.elementsEqual(ncall.callId) { - self.presenter.update(participants: ncall.participants) - } - } - - func updateGroupCall(contacts: [Contact]) { - - if let ncall = self.nynCall { - - for ctc in contacts { - let name = "\(ctc.names ?? "") \(ctc.surnames ?? "")" - - NynjaCommunicatorService.sharedInstance.addConferenceMember(conferenceId: ncall.callId, phoneId: ctc.phone_id!, name: name) - } - } - } - - func removeCallMember(memberId: String) { - if let ncall = self.nynCall { - NynjaCommunicatorService.sharedInstance.removeConferenceMember(conferenceId: ncall.callId, memberId: memberId) - } - } - - func endCall() { - if let nc = self.nynCall { - nc.end() - } - } - - //MARK: Should remove to serverside - - let storageService: StorageService = .sharedInstance - let payloadBuilder: MessagePayloadBuilderInput = MessagePayloadBuilder() - private let mqttService: MQTTService = .sharedInstance - let processingManager = DefaultMessagesProcessingManager.shared - - private(set) lazy var messageFactory: MessageFactoryProtocol = { - let factory = MessageFactory() - - let dependencies = MessageFactory.Dependencies( - storageService: storageService, - payloadBuilder: payloadBuilder - ) - factory.inject(dependencies: dependencies) - - return factory - }() - - private(set) lazy var messageSendingService: MessageSendingServiceProtocol = { - let service = MessageSendingService() - let dependencies = MessageSendingService.Dependencies( - mqttService: mqttService, - storageService: storageService, - processingManager: processingManager - ) - service.inject(dependencies: dependencies) - - return service - }() - - func conferenceCreated(call: NYNCall) { - sendCall(ncall: call) - } - - func sendCall(ncall: NYNCall) { - let room = Room() - room.id = ncall.externalInfo - let membersIds = ncall.participants.map({ $0.address ?? "" }) - let message = messageFactory.makeCallMessage(members: membersIds, room: room) - try? storageService.perform(action: .save, with: message) - messageSendingService.sendMessage(message) - } -} diff --git a/Nynja/Modules/Call/Presenter/CallPresenter.swift b/Nynja/Modules/Call/Presenter/CallPresenter.swift deleted file mode 100644 index 546b0c7d7..000000000 --- a/Nynja/Modules/Call/Presenter/CallPresenter.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// CallCallPresenter.swift -// Nynja -// -// Created by Bohdan Paliychuk on 26/07/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -class CallPresenter: CallPresenterProtocol, CallInteractorOutputProtocol, EditParticipantsDelegate, ManageCallParticipantsProtocol { - - weak var view: CallViewProtocol! - var interactor: CallInteractorInputProtocol! - var wireFrame: CallWireFrameProtocol! - var contact: Contact? - var type: CallMode! - var room: Room? - - func acceptCall(withVideo: Bool) { - interactor.acceptCall(withVideo: withVideo) - } - - func disableVideo() { - interactor.disableVideo() - } - - func declineCall() { - interactor.declineCall() - } - - func rejectCall() { - interactor.rejectCall(); - } - - func updateCallParticipants() { - - wireFrame.updateCallParticipants() - } - - func showMenuWith(groupCollectionCell: GroupCollectionViewCell) { - - wireFrame.showMenuWith(participant: groupCollectionCell.callPart, delegate: self) - } - - func endCall() { - interactor.endCall() - } - - func speakerAction() { - interactor.speakerAction() - } - - func messageAcion(with roomId:String, isVideo: Bool) { - guard let room = RoomDAO.findRoom(by: roomId) else {return} - wireFrame.messageActionWith(room:room, isVideo:isVideo) - } - - func microphoneAction() { - interactor.microphoneAction() - } - - func toggleMicrophone() { - - interactor.toggleMicrophone() - } - - func isMuted()->Bool { - - return interactor.isMuted() - } - - func switchCamera() { - interactor.switchCamera() - } - - func callClosed() { - if let navigation = (self.view as? UIViewController)?.navigationController { - (navigation.parent as? MainViewProtocol)?.presenter.wireFrame.hideReturnToCallView() - navigation.popViewController(animated: true) - } - } - - func callConnected(withVideo: Bool) { - if withVideo { - self.view.setupUI() - } else { - self.view.setupUI() - } - - self.view.updateCallBy(status:.callInProgress) - } - - func callFailed() { - - self.view.callFailed() - } - - func remoteVideoStreamStopped() { - //self.view.remoteVideoStreamStopped() - self.view.setupUI() - self.view.updateCallBy(status:.callInProgress) - } - - - func setRingingWithoutVideo() { - self.view.setupUI() - self.view.updateCallBy(status:.callStarting) - } - - func setRingingStatus() { - self.view.setupUI() - self.view.updateCallBy(status:.callOutgoingAudio) - } - - func willShow() { - self.view.setupUI() - } - - func setupViews(myView: UIView, remoteView: UIView) { - self.interactor.setupViews(myView: myView, remoteView: remoteView) - } - - func updateTime(text: String) { - self.view.updateTime(text: text) - } - - func update(participants: [NYNCallParticipant]) { - - self.view.update(participants: participants) - } - - func viewShowed() { - self.interactor.setupDelegate() - } - - // MARK: EditParticpantsDelegate - - func updateGroupCall(contacts: [Contact]) { - - self.interactor.updateGroupCall(contacts:contacts) - } - - func participantsUpdated(contacts: [Contact]) { - - } - - func participantsUpdated(result: ParticipantsResult){ - - switch result { - case let .updateGroupCall(contacts): - updateGroupCall(contacts: contacts) - LogService.log(topic: .callSystem, text: "updateGroupCall") - default: - break - } - } - - // MARK: ManageCallParticipantsProtocol - func remove(participant: NYNCallParticipant?) { - interactor.removeCallMember(memberId: (participant?.memberId)!) - } - - func mute(participant: NYNCallParticipant?) { - - } - - func unmute(participant: NYNCallParticipant?) { - - } - -} diff --git a/Nynja/Modules/Call/View/CallViewController.swift b/Nynja/Modules/Call/View/CallViewController.swift deleted file mode 100644 index 55e62a488..000000000 --- a/Nynja/Modules/Call/View/CallViewController.swift +++ /dev/null @@ -1,655 +0,0 @@ -// -// CallCallViewController.swift -// Nynja -// -// Created by Bohdan Paliychuk on 26/07/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -enum CallMode { - case incamingCall - case incamingGroupCall - case outGoingCall - case outGoingGroupCall - case incamingVideoCall - case incamingVideoGroupCall - case outGoingVideoCall - case outGoingVideoGroupCall -} - -enum CallStatus { - case callStarting - case callIncomingAudio - case callIncomingVideo - case callOutgoingAudio - case callOutgoingVideo - case callConnecting - case callInProgress -} - -import UIKit - -class CallViewController: BaseVC, CallViewProtocol, BottomCallViewProtocol, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, GroupCollectionViewCellDelegate, GroupAddParticipantsCollectionViewCellDelegate { - - - var presenter: CallPresenterProtocol! - var contact: Contact? - var callMode: CallMode! - var callStatus: CallStatus! - var dataSource: NSMutableArray = [] - var moderator: Bool = false - var roomId: String = "" - - let bottomViewHeight: Float = 200.0 - let labelNameHeight: Float = 40.0 - let labelStatusHeight: Float = 30.0 - let offset: Float = 10.0 - let middleGVHeight: Float = 100.0 - let statusBarHeight: Float = Float(UIApplication.shared.statusBarFrame.size.height) - - lazy var expectedRowsInCollectionView: Int = { - - var sections:Int = 0 - - //Check which iPhone it is - let screenHeight:Float = Float(UIScreen.main.bounds.size.height) - - let clearSpace:Float = screenHeight - (bottomViewHeight + labelNameHeight + labelStatusHeight + statusBarHeight + 5*offset/*top offset + vertical spacing controlls offset*/) - - if 0 > clearSpace { - - sections = 3 - - } else { - - let minSectionHeight:Float = 83.0 - - sections = Int(floor(clearSpace/minSectionHeight)) - } - - return sections - }() - - lazy var contentView: UIView = { - let content = UIView() - - if (self.callMode == .outGoingGroupCall || self.callMode == .outGoingVideoGroupCall || self.callMode == .incamingVideoGroupCall || self.callMode == .incamingGroupCall) { - content.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() - } else { - content.backgroundColor = .clear - } - - self.view.addSubview(content) - content.snp.makeConstraints({ (make) in - make.top.left.right.bottom.equalTo(self.view) - }) - - return content - }() - - lazy var backgtoundImage: UIImageView = { - - let img = UIImageView() - img.isUserInteractionEnabled = true - img.contentMode = .scaleAspectFill - let height = UIScreen.main.bounds.height * 0.6 - self.contentView.addSubview(img) - - img.snp.makeConstraints({ (make) in - make.top.left.right.equalTo(self.view) - make.bottom.equalTo(self.bottomView).offset(-bottomViewHeight) - }) - return img - }() - - lazy var collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 0 - let cv = GroupCollectionView(frame: .zero, collectionViewLayout: layout) - cv.isUserInteractionEnabled = true - self.contentView.addSubview(cv) - cv.delegate = self - cv.dataSource = self - cv.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() - cv.register(GroupCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: GroupCollectionViewCell.self)) - cv.register(GroupAddParticipantsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: GroupAddParticipantsCollectionViewCell.self)) - - cv.snp.makeConstraints({ (make) in - make.top.equalTo(self.statusLabel.snp.bottom).offset(offset) - make.left.right.equalTo(self.contentView) - make.bottom.equalTo(self.bottomView.snp.top) - - }) - return cv - }() - - private lazy var nameLabel: UILabel = { - let scwidth = UIScreen.main.bounds.width - let lbl = UILabel() - lbl.textAlignment = .center - lbl.font = UIFont(name: Constants.fonts.medium, size: 22.0) - lbl.textColor = Constants.colors.white.getColor() - lbl.numberOfLines = 1 -// lbl.adjustsFontSizeToFitWidth = true - lbl.baselineAdjustment = .alignCenters - lbl.lineBreakMode = .byTruncatingTail - self.contentView.addSubview(lbl) - - if (self.callMode == .outGoingGroupCall || self.callMode == .outGoingVideoGroupCall || self.callMode == .incamingVideoGroupCall || self.callMode == .incamingGroupCall) { - - lbl.snp.makeConstraints({ (make) in - make.top.equalTo(self.view).offset(3*offset) - make.centerX.equalTo(self.view) - make.height.equalTo(labelNameHeight) - }) - - } else { - - lbl.snp.makeConstraints({ (make) in - make.centerX.equalTo(self.view) - make.centerY.equalTo(self.view) - make.height.equalTo(40.0) - }) - } - - return lbl - }() - - private lazy var statusLabel: UILabel = { - let scwidth = UIScreen.main.bounds.width - let lbl = UILabel() - lbl.textAlignment = .center - lbl.font = UIFont(name: Constants.fonts.regular, size: 16.0) - lbl.textColor = Constants.colors.white.getColor() - lbl.backgroundColor = .clear - lbl.numberOfLines = 1 - lbl.adjustsFontSizeToFitWidth = true - lbl.baselineAdjustment = .alignCenters - lbl.lineBreakMode = .byClipping - self.contentView.addSubview(lbl) - - lbl.snp.makeConstraints({ (make) in - make.top.equalTo(self.nameLabel.snp.bottom).offset(offset) - make.left.right.equalTo(self.contentView) - make.height.equalTo(labelStatusHeight) - }) - - return lbl - }() - - - lazy var bottomView : BottomCallView = { - let lv = BottomCallView() - lv.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() - lv.delegate = self - let height = bottomViewHeight//UIScreen.main.bounds.width / 1.4 - - self.contentView.addSubview(lv) - lv.snp.makeConstraints({ (make) in - make.left.right.equalTo(self.contentView) - make.bottom.equalTo(self.contentView) - make.height.equalTo(height) - }) - - return lv - }() - - lazy var resizeVideoViewsButton : UIButton = { - let btn = UIButton() - let img = #imageLiteral(resourceName: "minimaze") - btn.setBackgroundImage(img, for: .normal) - let width = UIScreen.main.bounds.width * 0.088 - let height = UIScreen.main.bounds.width * 0.09 - btn.layer.masksToBounds = true - self.contentView.addSubview(btn) - let topPadding = UIScreen.main.bounds.width * 0.078 - let leftPadding = UIScreen.main.bounds.width * 0.037 - btn.addTarget(self, action: #selector(resizeButtonAction), for: .touchUpInside) - - btn.snp.makeConstraints({ (make) in - make.width.equalTo(width) - make.height.equalTo(height) - make.left.equalTo(self.contentView.snp.left).offset(leftPadding) - make.top.equalTo(self.contentView).offset(topPadding) - }) - return btn - }() - - lazy var yourVideoView: UIView = { - let content = UIView() - content.backgroundColor = .clear - self.contentVideoView.addSubview(content) - - let width = UIScreen.main.bounds.width * 0.256 - let height = UIScreen.main.bounds.width * 0.45 - let topPadding = UIScreen.main.bounds.width * 0.063 - let rightPadding = UIScreen.main.bounds.width * 0.016 - - content.snp.makeConstraints({ (make) in - make.width.equalTo(width) - make.height.equalTo(height) - make.right.equalTo(self.contentVideoView).offset(-rightPadding) - make.top.equalTo(self.contentVideoView).offset(topPadding) - }) - - return content - }() - - lazy var partnerVideoView: UIView = { - let content = UIView() - content.backgroundColor = .clear - self.contentVideoView.addSubview(content) - content.snp.makeConstraints({ (make) in - make.top.left.right.bottom.equalTo(self.contentVideoView) - }) - - return content - }() - - lazy var contentVideoView: UIView = { - let content = UIView() - content.backgroundColor = .clear - self.view.addSubview(content) - content.snp.makeConstraints({ (make) in - make.top.left.right.bottom.equalTo(self.view) - }) - - return content - }() - - lazy var backgtoundVideoImage: UIImageView = { - let img = UIImageView() - img.isUserInteractionEnabled = true - img.contentMode = .scaleAspectFit - self.contentVideoView.addSubview(img) - - img.snp.makeConstraints({ (make) in - make.top.left.right.bottom.equalTo(self.contentVideoView) - }) - return img - }() - - lazy var middleGradientView: GradientView = { - let view = GradientView(colors: [Constants.colors.grayForCallScreenTop.getColor(withAlpha: 0), Constants.colors.grayForCallScreenBottom.getColor()]) - self.bottomView.addSubview(view) - - view.snp.makeConstraints({ (make) in - make.top.equalTo(self.bottomView).offset(-middleGVHeight) - make.trailing.equalTo(self.view) - make.leading.equalTo(self.view) - make.height.equalTo(middleGVHeight) - }) - - return view - }() - - func setupUI() { - - self.contentView.isHidden = false - backImage.isHidden = true - - if (self.callMode == .outGoingGroupCall || self.callMode == .outGoingVideoGroupCall || self.callMode == .incamingVideoGroupCall || self.callMode == .incamingGroupCall) { - - self.middleGradientView.isHidden = true - self.backgtoundImage.isHidden = true - self.collectionView.isHidden = false - - } else { - - self.middleGradientView.isHidden = false - self.backgtoundImage.isHidden = false - self.collectionView.isHidden = true - - self.backgtoundImage.setImage(url: self.contact?.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) - } - - self.middleGradientView.isHidden = false - - var viewTitle:String = "" - - if let ctct = self.contact { - viewTitle = "\(ctct.names ?? "") \(ctct.surnames ?? "")" - } - - viewTitle = "call_incoming_audio_conference".localized - - if self.callMode == .outGoingVideoGroupCall || self.callMode == .outGoingGroupCall { - if self.presenter != nil { - if let room = RoomDAO.findRoom(by: self.roomId) { - if let rn = room.name { - viewTitle = rn - } - } - } - } - - nameLabel.text = viewTitle - - self.bottomView.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() - contentVideoView.isHidden = true - partnerVideoView.isHidden = true - switch self.callMode! { - case .incamingCall, .incamingGroupCall: - statusLabel.text = "call_incoming".localized - case .outGoingCall, .outGoingVideoCall, .outGoingGroupCall, .outGoingVideoGroupCall: - statusLabel.text = "call_connecting".localized - case .incamingVideoCall, .incamingVideoGroupCall: - statusLabel.text = "call_incoming_video".localized - } - - bottomView.setupCallView(withMode: self.callMode) - self.presenter.setupViews(myView: yourVideoView, remoteView: partnerVideoView) - } - - func updateCallBy(status: CallStatus) { - - self.callStatus = status - - switch self.callStatus { - case .callStarting: - statusLabel.text = "call_connecting".localized - break - case .callIncomingAudio: - statusLabel.text = "call_incoming".localized - break - case .callIncomingVideo: - self.bottomView.inCallVideo() - self.middleGradientView.isHidden = true - bottomView.backgroundColor = .clear - let hidden:Bool = (self.callMode == .outGoingGroupCall || self.callMode == .outGoingVideoGroupCall || self.callMode == .incamingVideoGroupCall || self.callMode == .incamingGroupCall) - backgtoundImage.isHidden = hidden - middleGradientView.isHidden = hidden - collectionView.isHidden = !hidden - statusLabel.isHidden = true - nameLabel.isHidden = true - resizeVideoViewsButton.isHidden = false - contentVideoView.isHidden = false - partnerVideoView.isHidden = false - contentView.bringSubview(toFront: yourVideoView) - self.view.bringSubview(toFront: contentView) - break - case .callOutgoingAudio: - statusLabel.text = "call_ringing".localized - self.bottomView.outGoingVideoCall() - break - case .callOutgoingVideo: - break - case .callConnecting: - break - case .callInProgress: - self.bottomView.inCall() - statusLabel.text = "voice_call_status".localized + " - 00:00" - let hidden:Bool = (self.callMode == .outGoingGroupCall || self.callMode == .outGoingVideoGroupCall || self.callMode == .incamingVideoGroupCall || self.callMode == .incamingGroupCall) - backgtoundImage.isHidden = hidden - middleGradientView.isHidden = hidden - collectionView.isHidden = !hidden - statusLabel.isHidden = false - nameLabel.isHidden = false - resizeVideoViewsButton.isHidden = true - contentVideoView.isHidden = true - partnerVideoView.isHidden = true - break - - default: break - - } - } - - func callFailed() { - self.bottomView.callFailed() - - statusLabel.text = "call_failed".localized - backgtoundImage.isHidden = true - middleGradientView.isHidden = true - collectionView.isHidden = true - statusLabel.isHidden = false - nameLabel.isHidden = false - } - - func remoteVideoStreamStopped() { - - backgtoundVideoImage.isHidden = false - backgtoundVideoImage.image = backgtoundImage.image - - partnerVideoView.isHidden = true - self.contentVideoView.bringSubview(toFront: self.yourVideoView) - } - - - @objc func resizeButtonAction() { - presenter.messageAcion(with: roomId, isVideo: true) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Note: need to prevent weird animation which appears when incoming/outcoming call appears. - self.view.layoutIfNeeded() - - self.presenter.viewShowed() - self.presenter.setupViews(myView: yourVideoView, remoteView: partnerVideoView) - - // Note: need to prevent weird animation with remote and local video views. - self.partnerVideoView.layoutIfNeeded() - self.yourVideoView.layoutIfNeeded() - } - - override func initialize() { - super.initialize() - - self.view.backgroundColor = Constants.colors.grayForCallScreenTop.getColor() - - self.presenter.willShow() - } - - //MARK: Collection View Delegates - public func numberOfSections(in collectionView: UICollectionView) -> Int { - - return 1 - } - - //2 - func collectionView(_ collectionView: UICollectionView, - numberOfItemsInSection section: Int) -> Int { - - var rows:Int = dataSource.count - - if rows > 0 { - rows = dataSource.count + 1 - } - - return rows - } - - //3 - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - let cell:UICollectionViewCell? - - if 0 == indexPath.row { - - let cellPlus:GroupAddParticipantsCollectionViewCell = - collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GroupAddParticipantsCollectionViewCell.self), for: indexPath) as! GroupAddParticipantsCollectionViewCell - - cellPlus.delegate = self - - cell = cellPlus - } else { - - let cellPart:GroupCollectionViewCell = - collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GroupCollectionViewCell.self), for: indexPath) as! GroupCollectionViewCell - - let callPart:NYNCallParticipant? = dataSource[indexPath.row - 1] as? NYNCallParticipant - - if let cp = callPart { - - cellPart.callPart = cp - cellPart.canRemove = self.moderator - cellPart.delegate = self - cellPart.updateCell() - - } else { - LogService.log(topic: .callSystem, text: "Illegal cell") - } - - cell = cellPart - } - - return cell! - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - - let spacing : CGFloat = (collectionViewLayout as? UICollectionViewFlowLayout)?.minimumInteritemSpacing ?? 0.0 - let offsetIn:CGFloat = 5.0 - let cellHeight = floor((collectionView.bounds.size.height - ((CGFloat(expectedRowsInCollectionView) - 1)*spacing))/CGFloat(expectedRowsInCollectionView)) - let cellWidth = floor((0.75*(cellHeight - 3*offsetIn) + 2*offsetIn)) - - return CGSize(width: cellWidth, height: cellHeight) - } - - //MARK: Group Add Participants Collection View Cell delegates - - func didPressAddGroupCollectionCell(groupCollectionCell: GroupAddParticipantsCollectionViewCell) { - presenter.updateCallParticipants() - } - - //MARK: Group Collection View Cell delegates - - func showMenuWith(groupCollectionCell: GroupCollectionViewCell) { - presenter.showMenuWith(groupCollectionCell:groupCollectionCell) - } - - //MARK: bottom view delegate - - func acceptButtonPressed() { - presenter.acceptCall(withVideo: (self.callMode == .incamingVideoCall) || (self.callMode == .incamingVideoGroupCall)) - } - - func declineButtonPressed() { - - if self.callMode == .incamingVideoGroupCall || self.callMode == .incamingGroupCall { - if self.callStatus == .callInProgress { - if self.moderator { - askEndOrLeave() - } else { - presenter.declineCall() - } - } else { - presenter.rejectCall() - } - } else { - if self.moderator { - askEndOrLeave() - } else { - presenter.declineCall() - } - } - } - - func speakerButtonPressed() { - presenter.speakerAction() - } - - func messageButtonPressed() { - presenter.messageAcion(with: roomId, isVideo: false) - } - - func microphoneButtonPressed() { - presenter.microphoneAction() - } - - func toggleMicrophone() { - - presenter.toggleMicrophone() - } - - func isMuted()->Bool { - - return presenter.isMuted() - } - - func acceptAudioButtonPressed() { - if self.callMode == .incamingVideoCall || self.callMode == .incamingVideoGroupCall { - presenter.acceptCall(withVideo: false) - } else { - presenter.disableVideo() - } - } - - func switchCameraButtonPressed() { - presenter.switchCamera() - } - - func updateTime(text: String) { - statusLabel.text = "voice_call_status".localized + " - \(text)" - bottomView.timerLabel.text = text - } - - func changeUIToIncall() { - - self.updateCallBy(status: .callInProgress) - self.setupUI() - } - - func update(participants: [NYNCallParticipant]) { - - dataSource.removeAllObjects() - - for mem in participants { - - if let phoneId = mem.address { - - let contact:Contact? = ContactDAO.findContactBy(phoneId: phoneId) - - if let ct = contact { - - if let urlAvatar = ct.avatarUrl { - - mem.avatarUrl = urlAvatar.absoluteString - } - } - } - - dataSource.add(mem) - } - - collectionView.reloadData() - } - - func onCameraButtonPressed() { - - } - - func onMoreButtonPressed() { - - } - - func onPortOutButtonPressed() { - let alertVC = UIAlertController(title: "voice_call_the_feature_currently_unavailable".localized, message: nil, preferredStyle: .alert) - let okAction = UIAlertAction(title: "ok".localized, style: .cancel) { (action) in - alertVC.dismiss(animated: true, completion: nil) - } - alertVC.addAction(okAction) - - self.present(alertVC, animated: true, completion: nil) - } - - func askEndOrLeave() { - let alertVC = UIAlertController(title: "question_end_call".localized, message: nil, preferredStyle: .alert) - let forMeAction = UIAlertAction(title: "end_call_for_me".localized, style: .default) { (action) in - self.presenter.declineCall() - } - let forAllAction = UIAlertAction(title: "end_call_for_all".localized, style: .default) { (action) in - self.presenter.endCall() - } - - alertVC.addAction(forMeAction) - alertVC.addAction(forAllAction) - - self.present(alertVC, animated: true, completion: nil) - } - -} diff --git a/Nynja/Modules/Call/WireFrame/CallWireframe.swift b/Nynja/Modules/Call/WireFrame/CallWireframe.swift deleted file mode 100644 index 6de6c4e3b..000000000 --- a/Nynja/Modules/Call/WireFrame/CallWireframe.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// CallCallWireframe.swift -// Nynja -// -// Created by Bohdan Paliychuk on 26/07/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit -import VoxImplant - -class CallWireFrame: CallWireFrameProtocol { - - weak var navigation : UINavigationController? - weak var mainWF: MainWireFrame? - weak var call: VICall? - weak var view: CallViewProtocol! - weak var nynCall: NYNCall? - weak var external: EditParticipantsDelegate? - - func presentCall(navigation: UINavigationController, callMode: CallMode, contact: Contact, call: VICall? = nil, main: MainWireFrame?) { - - self.navigation = navigation - self.mainWF = main - - let view = CallViewController() - let presenter = CallPresenter() - let interactor = CallInteractor() - - interactor.call = call - - view.callMode = callMode - - self.view = view - main?.callVC = view - - presenter.type = callMode - presenter.contact = contact - - // Connecting - view.presenter = presenter - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - - navigation.pushViewController(view as UIViewController, animated: true) - } - - func presentDialInCall(navigation: UINavigationController, callMode: CallMode, call: NYNCall? = nil, main: MainWireFrame?) { - - let view = CallViewController() - let presenter = CallPresenter() - let interactor = CallInteractor() - interactor.nynCall = call - self.navigation = navigation - view.callMode = callMode - self.nynCall = call - self.view = view - self.mainWF = main - main?.callVC = view - - presenter.type = callMode - // Connecting - view.presenter = presenter - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - - navigation.pushViewController(view as UIViewController, animated: true) - } - - func presentCreateGroupCall(navigation: UINavigationController, callMode: CallMode, main: MainWireFrame?, call: NYNCall) { - - let view = CallViewController() - let presenter = CallPresenter() - let interactor = CallInteractor() - - self.external = presenter - - interactor.nynCall = call - self.navigation = navigation - view.callMode = callMode - view.moderator = call.isModerator - view.roomId = call.externalInfo - self.nynCall = call - self.view = view - self.mainWF = main - main?.callVC = view - - presenter.type = callMode - // Connecting - view.presenter = presenter - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - - navigation.pushViewController(view as UIViewController, animated: true) - } - - func messageActionWith(room: Room, isVideo: Bool) { - - if self.nynCall != nil { - self.navigation?.popViewController(animated: false) - self.navigation?.view.layoutIfNeeded() - (navigation?.viewControllers.last as? MainViewProtocol)?.presenter.showMessages(room: room, call: self.nynCall!, callVC: self.view!, isVideo: isVideo) - } - } - - func messageAction(isVideo: Bool, contact: Contact) { - self.navigation?.popViewController(animated: false) - self.navigation?.view.layoutIfNeeded() - (navigation?.viewControllers.last as? MainViewProtocol)?.presenter.showMessages(contact: contact, call: self.nynCall!, callVC: self.view!, isVideo: isVideo) - } - - func updateCallParticipants() { - - var members:[Member] = [] - var room:Room? - - if let nc = nynCall { - - for part in nc.participants { - - let member:Member = Member() - member.phone_id = part.address - members.append(member) - } - - if nc.externalInfo.count > 0 { - room = RoomDAO.findRoom(by: nc.externalInfo) - } - } - - AddParticipantsWireFrame().presentAddParticipants(navigation: navigation!, main: mainWF, selectedContacts: nil, delegate: external, mode: .updateGroupCall, members: members, room:room) - } - - func showMenuWith(participant: NYNCallParticipant?, delegate: ManageCallParticipantsProtocol) { - - let actionSheet = UIAlertController(title: nil, - message: nil, - preferredStyle: UIAlertControllerStyle.actionSheet) - - if (false == (participant?.isMe)!) { - actionSheet.addAction(UIAlertAction(title: "remove_participant_from_call".localized, - style: .default, - handler: { (action) in - delegate.remove(participant: participant) - })) - } - -// if participant.isMuted { -// actionSheet.addAction(UIAlertAction(title: "mute_call".localized, -// style: .default, -// handler: { (action) in -// delegate.mute(participant: participant) -// })) -// } else { -// actionSheet.addAction(UIAlertAction(title: "unmute".localized, -// style: .default, -// handler: { (action) in -// delegate.unmute(participant: participant) -// })) -// } - - actionSheet.addAction(UIAlertAction(title: "cancel".localized, style: .cancel, handler: { (action) in - - self.navigation?.dismiss(animated: true, completion: nil) - })) - - navigation?.present(actionSheet, animated: true, completion: nil) - } - - func removeParticipant(participant:NYNCallParticipant?) { - - } - - func muteParticipant(participant:NYNCallParticipant?) { - - - } - - func unmuteParticipant(participant:NYNCallParticipant?) { - - - } - -} diff --git a/Nynja/Modules/Main/Interactor/MainInteractor.swift b/Nynja/Modules/Main/Interactor/MainInteractor.swift index 2458db53e..f587c50d2 100644 --- a/Nynja/Modules/Main/Interactor/MainInteractor.swift +++ b/Nynja/Modules/Main/Interactor/MainInteractor.swift @@ -38,7 +38,7 @@ class MainInteractor: MainInteractorInputProtocol, VoxServiceDelegate, EditPhoto } func videoCall(name: String) { - //TODO: Call NynjaCommunicatorService with video true when ready + NynjaCommunicatorService.sharedInstance.call(user: name, withVideo: true) } func dialInGroup(name: String) { NynjaCommunicatorService.sharedInstance.dialInGroup(groupname: name) diff --git a/Nynja/Modules/Main/MainProtocols.swift b/Nynja/Modules/Main/MainProtocols.swift index 15b77d7c1..5484e44ff 100644 --- a/Nynja/Modules/Main/MainProtocols.swift +++ b/Nynja/Modules/Main/MainProtocols.swift @@ -33,6 +33,7 @@ protocol MainWireFrameProtocol: class { func showAddContactViaPhoneNumber() func showChatList() func showCreateConferenceCall() + func showNotImplemented() func sendAudio(withURL url: URL) func sendImage(with url: URL) @@ -57,9 +58,7 @@ protocol MainWireFrameProtocol: class { func getContact() -> String? func viewShowed() - func showMessages(contact: Contact, callVC: CallViewProtocol, isVideo: Bool) func showMessages(contact: Contact, callVC: CallInProgressViewProtocol, isVideo: Bool) - func showMessages(room: Room, callVC: CallViewProtocol, isVideo: Bool) func showMessages(room: Room, callVC: CallInProgressViewProtocol, isVideo: Bool) func showNotificationsSettings() func showWheelPositionPicker() @@ -166,8 +165,6 @@ protocol MainPresenterProtocol: class { func isActionEnabled() -> Bool func logout() func showLanguageSettings() - func showMessages(contact: Contact, call: NYNCall, callVC: CallViewProtocol, isVideo: Bool) - func showMessages(room: Room, call: NYNCall, callVC: CallViewProtocol, isVideo: Bool) func showMessages(contact: Contact, call: NYNCall, callVC: CallInProgressViewProtocol, isVideo: Bool) func showMessages(room: Room, call: NYNCall, callVC: CallInProgressViewProtocol, isVideo: Bool) diff --git a/Nynja/Modules/Main/Presenter/MainPresenter.swift b/Nynja/Modules/Main/Presenter/MainPresenter.swift index c10866612..374f50643 100644 --- a/Nynja/Modules/Main/Presenter/MainPresenter.swift +++ b/Nynja/Modules/Main/Presenter/MainPresenter.swift @@ -8,11 +8,11 @@ import VoxImplant class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, ScheduleMessageDelegate, EditParticipantsDelegate { - + func returnToCall() { self.wireFrame.returnToCall() } - + weak var view: MainViewProtocol! var interactor: MainInteractorInputProtocol! var wireFrame: MainWireFrameProtocol! @@ -21,23 +21,23 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu func showQRReader() { wireFrame.showQRReader() } - + func showContacts() { wireFrame.showContacts() } - + func showProfile() { wireFrame.showProfile() } - + func showAddContactViaPhoneNumber() { wireFrame.showAddContactViaPhoneNumber() } - + func showHistory() { wireFrame.showHistory() } - + func showInviteFriends() { PermissionManager().requestContactsPermission { (status) in if status == .authorized { @@ -45,7 +45,7 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu } } } - + func voiceCall() { if let name = wireFrame.getContact() { wireFrame.isVideo = false @@ -53,7 +53,7 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu interactor.call(name: name) } } - + func voiceGroupCall() { if let room = wireFrame.getRoom() { @@ -63,32 +63,32 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu //interactor.dialInGroup(name: room.id!) } } - + func videoCall() { - self.wireFrame.showNotImplementedAlert() + //TODO: To enable video call performVideoCall() } - + func videoGroupCall() { - + self.wireFrame.showNotImplementedAlert() } - + private func performVideoCall() { if let name = wireFrame.getContact() { wireFrame.isVideo = true interactor.videoCall(name: name) } } - + func showChatList() { wireFrame.showChatList() } - + func sendAudio(withURL url: URL) { wireFrame.sendAudio(withURL: url) } - + func sendImage(_ image: UIImage) { guard let url = ResourceManager(permissionManager: PermissionManager()).savePhoto(image: image, setting: .highest) else { LogService.log(topic: .fileSystem, text: "error save image") @@ -97,78 +97,57 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu wireFrame.sendImage(with: url) } - + func sendVideo(with url: URL) { wireFrame.sendVideo(with: url) } - + func getRecentsLocation() -> [LocationType] { return wireFrame.getRecentsLocation() } - + func getStarredLocation() -> [LocationType] { return wireFrame.getStarredLocation() } - + func getRecentsMedia() -> [Media] { return wireFrame.getRecentsMedia() } - + func sendTyping(_ isTyping: Bool) { wireFrame.sendTyping(isTyping) } - + func showMyContacts() { wireFrame.showMyContacts() } - + func viewShowed() { self.wireFrame.viewShowed() } - + func isActionEnabled() -> Bool { return wireFrame.getContact() != nil } - + func logout() { self.interactor.saveLogoutState() self.interactor.logout() self.changeScreenToAuth() } - + func changeScreenToAuth() { self.wireFrame.logout() } - - func showMessages(contact: Contact,call: NYNCall, callVC: CallViewProtocol, isVideo: Bool = false) { - - if isVideo { - if VoxService.sharedInstance.isRemoveVideoStream { - view.showPartnerVideoViewWithPhotoURL(url: contact.avatarUrl) - } else { - let prevView = self.view.showPartnerVideoView() - self.interactor.setVideoView(view: prevView) - } - } else { - self.view.showReturnToCall(call: call) - } - self.wireFrame.showMessages(contact: contact, callVC: callVC,isVideo: isVideo) - } - - func showMessages (room: Room, call: NYNCall, callVC: CallViewProtocol, isVideo: Bool = false) { - - self.view.showReturnToCall(call: call) - self.wireFrame.showMessages(room:room, callVC: callVC, isVideo: isVideo) - } func showMessages (room: Room, call: NYNCall, callVC: CallInProgressViewProtocol, isVideo: Bool = false) { - + self.view.showReturnToCall(call: call) self.wireFrame.showMessages(room:room, callVC: callVC, isVideo: isVideo) } - + func showMessages(contact: Contact, call: NYNCall, callVC: CallInProgressViewProtocol, isVideo: Bool) { - + if isVideo { if VoxService.sharedInstance.isRemoveVideoStream { view.showPartnerVideoViewWithPhotoURL(url: contact.avatarUrl) @@ -179,7 +158,7 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu } else { self.view.showReturnToCall(call: call) } - + self.wireFrame.showMessages(contact: contact, callVC: callVC,isVideo: isVideo) } @@ -187,31 +166,31 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu func openMapView() { self.wireFrame.openMapView() } - + func showLanguageSettings() { self.wireFrame.showLanguageSettings() } - + func about() { self.wireFrame.showSplash() } - + func showNotificationsSettings() { self.wireFrame.showNotificationsSettings() } - + func showWheelPositionPicker() { self.wireFrame.showWheelPositionPicker() } - + func showBuildNumber() { self.wireFrame.showBuildNumber() } - + func showChangeNumber() { self.wireFrame.showChangeNumber() } - + func conferenceVoiceCall() { wireFrame.showAddParticipantsToCreateConferenceCall() } @@ -224,19 +203,19 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu func showThemePicker() { self.wireFrame.showThemePicker() } - + func showSecuritySettings() { self.wireFrame.showSecuritySettings() } - + func showSupport() { self.wireFrame.showSupport() } - + func showPrivacy() { self.wireFrame.showPrivacy() } - + func showContactsToShare() { self.wireFrame.showContactsToShare() } @@ -255,63 +234,63 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu assertionFailure("Contact not found") } } - + func deleteAccount() { self.interactor.deleteAccount() self.wireFrame.logout() } - + func showDataAndStorage() { wireFrame.showDataAndStorage() } - + func showSettingsDataAndStorage(with usageMode: DataDownloadAndUsageMode) { wireFrame.showSettingsDataAndStorage(with: usageMode) } - + func showMySelfChat() { if let contact = self.interactor.contact { self.wireFrame.showMySelfChat(contact: contact) } } - + func showQRGenerator() { self.wireFrame.showQRGenerator() } - + func showAddContactByUserName() { self.wireFrame.showAddContactByUserName() } - + func showEditName() { self.wireFrame.showEditName() } - + func showEditUsername() { - self.wireFrame.showEditUsername() + self.wireFrame.showEditUsername() } - + // MARK: Group func showAddParticipants() { self.wireFrame.showAddParticipants() } - + func showGroupsList() { self.wireFrame.showGroupsList() } - + func showGroupsOptions() { self.wireFrame.showGroupsOptions() } - + func showUILocker() { view?.showUILocker() } - + func hideUILocker() { view?.hideUILocker() } - + // MARK: - ScheduleMessageDelegate func scheduleMessageHasBeenSent() { //TODO: @@ -320,11 +299,11 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu // MARK: EditParticpantsDelegate func participantsUpdated(contacts: [Contact]) { - + } - + func participantsUpdated(result: ParticipantsResult){ - + switch result { case let .createGroupCall(contacts, room): interactor.createGroupCall(contacts: contacts, room: room) diff --git a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift index 0e98e7d7a..3f079c41e 100644 --- a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift @@ -109,7 +109,6 @@ extension MainViewController: NavigateProtocol { closeWheel(indexPath: indexPath) } - // MARK: - Chat Actions func showMap(indexPath: IndexPath?) { diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index 9b919e521..7132257c3 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -86,6 +86,17 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato AddParticipantsWireFrame().presentAddParticipants(navigation: contentNavigation!, main: self, selectedContacts: [], delegate: self.external, mode: .createConferenceCall, members:[] , room: nil) } + func showNotImplemented() { + + let alertVC = UIAlertController(title: "voice_call_the_feature_currently_unavailable".localized, message: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: "ok".localized, style: .cancel) { (action) in + alertVC.dismiss(animated: true, completion: nil) + } + alertVC.addAction(okAction) + + self.view?.present(alertVC, animated: true, completion: nil) + } + func showContactsToShare() { ContactsWireFrame().presentContacts(navigation: contentNavigation, contactsViewMode: .shareContact, mainWireFrame: self) @@ -212,31 +223,12 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato } func incomingCall(call: VICall, isVideo: Bool) { - self.view?.view.endEditing(true) - self.call = call - if isVideo { - CallWireFrame().presentCall(navigation: self.navigation!, callMode:self.isGroup ? .incamingVideoGroupCall : .incamingVideoCall, contact: self.getNameFrom(call: call), call: call, main: self) - } else { - CallWireFrame().presentCall(navigation: self.navigation!, callMode:self.isGroup ? .incamingGroupCall : .incamingCall, contact: self.getNameFrom(call: call), call: call, main: self) - } } func ringing(call: VICall) { - self.view?.view.endEditing(true) - self.call = call - let callMode: CallMode = self.isGroup ? (self.isVideo ? .outGoingVideoGroupCall : .outGoingGroupCall) : (self.isVideo ? .outGoingVideoCall : .outGoingCall) - CallWireFrame().presentCall(navigation: navigation!, callMode: callMode, contact: self.getNameFrom(call: call), call: call, main: self) } func callClosed(call: VICall, isError: Bool) { - if !isVideo { - self.hideReturnToCallView() - } - if isVideo { - self.view?.hidePartnerVideoView() - } - navigation?.popViewController(animated: true) - navigation?.dismiss(animated: true, completion: nil) } func getNameFrom(call: VICall) -> Contact { @@ -252,21 +244,10 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato return contact } - var callVC :CallViewProtocol? var callInProgressVC :CallInProgressViewProtocol? var isVideo: Bool = false var isGroup: Bool = false - func showMessages(contact: Contact, callVC: CallViewProtocol, isVideo: Bool = false) { - self.isVideo = isVideo - self.view?.view.endEditing(true) - self.callVC = callVC - - if !isVideo { - self.showReturnToCallView() - } - showChat(contact) - } func showMessages(contact: Contact, callVC: CallInProgressViewProtocol, isVideo: Bool = false) { self.isVideo = isVideo @@ -279,19 +260,6 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato showChat(contact) } - func showMessages(room: Room, callVC: CallViewProtocol, isVideo: Bool = false) { - - self.isVideo = isVideo - self.view?.view.endEditing(true) - self.callVC = callVC - - if !isVideo { - self.showReturnToCallView() - } - - showChat(room) - } - func showMessages(room: Room, callVC: CallInProgressViewProtocol, isVideo: Bool = false) { self.isVideo = isVideo @@ -306,11 +274,7 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato } func callEnded(call: NYNCall, isError: Bool) { - - } - - func stateDidChange(call: NYNCall, state: NYNCallState) { - //TODO: Propagate to call view + self.hideReturnToCallView() } func viewShowed() { @@ -336,7 +300,6 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato func hideReturnToCallView() { updateTopContentNavigationOffset(0) - self.callVC = nil self.view?.hideReturnToCall() } @@ -351,9 +314,6 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato isVideo = false self.showReturnToCallView() self.view?.hidePartnerVideoView() - if let callVC = callVC as? CallViewController { - callVC.changeUIToIncall() - } // if let call = self.call { // self.view?.showReturnToCall(call: call) @@ -362,13 +322,7 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato func returnToCall() { - if callVC != nil { - - self.navigation?.pushViewController(callVC as! UIViewController, animated: true) - if !self.isVideo { - self.hideReturnToCallView() - } - } else if callInProgressVC != nil { + if callInProgressVC != nil { self.navigation?.pushViewController(callInProgressVC as! UIViewController, animated: true) if !self.isVideo { @@ -574,6 +528,7 @@ class MainWireFrame: MainWireFrameProtocol, VoxServiceDelegate, NynjaCommunicato func presentCallInProgressViewForCall(call:NYNCall) { + self.isVideo = call.recvVideo let callMode: CallInProgressMode = self.isVideo ? .oneToOneVideo : call.isConference() ? .groupAudio : .oneToOneAudio if callMode == .groupAudio { diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 60bea51c5..2151b73ad 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -1580,7 +1580,15 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw extension MessageVC { func didPressButtonJoinIn(callInfoView: CallInfoView) { - presenter.rejoinRunningCall() + AlertManager.sharedInstance.showAlertWithTwoActions(title: "", + message: "are_u_sure_join_the_call".localized, + firstActionTitle: "no".localized, + secondActionTitle: "yes".localized, + firstAction: nil, + secondAction: { + self.presenter.rejoinRunningCall() + }) + } } diff --git a/Nynja/OptionsItemsFactory.swift b/Nynja/OptionsItemsFactory.swift index 6f0e2d9a6..09089073b 100644 --- a/Nynja/OptionsItemsFactory.swift +++ b/Nynja/OptionsItemsFactory.swift @@ -18,7 +18,7 @@ class OptionsItemsFactory: WCBaseItemsFactory { // MARK: - Second lvl override var secondLevelItems: ItemModels { // return [logout, notifications, changeNumber, wheelPosition, buildNumber, support, languageSettings, theme, dataAndStorage, security, privacy, about, deleteAccount] - return [languageSettings, theme, dataAndStorage, security, privacy, notifications, changeNumber, wheelPosition, buildNumber, support] + return [languageSettings, theme, dataAndStorage, security, privacy, notifications, changeNumber, wheelPosition, buildNumber, support, logout] } //Language, Theme, Data and Storage, Security, Privacy, notification, change number, Wheel position, Build number, support // MARK: - Items diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 06840e9ce..21192854c 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.136 + 0.2.137 ConfServerAddress $(ConfServerAddress) ConfServerPort diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 2d5f6c7ef..bdc66b985 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -396,6 +396,7 @@ "unblock"="Unblock"; "are_u_sure_block"="Are you sure you want to block this user?"; "are_u_sure_unblock"="Are you sure you want to unblock this user?"; +"are_u_sure_join_the_call"="Do you want to join the call?"; "send_message"="Send a message"; "add_contact"="Add to contacts"; "u_are_blocked"="You are blocked"; @@ -595,6 +596,7 @@ // MARK: Call "voice_call_status"="Voice Call"; +"video_call_status"="Video Call"; "voice_call_the_feature_currently_unavailable"="This feature is currently unavailable"; // MARK: Settings group diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings index badf79152..cc25b1f21 100644 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ b/Nynja/Resources/ru.lproj/Localizable.strings @@ -360,6 +360,7 @@ "unblock"="Разблокировать"; "are_u_sure_block"="Вы уверены, что хотите заблокировать этого пользователя?"; "are_u_sure_unblock"="Вы уверены что хотите разблокировать этого пользователя?"; +"are_u_sure_join_the_call"="Вы хотите присоединиться к вызову?"; "send_message"="Отправить сообщение"; "add_contact"="Добавить в контакты"; "u_are_blocked"="Вы заблокированы"; @@ -556,6 +557,7 @@ // MARK: Call "voice_call_status"="Телефонный звонок"; +"video_call_status"="Видео звонок"; "voice_call_the_feature_currently_unavailable"="Эта функция в данный момент недоступна"; // MARK: Settings group diff --git a/Nynja/Services/NynjaCommunicatorService.swift b/Nynja/Services/NynjaCommunicatorService.swift index 6d503e111..4579d5a1a 100644 --- a/Nynja/Services/NynjaCommunicatorService.swift +++ b/Nynja/Services/NynjaCommunicatorService.swift @@ -12,28 +12,46 @@ import NynjaSDK protocol NynjaCommunicatorServiceDelegate: class { func dialing(call: NYNCall) func creatingGroupCall(name: String, call: NYNCall) - func callEnded(call: NYNCall, isError: Bool) - func stateDidChange(call: NYNCall, state: NYNCallState) - func participantsUpdated(call: NYNCall) func incomingCallRinging(call: NYNCall) - func conferenceCreated(call: NYNCall) + func callEnded(call: NYNCall, isError: Bool) } extension NynjaCommunicatorServiceDelegate { func dialing(call: NYNCall) {} func creatingGroupCall(name: String, call: NYNCall){} + func incomingCallRinging(call: NYNCall){} + func callEnded(call: NYNCall, isError: Bool){} +} + +protocol NynjaCallDelegate: class { + func callEnded(call: NYNCall, isError: Bool) + func stateDidChange(call: NYNCall, state: NYNCallState) + func participantsUpdated(call: NYNCall) + func conferenceCreated(call: NYNCall) + func didAddVideoStreamForCall(call: NYNCall) + func didRemoveVideoStreamForCall(call: NYNCall) + func didStartLocalCapturerForCall(call: NYNCall) + func didStopLocalCapturerForCall(call: NYNCall) +} + +extension NynjaCallDelegate { func callEnded(call: NYNCall, isError: Bool) {} - func stateDidChange(call: NYNCall, state: NYNCallState){} + func stateDidChange(call: NYNCall, state: NYNCallState) {} func participantsUpdated(call: NYNCall){} - func incomingCallRinging(call: NYNCall){} func conferenceCreated(call: NYNCall) {} + func didAddVideoStreamForCall(call: NYNCall) {} + func didRemoveVideoStreamForCall(call: NYNCall) {} + func didStartLocalCapturerForCall(call: NYNCall) {} + func didStopLocalCapturerForCall(call: NYNCall) {} } class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDelegate, NYNCallManagerDelegate { + let nynComm: NynjaCommunicator var isCallInProgress = false weak var delegate : NynjaCommunicatorServiceDelegate? + weak var callDelegate : NynjaCallDelegate? var call:NYNCall? private var creators: [String: CallCreatorMediator] = [String: CallCreatorMediator]() var username: String? @@ -116,7 +134,8 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.nynComm.getCallManager().createConference(withRequestId: creator.createId, withExternalId: UUID().uuidString, - withRequestInfo: creator.roomId) + withRequestInfo: creator.roomId, + withSubject: creator.name) } func addConferenceMember(conferenceId:String, phoneId: String, name:String) { @@ -232,7 +251,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func didChangeConferenceState(_ state: NYNCallState) { if let c = self.call { - self.delegate?.stateDidChange(call: c, state: state) + self.callDelegate?.stateDidChange(call: c, state: state) } } @@ -256,6 +275,30 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele }; } + func attachRemoteVideoRenderer(inView view: UIView) { + if let call = self.call { + call.attachRemoteRenderer(to: view) + } + } + + func attachLocalVideoPreview(inView view: UIView) { + if let call = self.call { + call.attachLocalPreview(to: view) + } + } + + func detachLocalVideoPreview(inView view: UIView) { + if let call = self.call { + call.detachLocalPreview(from: view) + } + } + + func switchCamera() { + if let call = self.call { + call.switchCamera() + } + } + //MARK: Helpers func getMySelf() -> Contact? { @@ -347,6 +390,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func callDidEnd(_ call: NYNCall) { isCallInProgress = false if let c = self.call, call.callId.elementsEqual(c.callId) { + self.callDelegate?.callEnded(call: c, isError: false) self.delegate?.callEnded(call: c, isError: false) self.call?.setDelegate(nil) self.call = nil @@ -358,7 +402,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele LogService.log(topic: .callSystem, text: log) if let c = self.call, let cid = callid { if c.callId.elementsEqual(cid) { - self.delegate?.stateDidChange(call: c, state: state) + self.callDelegate?.stateDidChange(call: c, state: state) } } } @@ -368,7 +412,43 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele LogService.log(topic: .callSystem, text: log) if let c = self.call, let cid = callid { if cid.elementsEqual(c.callId) { - self.delegate?.participantsUpdated(call: c) + self.callDelegate?.participantsUpdated(call: c) + } + } + } + + func call(_ call: NYNCall, didAddRemoteVideoTrack trackId: String) { + let log = NSString(format: "callWithIdDidAddStream: ", self) as String + LogService.log(topic: .callSystem, text: log) + if let c = self.call{ + if c.callId.elementsEqual(call.callId) { + self.callDelegate?.didAddVideoStreamForCall(call: call) + } + } + } + + func call(_ call: NYNCall, didRemoveRemoteVideoTrack trackId: String) { + let log = NSString(format: "callWithIdDidRemoVeStream: ", self) as String + LogService.log(topic: .callSystem, text: log) + if let c = self.call { + if c.callId.elementsEqual(call.callId) { + self.callDelegate?.didRemoveVideoStreamForCall(call: call) + } + } + } + + func callDidStartLocalCapturer(_ call: NYNCall) { + if let c = self.call{ + if c.callId.elementsEqual(call.callId) { + self.callDelegate?.didStartLocalCapturerForCall(call: call) + } + } + } + + func callDidStopLocalCapturer(_ call: NYNCall) { + if let c = self.call{ + if c.callId.elementsEqual(call.callId) { + self.callDelegate?.didStopLocalCapturerForCall(call: call) } } } @@ -431,7 +511,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.nynComm.getCallManager().startConference(withRequestId: cr.startId!, withConferenceId: cr.conferenceId!) guard let call = self.call else { return } - self.delegate?.conferenceCreated(call: call) + self.callDelegate?.conferenceCreated(call: call) } } } @@ -469,7 +549,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele // clean up call if let c = self.call, c.callId.elementsEqual(cr.conferenceId!) { - self.delegate?.callEnded(call: c, isError: true) + self.callDelegate?.callEnded(call: c, isError: true) self.call?.setDelegate(nil) self.call = nil isCallInProgress = false @@ -495,7 +575,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele // clean up call if let c = self.call, call.callId.elementsEqual(cr.conferenceId!) { - self.delegate?.callEnded(call: c, isError: true) + self.callDelegate?.callEnded(call: c, isError: true) self.call?.setDelegate(nil) self.call = nil isCallInProgress = false @@ -550,7 +630,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func didStopRingingIncomingCall(withId conferenceId: String) { // clean up call if let c = self.call, c.callId.elementsEqual(conferenceId) { - self.delegate?.callEnded(call: c, isError: false) + self.callDelegate?.callEnded(call: c, isError: false) self.call?.setDelegate(nil) self.call = nil isCallInProgress = false @@ -583,4 +663,5 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func callStateDidChange(_ call: NYNCall) { self.messageInteractorCallProtocol?.didChangeCallInvitationState(call) } + } diff --git a/Podfile b/Podfile index 03dab61bd..b198b57c0 100644 --- a/Podfile +++ b/Podfile @@ -28,19 +28,19 @@ def commonPodsForNynja pod 'CocoaLumberjack', :git => 'https://github.com/CocoaLumberjack/CocoaLumberjack', :commit => '12948ff' pod 'AWSS3', '~> 2.6.1' pod 'SDWebImage', '~> 4.0' - + pod 'GoogleMaps' pod 'GooglePlaces' pod 'Firebase/Storage' pod 'Firebase/Auth' pod 'GRDBCipher', '~> 2.10.0' pod 'SwiftyJSON', '~> 4.0.0' - + pod 'AutoScrollLabel' pod 'MaterialComponents/FlexibleHeader' pod 'JTAppleCalendar', '~> 7.0' - - pod 'NynjaSDK', '~> 1.3.3' + + pod 'NynjaSDK', '~> 1.4.0' pod 'CryptoSwift' end @@ -56,11 +56,11 @@ def commonPodsForNynjaTests pod 'CocoaLumberjack', :git => 'https://github.com/CocoaLumberjack/CocoaLumberjack', :commit => '12948ff' pod 'AWSS3', '~> 2.6.1' pod 'SDWebImage', '~> 4.0' - + pod 'GoogleMaps' pod 'GooglePlaces' pod 'GRDBCipher', '~> 2.10.0' - + pod 'AutoScrollLabel' pod 'MaterialComponents/FlexibleHeader' pod 'JTAppleCalendar', '~> 7.0' @@ -110,7 +110,7 @@ post_install do |installer| config.build_settings['SWIFT_VERSION'] = '4.0' end end - + installer.pods_project.build_configurations.each do |config| config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = '' end -- GitLab From b3625ceabab3fea08b8c236d347876d0a7dc668b Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 10 Aug 2018 12:03:19 +0300 Subject: [PATCH 18/32] Fixed crash on replies screen (#1085) --- .../Interactor/RepliesInteractor.swift | 6 ++-- Nynja/Modules/Replies/View/RepliesVC.swift | 28 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift b/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift index cc4e352b9..bb60d1b9a 100644 --- a/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift +++ b/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift @@ -30,7 +30,6 @@ final class RepliesInteractor: BaseInteractor, RepliesInteractorInputProtocol, M processingManager = DefaultMessagesProcessingManager.shared super.init() message = MessageDAO.fetchMessage(localId: messageLocalId) - processingManager.delegate = self } //MARK: - BaseInteractor @@ -44,6 +43,7 @@ final class RepliesInteractor: BaseInteractor, RepliesInteractorInputProtocol, M override func loadData() { super.loadData() + processingManager.delegate = self let replyIdentifiers = message.repliedby?.compactMap { Int64($0) } ?? [] loadReplies(replyIdentifiers) presenter?.updateHeaderView(replied: message.repliedby?.count ?? 0) @@ -130,7 +130,9 @@ final class RepliesInteractor: BaseInteractor, RepliesInteractorInputProtocol, M // MARK: - MessageProcessingDelegate func updateProgress(_ progress: ProgressModel) { - self.presenter?.updateProgress(progress) + dispatchAsyncMain { [weak presenter] in + presenter?.updateProgress(progress) + } } diff --git a/Nynja/Modules/Replies/View/RepliesVC.swift b/Nynja/Modules/Replies/View/RepliesVC.swift index 00f85889a..179e588d4 100644 --- a/Nynja/Modules/Replies/View/RepliesVC.swift +++ b/Nynja/Modules/Replies/View/RepliesVC.swift @@ -19,8 +19,8 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { } } - var collectionViewDataSource: RepliesDS! - var collectionViewDelegate: RepliesCollectionViewDelegate! + var collectionViewDataSource: RepliesDS? + var collectionViewDelegate: RepliesCollectionViewDelegate? //MARK: - Subviews @@ -97,7 +97,7 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { collectionViewDataSource = RepliesDS(view: self) collectionViewDelegate = RepliesCollectionViewDelegate(collectionView: collectionView, - dataSource: collectionViewDataSource) + dataSource: collectionViewDataSource!) collectionView.dataSource = collectionViewDataSource collectionView.delegate = collectionViewDelegate @@ -110,23 +110,26 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { } func updateTableViewDataSource(_ cells: [BaseChatCellModel]) { - collectionViewDataSource.cells = cells + collectionViewDataSource?.cells = cells collectionView.reloadData() collectionView.layoutIfNeeded() } func removeReplies(_ ids: [String]) { - let filtred = self.collectionViewDataSource.cells.filter { model -> Bool in + guard let dataSource = collectionViewDataSource else { + return + } + let filtred = dataSource.cells.filter { model -> Bool in ids.contains(where: { $0 == model.id }) } let indexes = filtred.map { model -> Int in - return self.collectionViewDataSource.cells.index(where: { $0.id == model.id })! + return dataSource.cells.index(where: { $0.id == model.id })! } guard !indexes.isEmpty else { return } - indexes.forEach { self.collectionViewDataSource.cells.remove(at: $0) } + indexes.forEach { dataSource.cells.remove(at: $0) } collectionView.performBatchUpdates({ collectionView.deleteItems(at: indexes.map { IndexPath(row: $0, section: 0) }) @@ -138,13 +141,12 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { } func updateProgress(progressModel: ProgressModel?) { - let url = progressModel?.url - let list = collectionViewDataSource.cells.filter({ (model) -> Bool in - model.progressModel?.url == url - }) - list.forEach { (model) in - model.progressModel = progressModel + guard let dataSource = collectionViewDataSource else { + return } + let url = progressModel?.url + let list = dataSource.cells.filter { $0.progressModel?.url == url } + list.forEach { $0.progressModel = progressModel } reloadIfVisible(models: list) } -- GitLab From 3081f5b098a377abb55689891cfe822d228f203a Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Fri, 10 Aug 2018 13:35:13 +0300 Subject: [PATCH 19/32] Group name trimmed --- .../EditGroupNameProtocols.swift | 13 +++--- .../Interactor/EditGroupNameInteractor.swift | 37 +++++++++++++++-- .../Presenter/EditGroupNamePresenter.swift | 40 ++++++++++++++----- .../View/EditGroupNameViewController.swift | 26 ++++++------ .../WireFrame/EditGroupNameWireframe.swift | 32 +++++++++------ .../TextInputValidationService.swift | 11 +++++ 6 files changed, 116 insertions(+), 43 deletions(-) diff --git a/Nynja/Modules/EditGroupName/EditGroupNameProtocols.swift b/Nynja/Modules/EditGroupName/EditGroupNameProtocols.swift index f171dbfac..3cf57dfd3 100644 --- a/Nynja/Modules/EditGroupName/EditGroupNameProtocols.swift +++ b/Nynja/Modules/EditGroupName/EditGroupNameProtocols.swift @@ -19,6 +19,7 @@ protocol EditGroupNameWireFrameProtocol: class { /** * Add here your methods for communication PRESENTER -> WIREFRAME */ + func dismiss() } protocol EditGroupNameViewProtocol: class { @@ -30,11 +31,10 @@ protocol EditGroupNameViewProtocol: class { */ func setup(groupName: String) + func hideKeyboard() } -protocol EditGroupNamePresenterProtocol: AnyObject, BasePresenterProtocol { - - var mode: GroupMode! { get set } +protocol EditGroupNamePresenterProtocol: BasePresenterProtocol { var view: EditGroupNameViewProtocol! { get set } var interactor: EditGroupNameInteractorInputProtocol! { get set } @@ -45,7 +45,7 @@ protocol EditGroupNamePresenterProtocol: AnyObject, BasePresenterProtocol { */ func showed() - func saveGroupName(_ name:String) + func handleSaveTap(for name: String?) } protocol EditGroupNameInteractorOutputProtocol: class { @@ -53,15 +53,18 @@ protocol EditGroupNameInteractorOutputProtocol: class { /** * Add here your methods for communication INTERACTOR -> PRESENTER */ + func nameSaved() + func showAlert(with message: String) } protocol EditGroupNameInteractorInputProtocol: class { var presenter: EditGroupNameInteractorOutputProtocol! { get set } - + var oldName: String! { get } /** * Add here your methods for communication PRESENTER -> INTERACTOR */ func notifyDelegate(newName: String) + func validate(_ name: String?) } diff --git a/Nynja/Modules/EditGroupName/Interactor/EditGroupNameInteractor.swift b/Nynja/Modules/EditGroupName/Interactor/EditGroupNameInteractor.swift index 73d1b68c5..d44e5b541 100644 --- a/Nynja/Modules/EditGroupName/Interactor/EditGroupNameInteractor.swift +++ b/Nynja/Modules/EditGroupName/Interactor/EditGroupNameInteractor.swift @@ -7,12 +7,43 @@ // class EditGroupNameInteractor: EditGroupNameInteractorInputProtocol { - + weak var presenter: EditGroupNameInteractorOutputProtocol! + private var validationService: TextInputValidationServiceProtocol! + private weak var delegate: GroupNameEditorDelegate? + var oldName:String! - weak var delegate: GroupNameEditorDelegate? + required init(withName oldName: String, delegate: GroupNameEditorDelegate?) { + self.oldName = oldName + self.delegate = delegate + } func notifyDelegate(newName: String) { - delegate?.groupNameWasChanged(newValue: newName) + self.delegate?.groupNameWasChanged(newValue: newName) + } + + func validate(_ name: String?) { + let groupName = name?.trimmingCharacters(in: .whitespaces) + do { + let groupName = try self.validationService.validateGroupName(groupName) + if self.oldName != groupName { + self.notifyDelegate(newName: groupName) + } + self.presenter.nameSaved() + } catch { + self.presenter.showAlert(with: error.localizedDescription) + } + } +} + +extension EditGroupNameInteractor: SetInjectable { + func inject(dependencies: EditGroupNameInteractor.Dependencies) { + self.presenter = dependencies.presenter + self.validationService = dependencies.validationService + } + + struct Dependencies { + let presenter: EditGroupNameInteractorOutputProtocol + let validationService: TextInputValidationServiceProtocol } } diff --git a/Nynja/Modules/EditGroupName/Presenter/EditGroupNamePresenter.swift b/Nynja/Modules/EditGroupName/Presenter/EditGroupNamePresenter.swift index ef2f0518f..2f76db340 100644 --- a/Nynja/Modules/EditGroupName/Presenter/EditGroupNamePresenter.swift +++ b/Nynja/Modules/EditGroupName/Presenter/EditGroupNamePresenter.swift @@ -16,22 +16,44 @@ class EditGroupNamePresenter: BasePresenter, EditGroupNamePresenterProtocol, Edi } } - var mode: GroupMode! + private var mode: GroupMode! + + required init(with mode: GroupMode) { + self.mode = mode + } weak var view: EditGroupNameViewProtocol! var interactor: EditGroupNameInteractorInputProtocol! var wireFrame: EditGroupNameWireFrameProtocol! - - var oldName:String! func showed() { - view.setup(groupName: oldName) + view.setup(groupName: self.interactor.oldName) } - func saveGroupName(_ name: String) { - if oldName != name { - interactor.notifyDelegate(newName: name) - } - (view as? UIViewController)?.navigationController?.popViewController(animated: false) + func handleSaveTap(for name: String?) { + self.interactor.validate(name) + } + + func showAlert(with message: String) { + AlertManager.sharedInstance.showAlertOk(message: message) + } + + func nameSaved() { + self.wireFrame.dismiss() + } + +} + +extension EditGroupNamePresenter: SetInjectable { + func inject(dependencies: EditGroupNamePresenter.Dependencies) { + self.view = dependencies.view + self.interactor = dependencies.interactor + self.wireFrame = dependencies.wireFrame + } + + struct Dependencies { + let view: EditGroupNameViewProtocol + let interactor: EditGroupNameInteractorInputProtocol + let wireFrame: EditGroupNameWireFrameProtocol } } diff --git a/Nynja/Modules/EditGroupName/View/EditGroupNameViewController.swift b/Nynja/Modules/EditGroupName/View/EditGroupNameViewController.swift index 857b8fd40..bd8b9096c 100644 --- a/Nynja/Modules/EditGroupName/View/EditGroupNameViewController.swift +++ b/Nynja/Modules/EditGroupName/View/EditGroupNameViewController.swift @@ -117,21 +117,11 @@ class EditGroupNameViewController: BaseVC, EditGroupNameViewProtocol { } @objc func saveTapped() { - let name = (nameField.input.text ?? "") - if valid(groupName: name.trimmingCharacters(in: .whitespaces)) { - self.view.endEditing(true) - self.presenter.saveGroupName(name) - } + self.presenter.handleSaveTap(for: nameField.input.text) } - // MARK: Utils - private func valid(groupName name: String) -> Bool { - if name.count < 2 { - AlertManager.sharedInstance.showAlertOk(message: Strings.groupNameEmptyMessage.localized) - return false - } - - return true + func hideKeyboard() { + self.view.endEditing(true) } // MARK: Private @@ -202,6 +192,16 @@ class EditGroupNameViewController: BaseVC, EditGroupNameViewProtocol { } } +extension EditGroupNameViewController: SetInjectable { + func inject(dependencies: EditGroupNameViewController.Dependencies) { + self.presenter = dependencies.presenter + } + + struct Dependencies { + let presenter: EditGroupNamePresenterProtocol + } +} + // MARK: - Testable extension EditGroupNameViewController: TestableViewControllerProtocol { diff --git a/Nynja/Modules/EditGroupName/WireFrame/EditGroupNameWireframe.swift b/Nynja/Modules/EditGroupName/WireFrame/EditGroupNameWireframe.swift index cb64a69f3..b8739c1e7 100644 --- a/Nynja/Modules/EditGroupName/WireFrame/EditGroupNameWireframe.swift +++ b/Nynja/Modules/EditGroupName/WireFrame/EditGroupNameWireframe.swift @@ -13,23 +13,29 @@ class EditGroupNameWireFrame: EditGroupNameWireFrameProtocol { weak var navigation : UINavigationController? func presentEditGroupName(navigation: UINavigationController, currentName:String, delegate:GroupNameEditorDelegate?, mode: GroupMode) { - let view = EditGroupNameViewController() - let presenter = EditGroupNamePresenter() - let interactor = EditGroupNameInteractor() self.navigation = navigation - presenter.oldName = currentName - interactor.delegate = delegate - // Connecting - view.presenter = presenter - presenter.mode = mode - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter + let view = EditGroupNameViewController() + let presenter = EditGroupNamePresenter(with: mode) + let interactor = EditGroupNameInteractor(withName: currentName, delegate: delegate) + + let presenterDependencies = EditGroupNamePresenter.Dependencies(view: view, interactor: interactor, wireFrame: self) + presenter.inject(dependencies: presenterDependencies) - navigation.pushViewController(view as UIViewController, animated: true) + let viewDependencies = EditGroupNameViewController.Dependencies(presenter: presenter) + view.inject(dependencies: viewDependencies) + let serviceFactory = ServiceFactory() + let validationService = serviceFactory.makeTextInputValidationService() + + let interactorDependencies = EditGroupNameInteractor.Dependencies(presenter: presenter, validationService: validationService) + interactor.inject(dependencies: interactorDependencies) + + navigation.pushViewController(view as UIViewController, animated: true) + } + + func dismiss() { + self.navigation?.popViewController(animated: false) } } diff --git a/Nynja/Validation/TextInputValidationService.swift b/Nynja/Validation/TextInputValidationService.swift index b28056ba4..c9f090947 100644 --- a/Nynja/Validation/TextInputValidationService.swift +++ b/Nynja/Validation/TextInputValidationService.swift @@ -11,6 +11,7 @@ import Foundation protocol TextInputValidationServiceProtocol { func validateFirstName(_ name: String?) throws -> String func validateLastNameNew(_ name: String?) throws -> String + func validateGroupName(_ name: String?) throws -> String func validateInterpretationTime(_ number: Int) throws -> Int } @@ -40,6 +41,13 @@ final class TextInputValidationService: TextInputValidationServiceProtocol { return text } + func validateGroupName(_ name: String?) throws -> String { + guard let text = name, text.count >= 1 else { + throw TextInputValidationError.groupNameIsEmpty + } + return text + } + func validateInterpretationTime(_ number: Int) throws -> Int { if number < 1 || number > 300 { throw TextInputValidationError.interpretationTimeIsOutOfRange @@ -54,6 +62,7 @@ enum TextInputValidationError: LocalizedError { case firstNameHasNotEnoughSymbols case firstNameHasTooMuchSymbols case lastNameHasTooMuchSymbols + case groupNameIsEmpty case interpretationTimeIsOutOfRange var errorDescription: String? { @@ -66,6 +75,8 @@ enum TextInputValidationError: LocalizedError { return "First_Name_must_be_at_more".localized case .lastNameHasTooMuchSymbols: return "Last_Name_must_be_at_more".localized + case .groupNameIsEmpty: + return "Group_Name_Empty_Message".localized case .interpretationTimeIsOutOfRange: return "interpretation_time_out_of_range".localized } -- GitLab From 7f3fc70e1a99fedab94e501eb4facc79050dbc78 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Fri, 10 Aug 2018 16:03:24 +0300 Subject: [PATCH 20/32] Call bubble Marketplace spaces fixed --- .../UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift index f4339719a..b9106e671 100644 --- a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift +++ b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift @@ -180,8 +180,7 @@ extension NynjaContextMenuItemsFactory { } private static func audioCallMessageItems(for model: BaseChatCellModel) -> [ContextMenuRow] { - return [ ContextMenuRow(items: [ - starItem(for: model), delete(), marketplace()])] + return [ ContextMenuRow(items: [marketplace()])] } private static func translatedMessageItems(for model: BaseChatCellModel) -> [ContextMenuRow] { -- GitLab From 92322ea34518b285aec9c68f6911a385118020c3 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Fri, 10 Aug 2018 16:15:10 +0300 Subject: [PATCH 21/32] Correct UI for Add participants screen --- .../View/AddParticipantsViewController.swift | 10 ++++++++++ .../View/TableView/ParticipantsDataSource.swift | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift index 3fba25e68..9fbe0a3cf 100644 --- a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift +++ b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift @@ -229,6 +229,10 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { tableView.register(headerFooter: ParticipantsHeaderView.self) participantsDataSource = ParticipantsDataSource() + if self.presenter.participantsMode == .create { + participantsDataSource.selectedParticipantsDelegate = self + } + tableView.dataSource = participantsDataSource avatarsView.dataSource = participantsDataSource @@ -458,3 +462,9 @@ extension AddParticipantsViewController: TestableViewControllerProtocol { } } } + +extension AddParticipantsViewController: NotifySelectedParticipantsProtocol { + func notify(selectedParticipants count: Int) { + self.doneButton.isEnabled = count != 0 + } +} diff --git a/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDataSource.swift b/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDataSource.swift index 7f91530f9..5f0f62d5e 100644 --- a/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDataSource.swift +++ b/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDataSource.swift @@ -5,8 +5,14 @@ // Created by Volodymyr Hryhoriev on 11/8/17. // +protocol NotifySelectedParticipantsProtocol: class { + func notify(selectedParticipants count: Int) +} + class ParticipantsDataSource: NSObject, UITableViewDataSource, UICollectionViewDataSource { + weak var selectedParticipantsDelegate: NotifySelectedParticipantsProtocol? + var groupedParticipants: GroupedParticipants = [:] { didSet { sortedLetters = Array(groupedParticipants.keys).sorted() @@ -15,7 +21,11 @@ class ParticipantsDataSource: NSObject, UITableViewDataSource, UICollectionViewD var sortedLetters: [String] = [] - var selectedParticipants: [Participant] = [] + var selectedParticipants: [Participant] = [] { + didSet { + self.selectedParticipantsDelegate?.notify(selectedParticipants: self.selectedParticipants.count) + } + } var emptySelectionText: String = "no_participant_selected".localized { didSet { -- GitLab From 32c2860e9d39c13c9daaef73dd15a58a74f8aab4 Mon Sep 17 00:00:00 2001 From: Angel Terziev Date: Fri, 10 Aug 2018 16:24:36 +0300 Subject: [PATCH 22/32] Updated Podfile to link NynjaSDK 1.4.1 --- Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile b/Podfile index b3b462678..fffbdb131 100644 --- a/Podfile +++ b/Podfile @@ -39,7 +39,7 @@ def commonPodsForNynja pod 'MaterialComponents/FlexibleHeader' pod 'JTAppleCalendar', '~> 7.0' - pod 'NynjaSDK', '~> 1.4.0' + pod 'NynjaSDK', '~> 1.4.1' pod 'CryptoSwift' end -- GitLab From f7c5e0fd315f52d3048363b2dcc1c5b02899b112 Mon Sep 17 00:00:00 2001 From: Roman <35774194+rchopovenko@users.noreply.github.com> Date: Fri, 10 Aug 2018 17:29:40 +0300 Subject: [PATCH 23/32] Other user profile open by tap on user avatar in Group --- Nynja.xcodeproj/project.pbxproj | 4 ++-- .../Message/Interactor/MessageInteractor.swift | 12 +++++++++--- .../Modules/Message/Presenter/MessagePresenter.swift | 6 +++++- .../Modules/Message/Protocols/MessageProtocols.swift | 4 +++- .../Message/View/MessageVC+CellDelegate.swift | 12 ++++++++++++ .../Cells/ChatCells/BaseChatCell/BaseChatCell.swift | 8 ++++++++ .../BaseChatCell/BaseChatCellDelegate.swift | 2 ++ .../Views/TableView/Cells/Models/MessageSender.swift | 5 +++-- .../Replies/Interactor/RepliesInteractor.swift | 6 +++--- 9 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 214ccdab4..47e2dce9b 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -12830,7 +12830,7 @@ "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", @@ -12857,7 +12857,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 7b5cce973..9b8e78de5 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -335,12 +335,12 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H func sender(for message: Message) -> MessageSender? { let isOwner = message.from == myContact?.phone_id if let _ = (message.feed_id as? muc), let from = message.from, let member = member(for: from) { - return MessageSender(fullname: member.fullName ?? "", nick: member.alias, avatar: member.avatar) + return MessageSender(phoneId: member.phone_id, fullname: member.fullName ?? "", nick: member.alias, avatar: member.avatar) } else if let contact = self.contact { if isOwner, let myContact = myContact { - return MessageSender(fullname: myContact.fullName ?? "", nick: myContact.nick, avatar: myContact.avatar) + return MessageSender(phoneId: myContact.phone_id, fullname: myContact.fullName ?? "", nick: myContact.nick, avatar: myContact.avatar) } else { - return MessageSender(fullname: contact.fullName ?? "", nick: contact.nick, avatar: contact.avatar) + return MessageSender(phoneId: contact.phone_id, fullname: contact.fullName ?? "", nick: contact.nick, avatar: contact.avatar) } } @@ -1023,4 +1023,10 @@ extension MessageInteractor { assertionFailure(error.localizedDescription) } } + + func getMessageSenderContact(for sender: MessageSender) -> Contact? { + guard let phoneId = sender.phoneId else { return nil } + guard let contact = ContactDAO.findContactBy(phoneId: phoneId) else { return nil } + return contact + } } diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 2c75b9737..e4a306091 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -955,7 +955,11 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract func startSendingMessage() { view.hideReplyPreview() } - + + func handleOpponentAvatarTap(for sender: MessageSender) { + guard let contact = self.interactor.getMessageSenderContact(for: sender) else { return } + self.openSharedContact(contact: contact) + } // MARK: - Utils private func updateStatus(_ status: String) { if internetStatus == .connected { diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index bd4dd59bd..1aeb02a2c 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -120,11 +120,11 @@ protocol MessagePresenterProtocol: BasePresenterProtocol, func declineReply() func openURL(url: URL) + func handleOpponentAvatarTap(for sender: MessageSender) func hasRunningCall() -> Bool func rejoinRunningCall() func openMarketplaceScreen() - } //MARK: Interactor - @@ -238,6 +238,8 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func editMessage(_ message: InputTextMessage) func clearEditMessageObject() func scheduleInfo(for message: InputScheduleMessage) -> ScheduleInfo? + + func getMessageSenderContact(for sender: MessageSender) -> Contact? func hasRunningCall() -> Bool func rejoinRunningCall() } diff --git a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift index afe12aea7..f4c5fe105 100644 --- a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift +++ b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift @@ -184,4 +184,16 @@ extension MessageVC: BaseChatCellDelegate, AudioManagerDelegate, ProximitySensor currentPlayingModel?.notifyAudioHandler() currentPlayingModel = nil } + + func updateCellIfVisible() { + if let model = currentPlayingModel { + reloadIfVisible(models: [model]) + } + } + + func opponentAvatarTapped(_ cell: BaseChatCell) { + inputBar.endEditing(true) + guard let sender = cell.model?.sender else { return } + self.presenter.handleOpponentAvatarTap(for: sender) + } } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift index e1314166c..2df83cb89 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift @@ -295,6 +295,10 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag bubble.isUserInteractionEnabled = true bubble.addGestureRecognizer(recognizer) + + let opponentAvaTap = UITapGestureRecognizer(target: self, action: #selector(opponentAvatarTapped)) + self.fromImageView.isUserInteractionEnabled = true + self.fromImageView.addGestureRecognizer(opponentAvaTap) } func setupSenderInfo(_ model: BaseChatCellModel) { @@ -407,6 +411,10 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag delegate?.didPauseTapped(self, url: url) } + @objc func opponentAvatarTapped() { + delegate?.opponentAvatarTapped(self) + } + func didChangeProgress(_ progress: Double) { guard let url = model?.fileUrl else { return diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellDelegate.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellDelegate.swift index 885ebbe94..455ac51e1 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellDelegate.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellDelegate.swift @@ -27,6 +27,7 @@ protocol BaseChatCellDelegate: class { func openURL(_ url: URL) func showMenuForURL(_ url: URL) func showMention(_ mention: MentionInfo) + func opponentAvatarTapped(_ cell: BaseChatCell) } extension BaseChatCellDelegate { @@ -49,4 +50,5 @@ extension BaseChatCellDelegate { func openURL(_ url: URL) {} func showMenuForURL(_ url: URL) {} func showMention(_ mention: MentionInfo) {} + func opponentAvatarTapped(_ cell: BaseChatCell) {} } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/MessageSender.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/MessageSender.swift index be4c7c0d6..86d349b4b 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/MessageSender.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/MessageSender.swift @@ -7,10 +7,11 @@ // struct MessageSender { - static let `deleted` = MessageSender(fullname: "deleted account name".localized, + static let `deleted` = MessageSender(phoneId: nil, + fullname: "deleted account name".localized, nick: "deleted account name".localized, avatar: nil) - + let phoneId: String? let fullname: String let nick: String? let avatar: String? diff --git a/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift b/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift index bb60d1b9a..7791c2480 100644 --- a/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift +++ b/Nynja/Modules/Replies/Interactor/RepliesInteractor.swift @@ -97,13 +97,13 @@ final class RepliesInteractor: BaseInteractor, RepliesInteractorInputProtocol, M func sender(for message: Message) -> MessageSender? { let isOwner = message.from == StorageService.sharedInstance.phoneId if let _ = (message.feed_id as? muc), let member = member(for: message) { - return MessageSender(fullname: member.fullName ?? "", nick: member.alias, avatar: member.avatar) + return MessageSender(phoneId: member.phone_id, fullname: member.fullName ?? "", nick: member.alias, avatar: member.avatar) } else { if isOwner, let phoneId = StorageService.sharedInstance.phoneId, let myContact = ContactDAO.findContactBy(phoneId: phoneId) { - return MessageSender(fullname: myContact.fullName ?? "", nick: myContact.nick, avatar: myContact.avatar) + return MessageSender(phoneId: myContact.phone_id, fullname: myContact.fullName ?? "", nick: myContact.nick, avatar: myContact.avatar) } else { if let to = (message.feed_id as? p2p)?.to, let contact = ContactDAO.findContactBy(phoneId: to) { - return MessageSender(fullname: contact.fullName ?? "", nick: contact.nick, avatar: contact.avatar) + return MessageSender(phoneId: contact.phone_id, fullname: contact.fullName ?? "", nick: contact.nick, avatar: contact.avatar) } } } -- GitLab From 56a201ab4e84ab61eea5e433f6071dbb05c79c56 Mon Sep 17 00:00:00 2001 From: reznik94 Date: Mon, 13 Aug 2018 02:52:03 +0300 Subject: [PATCH 24/32] =?UTF-8?q?[NY-2381]=20Download=20of=20file=20/=20vi?= =?UTF-8?q?deo=20stops=20after=20reconnect=20to=20Internet=20=E2=80=A6=20(?= =?UTF-8?q?#1090)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Nynja.xcodeproj/project.pbxproj | 6 +-- Nynja/ProgressModel.swift | 22 +++++---- .../Operations/DownloadOperation.swift | 42 +++-------------- Nynja/SyncFileManager/SyncFileManager.swift | 45 ++++++++----------- Nynja/TransferManager.swift | 24 ++++++---- 5 files changed, 55 insertions(+), 84 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 47e2dce9b..33baf44e4 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -15492,7 +15492,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "0ac45157-141b-4e67-9a87-602efcd8cb35"; + PROVISIONING_PROFILE = "69f3dc99-df33-4a29-8a8a-8d93926a3535"; PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; SKIP_INSTALL = YES; SWIFT_VERSION = 4.0; @@ -15665,7 +15665,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "e9cc21bd-73cb-4b39-92ab-097127d12162"; + PROVISIONING_PROFILE = "dc81b7c8-7ca8-4e18-9fee-d0f512f7c8ca"; PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -15785,7 +15785,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "056407e9-6a56-4005-9a4c-8c142132e54f"; + PROVISIONING_PROFILE = "dc81b7c8-7ca8-4e18-9fee-d0f512f7c8ca"; PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; diff --git a/Nynja/ProgressModel.swift b/Nynja/ProgressModel.swift index 2ef1ce78c..a9828c2a5 100644 --- a/Nynja/ProgressModel.swift +++ b/Nynja/ProgressModel.swift @@ -14,22 +14,26 @@ class ProgressModel { case done } - var url: URL - var progress: Float - var speed: Double - public private (set) var speedStringRepresentation: String = "" + var url: URL + var progress: Float = 0 + var speed: Double = 0 var result: URL? var status: ProgressStatus - var fileSize: Int64 + var fileSize: Int64 = 0 - required init(url: URL, status: ProgressStatus, result: URL? = nil, transferInfo: TransferInfo) { + required init(url: URL, status: ProgressStatus, result: URL? = nil, transferInfo: TransferInfo? = nil) { self.url = url - self.progress = transferInfo.progress - self.speed = transferInfo.speed + + if let info = transferInfo { + self.progress = info.progress + self.speed = info.speed + self.fileSize = info.fileSize + } + self.speedStringRepresentation = ProgressModel.getSpeedStringRepresentation(with: self.speed) - self.fileSize = transferInfo.fileSize + self.status = status self.result = result } diff --git a/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift b/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift index 23b35d03b..aa9faaea0 100644 --- a/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift +++ b/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift @@ -13,7 +13,7 @@ class DownloadOperation: AsyncOperation { private var message: Message! private weak var delegate: MessageProcessingDelegate? private weak var listener: TransferProgressListener? - private(set) var url: URL! + private(set) var url: URL init(msg: Message, url: URL, delegate: MessageProcessingDelegate?, listener: TransferProgressListener?) { self.message = msg @@ -23,42 +23,10 @@ class DownloadOperation: AsyncOperation { } override func main() { - let group = DispatchGroup() - - if let thumbUrl = message.thumbUrl { - group.enter() - - TransferManager.shared.download(url: thumbUrl, listener: nil) { [weak self] result in - self?.thumbnailCompletion(thumbUrl: thumbUrl) - - group.leave() - } - } - - group.enter() - - TransferManager.shared.download(url: url, listener: self.listener) { result in - group.leave() - } - - group.notify(queue: DispatchQueue.main) { [weak self] in - self?.operationCompletion() + TransferManager.shared.download(url: url, listener: listener) { [weak self] result in + guard let `self` = self else { return } + self.delegate?.updateProgress(TransferManager.shared.progressForUrl(self.url)) + self.state = .finished } } } - -// MARK: - Private - -private extension DownloadOperation { - func operationCompletion() { - delegate?.updateProgress(TransferManager.shared.progressForUrl(url)) - state = .finished - } - - func thumbnailCompletion(thumbUrl: URL) { - let transferInfo = TransferInfo(progress: 0, speed: 0, fileSize: 0) - let progress = ProgressModel(url: thumbUrl, status: .done, transferInfo: transferInfo) - - delegate?.updateProgress(progress) - } -} diff --git a/Nynja/SyncFileManager/SyncFileManager.swift b/Nynja/SyncFileManager/SyncFileManager.swift index 1a9a4f2ad..e445f52c6 100644 --- a/Nynja/SyncFileManager/SyncFileManager.swift +++ b/Nynja/SyncFileManager/SyncFileManager.swift @@ -65,7 +65,7 @@ class SyncFileManager { } } - func getFileLink(url: String, from destination: RemoteStorageDestination = .default, completion: ((_ localURL:URL?,_ transferInfo: TransferInfo?, _ request: AWSRequest?)->())?) { + func getFileLink(url: String, from destination: RemoteStorageDestination = .default, completion: ((_ localURL:URL?,_ transferInfo: TransferInfo?, _ request: AWSRequest?) -> ())?) { if url.first == "/" { if url.split(separator: "/").count > 2 { @@ -91,36 +91,29 @@ class SyncFileManager { if isAvaliable { completion?(URL(fileURLWithPath: fullPath), TransferInfo.finished, nil) } else { - let request = self.downloader.download(url: inputURL, from: destination) { res, transferInfo in - if res != nil { - self.updateDB(url: inputURL, localUrl: res!) { - if let fullPath = FileManagerService.sharedInstance.getFullPathWith(string: res!) { - completion?(URL(fileURLWithPath: fullPath), TransferInfo.finished, nil) - } - } - } else if transferInfo != nil { - completion?(nil, transferInfo, nil) - } else if res == nil && transferInfo == nil { - completion?(nil, nil, nil) - } - } + let request = self.makeDownloadRequest(url: inputURL, from: destination, completion: completion) completion?(nil, nil, request) } } else { - let request = self.downloader.download(url: url, from: destination) { res, progress in - if res != nil { - self.updateDB(url: url, localUrl: res!) { - if let fullPath = FileManagerService.sharedInstance.getFullPathWith(string: res!) { - completion?(URL(fileURLWithPath: fullPath), TransferInfo.finished, nil) - } - } - } else if progress != nil { - completion?(nil, progress, nil) - } else if res == nil && progress == nil { - completion?(nil, nil, nil) + let request = self.makeDownloadRequest(url: url, from: destination, completion: completion) + completion?(nil, nil, request) + } + } + } + + func makeDownloadRequest(url: String, from destination: RemoteStorageDestination, completion: ((URL?, TransferInfo?, AWSRequest?) -> ())?) -> AWSRequest? { + return self.downloader.download(url: url, from: destination) { res, progress in + if let localUrl = res { + self.updateDB(url: url, localUrl: localUrl) { + guard let fullPath = FileManagerService.sharedInstance.getFullPathWith(string: localUrl) else { + return } + completion?(URL(fileURLWithPath: fullPath), TransferInfo.finished, nil) } - completion?(nil, nil, request) + } else if let progress = progress { + completion?(nil, progress, nil) + } else { + completion?(nil, nil, nil) } } } diff --git a/Nynja/TransferManager.swift b/Nynja/TransferManager.swift index a1ab38e0d..5b229b6b5 100644 --- a/Nynja/TransferManager.swift +++ b/Nynja/TransferManager.swift @@ -15,7 +15,7 @@ protocol TransferProgressListener : class { private class ProcessingDescription { var listener: TransferProgressListener? = nil - var transferInfo: TransferInfo = TransferInfo(progress: 0, speed: 0, fileSize: 0) + var transferInfo: TransferInfo? = TransferInfo(progress: 0, speed: 0, fileSize: 0) var request: AWSRequest? var url: URL? } @@ -33,7 +33,7 @@ class TransferManager { func isURLProcessing(_ url: URL) -> Bool { if let processing = getProcessing(for: url) { - return processing.transferInfo.progress != 1 + return processing.transferInfo?.progress != 1 } return false } @@ -41,14 +41,17 @@ class TransferManager { func progressForUrl(_ url: URL) -> ProgressModel { if let description = getProcessing(for: url) { + guard let transferInfo = description.transferInfo else { + return ProgressModel(url: url, status: .notStarted) + } if description.request != nil { - if description.transferInfo.progress == 1.0 { - return ProgressModel(url: url, status: .done, transferInfo: description.transferInfo) + if transferInfo.progress == 1.0 { + return ProgressModel(url: url, status: .done, transferInfo: transferInfo) } else { - return ProgressModel(url: url, status: .atProgress, transferInfo: description.transferInfo) + return ProgressModel(url: url, status: .atProgress, transferInfo: transferInfo) } } else { - return ProgressModel(url: url, status: .notStarted, transferInfo: description.transferInfo) + return ProgressModel(url: url, status: .notStarted, transferInfo: transferInfo) } } @@ -147,12 +150,15 @@ class TransferManager { } func cancelledDownloading(url: URL) { - let transferInfo = TransferInfo(progress: 0, speed: 0, fileSize: 0) - let progress = ProgressModel(url: url, status: .notStarted, transferInfo: transferInfo) + guard let processing = getProcessing(for: url) else { + return + } + processing.transferInfo = nil + let progress = ProgressModel(url: url, status: .notStarted, transferInfo: nil) notifyAboutProgress(progress: progress) } - func updateProgress(url: URL, transferInfo: TransferInfo) { + func updateProgress(url: URL, transferInfo: TransferInfo?) { guard let processing = getProcessing(for: url) else { return } -- GitLab From 42401b968463a7e334531ff317c5a375401d320e Mon Sep 17 00:00:00 2001 From: Volodymyr Hryhoriev Date: Mon, 13 Aug 2018 11:39:44 +0300 Subject: [PATCH 25/32] =?UTF-8?q?[NY-1265]=20=D0=A1an't=20get=20all=20mess?= =?UTF-8?q?ages=20after=20user=20being=20offline=09=20(#1046)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [NY-1265] Fix issue with `Сan't get all messages after user being offline` which was found on `Chat` screen. --- Nynja.xcodeproj/project.pbxproj | 18 +- Nynja/ChatService.swift | 47 ++++- Nynja/DB/Models/DBMessage.swift | 115 +++++++----- Nynja/DB/Tables/MessageActionTable.swift | 7 +- Nynja/DatabaseManager.swift | 7 - Nynja/HistoryRequestModelTypeProtocol.swift | 24 +++ Nynja/MQTTModels/MessageExtension+BERT.swift | 2 +- Nynja/MessageDAO.swift | 170 +++++++----------- Nynja/MessageDAOProtocol.swift | 8 +- Nynja/MigrationManager.swift | 8 + Nynja/Models/ChatModel.swift | 18 -- .../Interactor/MessageInteractor+Fetch.swift | 65 ++++--- .../MessageInteractor+StorageSubscriber.swift | 7 +- .../Interactor/MessageInteractor+Utils.swift | 6 +- .../Interactor/MessageInteractor.swift | 102 ++++++++--- ...essagePresenter+MentionUnreadCounter.swift | 10 +- .../Message/Presenter/MessagePresenter.swift | 16 +- .../Message/Protocols/MessageProtocols.swift | 10 +- Nynja/Modules/Message/View/MessageVC.swift | 4 +- .../ChatScreenAlertFactory.swift | 4 +- .../HistoryRequestModelFactory.swift | 19 +- .../HandleServices/HistoryHandler.swift | 29 +-- .../HandleServices/MessageHandler.swift | 32 ++-- .../Services/Models/HistoryRequestModel.swift | 60 ++++++- .../Models/Message/MessageExtension.swift | 2 + 25 files changed, 471 insertions(+), 319 deletions(-) create mode 100644 Nynja/HistoryRequestModelTypeProtocol.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 33baf44e4..ffc746cb8 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1461,6 +1461,7 @@ A4679BBB20B305360021FE9C /* LinkField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BB820B305360021FE9C /* LinkField.swift */; }; A4688DFA20650FF50013660D /* DBObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4688DF920650FF50013660D /* DBObserver.swift */; }; A4688DFC20652DE30013660D /* StorageChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4688DFB20652DE30013660D /* StorageChange.swift */; }; + A46CF04321147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46CF04221147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift */; }; A47785A220D18D4A0053E0D2 /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70402BC1FF6972B00182D81 /* BaseView.swift */; }; A47785A420D286680053E0D2 /* ChannelChatItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47785A320D286680053E0D2 /* ChannelChatItemsFactory.swift */; }; A477CE7D2061236800081D34 /* MessageLinkDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A477CE7C2061236800081D34 /* MessageLinkDAOProtocol.swift */; }; @@ -3335,6 +3336,7 @@ A4679BB820B305360021FE9C /* LinkField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkField.swift; sourceTree = ""; }; A4688DF920650FF50013660D /* DBObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBObserver.swift; sourceTree = ""; }; A4688DFB20652DE30013660D /* StorageChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageChange.swift; sourceTree = ""; }; + A46CF04221147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryRequestModelTypeProtocol.swift; sourceTree = ""; }; A47785A320D286680053E0D2 /* ChannelChatItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelChatItemsFactory.swift; sourceTree = ""; }; A477CE7C2061236800081D34 /* MessageLinkDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLinkDAOProtocol.swift; sourceTree = ""; }; A477CE8120613A0900081D34 /* StarMessageDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMessageDAOProtocol.swift; sourceTree = ""; }; @@ -5709,6 +5711,7 @@ 3AC321761EEAC4700068F3C8 /* Models */ = { isa = PBXGroup; children = ( + A46CF04121147B9D0072F185 /* HistoryRequestModel */, 3A62B7D71F4CB9D100F45B51 /* BaseMQTTModel.swift */, FBCE83D620E523A8003B7558 /* WalletMQTTModel.swift */, A4F3DAAE2084944900FF71C7 /* RoomModel.swift */, @@ -5717,7 +5720,6 @@ 3A771CB11F193945008D968A /* UpdateRosterModel.swift */, 3AA13C751F2252F900BE5D8F /* SearchModel.swift */, 3A2374DA1F26458300701045 /* FriendRequstModel.swift */, - 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */, 3A21EFFB1F3B154A00AE61EC /* SendModel.swift */, 265AEA161FE9AFD400AC4806 /* MemberModel.swift */, 263D662C1FE8D03400A509F8 /* TypingModel.swift */, @@ -9632,6 +9634,15 @@ path = LinkField; sourceTree = ""; }; + A46CF04121147B9D0072F185 /* HistoryRequestModel */ = { + isa = PBXGroup; + children = ( + 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */, + A46CF04221147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift */, + ); + name = HistoryRequestModel; + sourceTree = ""; + }; A477CE7B2061235700081D34 /* MessageLink */ = { isa = PBXGroup; children = ( @@ -12830,7 +12841,7 @@ "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", @@ -12857,7 +12868,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", @@ -14554,6 +14565,7 @@ 73BFE52F809536A538E6A55E /* ImagePreviewViewController.swift in Sources */, 850C0B2620E00C3E003341D0 /* UIScreen+Keyboard.swift in Sources */, 8503B51B205036F2006F0593 /* BaseNynjaButton.swift in Sources */, + A46CF04321147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift in Sources */, 8538012C2052E29D002C6960 /* SoundTableHeaderView.swift in Sources */, 3A8045D31F60C8E200AED866 /* MQTTServiceProfile.swift in Sources */, B7F505192061158800C28FA1 /* SettingsArrowCellViewModel.swift in Sources */, diff --git a/Nynja/ChatService.swift b/Nynja/ChatService.swift index 3fa4ac54c..b6e279d3f 100644 --- a/Nynja/ChatService.swift +++ b/Nynja/ChatService.swift @@ -50,7 +50,7 @@ final class ChatService { } @discardableResult - static func updateLastMessage(_ message: Message, shouldUpdate: ((Int64) -> Bool)? = nil) -> Bool { + static func updateLastMessage(_ message: Message, shouldChangeUnread: Bool = true, shouldUpdate: ((Int64) -> Bool)? = nil) -> Bool { guard let chatModel = ChatService.fetchChatModel(from: message) else { return false } if let shouldUpdate = shouldUpdate, let lastId = chatModel.last_msg?.id, !shouldUpdate(lastId) { @@ -58,8 +58,8 @@ final class ChatService { } chatModel.last_msg = message - - let shouldIncrementUnread = (!message.isOwn || message.isCursor) && !message.isEdited + + let shouldIncrementUnread = (!message.isOwn || message.isCursor) && !message.isEdited && shouldChangeUnread if shouldIncrementUnread { let counter = chatModel.unreadCount + 1 chatModel.unread = counter @@ -81,15 +81,50 @@ final class ChatService { } } + // MARK: - Remove message + static func removeMessages(_ messages: [Message]) { + messages.forEach { removeMessage($0, shouldUpdateChat: false) } + } + + static func removeMessage(_ message: Message, shouldUpdateChat: Bool = true) { + if MessageDAO.removeMessage(using: message) { + guard let id = message.link else { + return + } + + if let action = MessageActionDAO.fetchMessageAction(by: id) { + try? StorageService.sharedInstance.perform(action: .delete, with: action) + } + + if let fetchType = self.fetchType(from: message), + let lastMessage = MessageDAO.fetchLastMessage(of: fetchType) { + + ChatService.updateLastMessage(lastMessage, shouldChangeUnread: false) { (lastMessageId) -> Bool in + return id == lastMessageId + } + } + } + } + + private static func fetchType(from message: Message) -> FetchType? { + if let p2p = message.p2pFeed, let from = p2p.from, let to = p2p.to { + return .p2p(from: from, to: to) + } else if let muc = message.mucFeed, let name = muc.name { + return .muc(name: name) + } + + return nil + } + // MARK: - Clear History static func clearHistory(_ message: Message) { do { if let muc = message.feed_id as? muc, let name = muc.name { try MessageDAO.clearHistory(FetchType.muc(name: name)) - updateChatModel(message) + updateChatModelAfterClear(with: message) } else if let p2p = message.feed_id as? p2p, let from = p2p.from, let to = p2p.to { try MessageDAO.clearHistory(FetchType.p2p(from: from, to: to)) - updateChatModel(message) + updateChatModelAfterClear(with: message) } } catch {} } @@ -109,7 +144,7 @@ final class ChatService { } catch {} } - private static func updateChatModel(_ message: Message) { + private static func updateChatModelAfterClear(with message: Message) { do { try storageService.perform(action: .save, with: message) diff --git a/Nynja/DB/Models/DBMessage.swift b/Nynja/DB/Models/DBMessage.swift index ca7f2db49..e936ee596 100644 --- a/Nynja/DB/Models/DBMessage.swift +++ b/Nynja/DB/Models/DBMessage.swift @@ -8,6 +8,8 @@ import GRDBCipher +private let tableName = MessageTable.name + class DBMessage: Record, DBModelProtocol { var id: Int64? @@ -63,7 +65,7 @@ class DBMessage: Record, DBModelProtocol { // MARK: - Record override static var databaseTableName: String { - return MessageTable.name + return tableName } required init(row: Row) { @@ -179,41 +181,39 @@ class DBMessage: Record, DBModelProtocol { return message } + static func penultimateMessage(_ db: Database, ofType fetchType: FetchType) throws -> DBMessage? { + guard let lastMessage = try lastMessage(db, ofType: fetchType), + let lastMessageServerId = lastMessage.serverId else { + return nil + } + + let condition = conditionBefore(messageServerId: lastMessageServerId) + + var sql: String + + switch fetchType { + case let .p2p(from, to): + sql = sqlP2p(from: from, to: to, conditions: condition, ordered: .desc) + case let .muc(mucName): + sql = sqlMuc(name: mucName, conditions: condition, ordered: .desc) + } + + let message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) + try message?.construct(db) + return message + } + static func lastMessage(_ db: Database, ofType fetchType: FetchType) throws -> DBMessage? { - let message: DBMessage? + var sql: String switch fetchType { case let .p2p(from, to): - let messageTable = MessageTable.name - let p2pTable = P2pTable.name - - let sql = """ - select \(messageTable).* - from \(messageTable) - inner join \(p2pTable) on \(messageTable).[\(MessageTable.Column.feedId.title)] = \(p2pTable).[\(P2pTable.Column.id.title)] - where \(p2pTable).[\(P2pTable.Column.from.title)] = '\(from)' - and \(p2pTable).[\(P2pTable.Column.to.title)] = '\(to)' - and \(messageTable).[\(MessageTable.Column.feedType.title)] = \(FeedType.p2p.rawValue) - and \(messageTable).[\(MessageTable.Column.status.title)] != 'delete' - order by \(messageTable).[\(MessageTable.Column.created.title)] DESC - """ - - message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) - + sql = sqlP2p(from: from, to: to, ordered: .desc) case let .muc(mucName): - guard let feedId = try DBMuc.muc(db, name: mucName)?.id else { return nil } - - let sql = """ - select * - from \(MessageTable.name) - where \(MessageTable.Column.feedId) = \(feedId) - and \(MessageTable.Column.feedType.title) = '\(FeedType.muc.rawValue)' - and \(MessageTable.Column.status.title) != 'delete' - order by \(MessageTable.Column.created.title) DESC - """ - - message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) + sql = sqlMuc(name: mucName, ordered: .desc) } + + let message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) try message?.construct(db) return message } @@ -227,7 +227,7 @@ class DBMessage: Record, DBModelProtocol { if !serverIds.isEmpty { let ids = serverIds.joinedByComma() - conditions = "and \(MessageTable.name).\(MessageTable.Column.serverId.title) in (\(ids))" + conditions = "and \(tableName).\(MessageTable.Column.serverId.title) in (\(ids))" return try messages(db, fetchType: fetchType, conditions: conditions) } else { return [] @@ -235,7 +235,7 @@ class DBMessage: Record, DBModelProtocol { } static func messages(_ db: Database, fetchType: FetchType, before messageServerId: Int64) throws -> [DBMessage] { - let condition = "and \(MessageTable.name).\(MessageTable.Column.serverId.title) < \(messageServerId)" + let condition = conditionBefore(messageServerId: messageServerId) return try messages(db, fetchType: fetchType, conditions: condition) } @@ -317,7 +317,7 @@ class DBMessage: Record, DBModelProtocol { } static private func requestUnsentMessages(from: String) -> AnyTypedRequest { - let messageTable = MessageTable.name + let messageTable = tableName let sql = """ select \(messageTable).* @@ -325,24 +325,40 @@ class DBMessage: Record, DBModelProtocol { where \(messageTable).[\(MessageTable.Column.from.title)] = '\(from)' and \(messageTable).[\(MessageTable.Column.serverId.title)] is null and \(messageTable).\(MessageTable.Column.feedType.title) = \(FeedType.p2p.rawValue) - order by \(messageTable).[\(MessageTable.Column.created.title)] + \(orderedBy(.asc)) """ return SQLRequest(sql).asRequest(of: DBMessage.self) } static private func requestP2pMessages(from: String, to: String, conditions: String = "") -> AnyTypedRequest { - let sql = String(format: sqlP2p(from: from, to: to), conditions) + let sql = sqlP2p(from: from, to: to, conditions: conditions, ordered: .asc) return SQLRequest(sql).asRequest(of: DBMessage.self) } static private func requestMucMessages(name: String, conditions: String = "") -> AnyTypedRequest { - let sql = String(format: sqlMuc(name: name), conditions) + let sql = sqlMuc(name: name, conditions: conditions, ordered: .asc) return SQLRequest(sql).asRequest(of: DBMessage.self) } - static private func sqlP2p(from: String, to: String) -> String { - let messageTable = MessageTable.name + + // MARK: - Raw SQL + + static private var skippedStatuses: String { + let statusColumn = "\(tableName).\(MessageTable.Column.status.title)" + return "(\(statusColumn) is null or \(statusColumn) not in ('delete', 'edit', 'deleted'))" + } + + static private func conditionBefore(messageServerId: MessageServerId) -> String { + return "and \(tableName).\(MessageTable.Column.serverId.title) < \(messageServerId)" + } + + static private func orderedBy(_ ordered: Ordered) -> String { + return "order by \(tableName).[\(MessageTable.Column.created.title)] \(ordered.rawValue)" + } + + static private func sqlP2p(from: String, to: String, conditions: String = "", ordered: Ordered) -> String { + let messageTable = tableName let p2pTable = P2pTable.name let sql = """ @@ -352,15 +368,16 @@ class DBMessage: Record, DBModelProtocol { where \(p2pTable).[\(P2pTable.Column.from.title)] = '\(from)' and \(p2pTable).[\(P2pTable.Column.to.title)] = '\(to)' and \(messageTable).\(MessageTable.Column.feedType.title) = \(FeedType.p2p.rawValue) + and \(skippedStatuses) %@ - order by \(messageTable).[\(MessageTable.Column.created.title)] + \(orderedBy(ordered)) """ - return sql + return String(format: sql, conditions) } - static private func sqlMuc(name: String) -> String { - let messageTable = MessageTable.name + static private func sqlMuc(name: String, conditions: String = "", ordered: Ordered) -> String { + let messageTable = tableName let mucTable = MucTable.name let sql = """ @@ -369,10 +386,20 @@ class DBMessage: Record, DBModelProtocol { left join \(messageTable) on \(mucTable).id = \(messageTable).feedId where \(mucTable).[\(MucTable.Column.name.title)] = '\(name)' and \(messageTable).\(MessageTable.Column.feedType.title) = \(FeedType.muc.rawValue) + and \(skippedStatuses) %@ - order by \(messageTable).[\(MessageTable.Column.created.title)] + \(orderedBy(ordered)) """ - return sql + return String(format: sql, conditions) + } +} + +private extension DBMessage { + + enum Ordered: String { + case asc + case desc } + } diff --git a/Nynja/DB/Tables/MessageActionTable.swift b/Nynja/DB/Tables/MessageActionTable.swift index b558a55de..816b7e561 100644 --- a/Nynja/DB/Tables/MessageActionTable.swift +++ b/Nynja/DB/Tables/MessageActionTable.swift @@ -16,15 +16,10 @@ class MessageActionTable: Table { static func create(in db: Database) throws { try db.create(table: MessageActionTable.name, body: { (t) in - t.column(Column.messageId, .integer) + t.column(Column.messageId, .integer).primaryKey(onConflict: .replace, autoincrement: false) t.column(Column.seenBy, .integer) t.column(Column.phoneId, .text).defaults(to: "") t.column(Column.action, .text).defaults(to: false) - - t.uniqueKey([Column.messageId.title], onConflict: .replace) -// t.uniqueKey([Column.messageId.title, Column.seenBy.title, Column.action.title], onConflict: .replace) - -// t.foreignKey([Column.messageId.title], references: MessageTable.name, columns: nil, onDelete: .cascade, onUpdate: nil, deferred: false) }) } diff --git a/Nynja/DatabaseManager.swift b/Nynja/DatabaseManager.swift index 545449390..98d1d6532 100644 --- a/Nynja/DatabaseManager.swift +++ b/Nynja/DatabaseManager.swift @@ -139,13 +139,6 @@ final class DatabaseManager: DBManagerProtocol { try values.forEach { (value) in try database.execute("insert into grdb_migrations (identifier) values('\(value)')") } - //let statement = "INSERT INTO \(tableName) (\(identifierColumn)) VALUES (\();" - -// let statements = values.map { -// return "INSERT INTO \(tableName) (\(identifierColumn)) VALUES (`\($0)`)" -// } -// -// try database.execute(statement, arguments: values) } } diff --git a/Nynja/HistoryRequestModelTypeProtocol.swift b/Nynja/HistoryRequestModelTypeProtocol.swift new file mode 100644 index 000000000..99c88972c --- /dev/null +++ b/Nynja/HistoryRequestModelTypeProtocol.swift @@ -0,0 +1,24 @@ +// +// HistoryRequestModelTypeProtocol.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/3/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol HistoryRequestModelTypeProtocol { + var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { get } +} + +extension Contact: HistoryRequestModelTypeProtocol { + var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { + return phone_id.flatMap { .p2p(opponentId: $0) } + } +} + +extension Room: HistoryRequestModelTypeProtocol { + var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { + return id.flatMap { .muc(mucId: $0) } + } +} + diff --git a/Nynja/MQTTModels/MessageExtension+BERT.swift b/Nynja/MQTTModels/MessageExtension+BERT.swift index 484da32c7..970f0ec64 100644 --- a/Nynja/MQTTModels/MessageExtension+BERT.swift +++ b/Nynja/MQTTModels/MessageExtension+BERT.swift @@ -10,7 +10,7 @@ import Foundation extension Message { - func getBert(chain : String? = "chain") -> BertObject { + func getBert(chain: String? = "chain") -> BertObject { let topic = BertAtom(fromString: "Message") let _from = Bert.getBin(self.from) let _to = Bert.getBin(self.to) diff --git a/Nynja/MessageDAO.swift b/Nynja/MessageDAO.swift index 8e0e19371..67b936cbb 100644 --- a/Nynja/MessageDAO.swift +++ b/Nynja/MessageDAO.swift @@ -18,38 +18,44 @@ class MessageDAO: MessageDAOProtocol { // MARK: - Fetch // MARK: -- Message static func fetchMessage(by rowId: Int64) -> Message? { - guard let message = dbManager.fetch({ db in + return fetchMessage { db in return try DBMessage.message(from: db, rowId: rowId) - }) else { - return nil } - - return Message(message: message) } static func fetchMessage(primaryId: Int64) -> Message? { - guard let message = dbManager.fetch({ db in + return fetchMessage { db in return try DBMessage.message(db, id: primaryId) - }) else { - return nil } - - return Message(message: message) } static func fetchMessage(serverId: Int64) -> Message? { - guard let message = dbManager.fetch({ db in + return fetchMessage { db in return try DBMessage.message(db, serverId: serverId) - }) else { - return nil } - - return Message(message: message) } static func fetchMessage(localId: String) -> Message? { - guard let message = dbManager.fetch({ db in + return fetchMessage { db in return try DBMessage.message(db, localId: localId) + } + } + + static func fetchLastMessage(of type: FetchType) -> Message? { + return fetchMessage { db in + return try DBMessage.lastMessage(db, ofType: type) + } + } + + static func fetchPenultimateMessage(of type: FetchType) -> Message? { + return fetchMessage { db in + return try DBMessage.penultimateMessage(db, ofType: type) + } + } + + private static func fetchMessage(using closure: (Database) throws -> DBMessage?) -> Message? { + guard let message = dbManager.fetch({ db in + return try closure(db) }) else { return nil } @@ -125,44 +131,50 @@ class MessageDAO: MessageDAOProtocol { } // MARK: - Remove - static func removeMessage(message: Message, notify: ((_ message: Message) -> Void)? = nil) { - let msg = Message() - msg.id = message.link - msg.feed_id = message.feed_id - if message.feed_id is p2p { - if let ids = message.seenby as? [Int64] { - let res = ids.count { $0 == -1 } - if res > 0 { - removeMessage(msg: msg) - notify?(msg) - return - } - } - if let ids = message.seenby as? [String], let phoneId = ids.first { - if phoneId == StorageService.sharedInstance.phoneId { - removeMessage(msg: msg) - notify?(msg) - return - } - } - return - } - if let id = (message.feed_id as? muc)?.name { - if let ids = message.seenby as? [Int64] { - let res = ids.count { $0 == -1 } - if res > 0 { - removeMessage(msg: msg) - notify?(msg) - return - } - if let isNeedRemove = RoomDAO.containsMembers(with: ids, roomId: id), isNeedRemove { - removeMessage(msg: msg) - notify?(msg) - return - } - } - return + + static func removeMessage(using message: Message) -> Bool { + guard let id = message.link, + let deletedMessage = fetchMessage(serverId: id) else { + return false + } + + let shouldMarkMessageAsDelete = self.shouldMarkMessageAsDelete(message) + if shouldMarkMessageAsDelete { + deletedMessage.status = StringAtom(string: "deleted") } + deletedMessage.seenby = message.seenby + + do { + try dbManager.perform(action: .save, with: deletedMessage) + } catch { + return false + } + + return shouldMarkMessageAsDelete + } + + private static func shouldMarkMessageAsDelete(_ message: Message) -> Bool { + let stringIds = message.seenby as? [String] ?? [] + let intIds = message.seenby as? [Int64] ?? [] + + guard !stringIds.contains("-1"), + !intIds.contains(-1) else { + return true + } + + guard let phoneId = StorageService.sharedInstance.phoneId else { + assertionFailure("Check phoneId") + return false + } + + if let _ = message.p2pFeed { + return stringIds.contains(phoneId) + } else if let roomId = message.mucFeed?.name, + let _ = MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId) { + return true + } + + return false } private static func findLocalId(serverId: Int64) -> String? { @@ -178,58 +190,6 @@ class MessageDAO: MessageDAOProtocol { } } - private static func removeMessage(msg: Message) { - var message = msg - message = updateLocalMessageId(for: message) - try? StorageService.sharedInstance.perform(action: .delete, with: message) - if let feed = msg.feed_id { - updateLastMessage(feed: feed) - } - } - - private static func updateLastMessage(feed: AnyObject) { - if let m = feed as? muc, let name = m.name { - guard let lastMessage = lastMessage(of: .muc(name: name)), - let room = RoomDAO.fetchRoom(by: name) else { - return - } - - room.messageId = lastMessage.id - - let action: DatabaseAction = .updateColumns([RoomTable.Column.messageId.title]) - try? StorageService.sharedInstance.perform(action: action, with: room) - } - if let p = feed as? p2p, let to = p.to, let fr = p.from { - guard let target = p.opponentId, - let lastMessage = lastMessage(of: .p2p(from: fr, to: to)), - let contact = ContactDAO.fetchContact(by: target) else { - return - } - - contact.messageId = lastMessage.id - - let action: DatabaseAction = .updateColumns([ContactTable.Column.messageId.title]) - try? StorageService.sharedInstance.perform(action: action, with: contact) - } - } - - private static func lastMessage(of fetchType: FetchType) -> DBMessage? { - return dbManager.fetch { db in - return try DBMessage.lastMessage(db, ofType: fetchType) - } - } - - // MARK: - Update - static func updateLocalMessageId(for message: Message) -> Message { - var localID: String? = nil - - if let id = message.id, let res = findLocalId(serverId: id) { - localID = res - } - - message.msg_id = localID - return message - } // MARK: - Helpers diff --git a/Nynja/MessageDAOProtocol.swift b/Nynja/MessageDAOProtocol.swift index 831db64c8..0db692d17 100644 --- a/Nynja/MessageDAOProtocol.swift +++ b/Nynja/MessageDAOProtocol.swift @@ -23,6 +23,9 @@ protocol MessageDAOProtocol: DAOProtocol { static func fetchMessage(serverId: Int64) -> Message? static func fetchMessage(localId: String) -> Message? + static func fetchLastMessage(of type: FetchType) -> Message? + static func fetchPenultimateMessage(of type: FetchType) -> Message? + // MARK: -- Unsent static func fetchUnsentMessages(from: String) -> [Message] @@ -40,9 +43,6 @@ protocol MessageDAOProtocol: DAOProtocol { static func clearHistory(_ type: FetchType) throws // MARK: - Remove - static func removeMessage(message: Message, notify: ((_ message: Message) -> Void)?) - - // MARK: - Update - static func updateLocalMessageId(for message: Message) -> Message + static func removeMessage(using message: Message) -> Bool } diff --git a/Nynja/MigrationManager.swift b/Nynja/MigrationManager.swift index 333819072..238d326c1 100644 --- a/Nynja/MigrationManager.swift +++ b/Nynja/MigrationManager.swift @@ -20,6 +20,7 @@ enum Migration: Int, Describable { case createLinkForRoom case renameChannelFeatureKeys case updateMessageIdForeignKeyOnContactRoomTables + case primaryKeyMessageAction static var allTitles: [String] = { var i = 0 @@ -223,6 +224,13 @@ class MigrationManager { try links.forEach { try $0.save(db) } try roomMembers.forEach { try $0.save(db) } } + + migrator.registerMigration(Migration.primaryKeyMessageAction.title) { db in + let actions = try DBMessageAction.fetchAll(db) + try db.drop(table: MessageActionTable.name) + try MessageActionTable.create(in: db) + try actions.forEach { try $0.saveAggregate(db) } + } } private func recreateDescTable(_ db: Database, closure: ((DBDesc) -> Void)? = nil) throws { diff --git a/Nynja/Models/ChatModel.swift b/Nynja/Models/ChatModel.swift index 56cea723d..bb50bb25c 100644 --- a/Nynja/Models/ChatModel.swift +++ b/Nynja/Models/ChatModel.swift @@ -6,22 +6,4 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol HistoryRequestModelTypeProtocol { - var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { get } -} - typealias ChatModel = BaseChatModel & DBModelConvertible & HistoryRequestModelTypeProtocol - -extension Contact: HistoryRequestModelTypeProtocol { - var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { - guard let phone_id = phone_id else { return nil } - return .p2p(opponentId: phone_id) - } -} - -extension Room: HistoryRequestModelTypeProtocol { - var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { - guard let id = id else { return nil } - return .muc(mucId: id) - } -} diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift index ea2f35bee..66b480fc7 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift @@ -30,12 +30,18 @@ extension MessageInteractor { return nil } - func fetchCheckpoint() { + private func fetchCheckpoint() { if let feed = self.feed { self.checkpoint = ChatCheckpointDAO.fetchCheckpoint(for: feed) } } + func fetchPenultimateMessage() { + if let type = fetchType { + penultimateMessage = MessageDAO.fetchPenultimateMessage(of: type) + } + } + // MARK: - Fetch Chat Model func fetchRoomFromStorage() { if let r = room, let id = r.id, let room = RoomDAO.findRoom(by: id) { @@ -50,40 +56,26 @@ extension MessageInteractor { // MARK: - Fetch History func fetchFromStorage() { - if let contact = self.contact { - self.feed = FeedDAO.fetchP2p(for: contact.phoneId) - fetchCheckpoint() - } else if let room = self.room, let roomId = room.id { - self.feed = FeedDAO.fetchMuc(for: roomId) - fetchCheckpoint() - } + prepareFeedAndCheckpoint() guard let type = self.fetchType else { - //TODO: - Error return } - let messages = MessageDAO.fetchMessages(type) - + let messages = fetchMessagesFromStorage(for: type) self.configuration.messages = messages - self.lastMessage = messages.last - self.prepareMessagesToUsing(type) } func fetchNewFromStorage() { - if let contact = self.contact { - self.feed = FeedDAO.fetchP2p(for: contact.phoneId) - fetchCheckpoint() - } else if let room = self.room, let roomId = room.id { - self.feed = FeedDAO.fetchMuc(for: roomId) - fetchCheckpoint() - } + prepareFeedAndCheckpoint() - guard let type = fetchType else { return } + guard let type = fetchType else { + return + } let oldMessages = configuration.messages - var newMessages = MessageDAO.fetchMessages(type) + var newMessages = fetchMessagesFromStorage(for: type) if let startID = newMessages.first?.msg_id, oldMessages.index(where: { $0.msg_id == startID }) != nil { // If all history fetched and the first message already exists in local data source. @@ -107,6 +99,35 @@ extension MessageInteractor { } } + private func prepareFeedAndCheckpoint() { + if let contact = self.contact { + self.feed = FeedDAO.fetchP2p(for: contact.phoneId) + } else if let room = self.room, let roomId = room.id { + self.feed = FeedDAO.fetchMuc(for: roomId) + } + + fetchCheckpoint() + } + + private func fetchMessagesFromStorage(for type: FetchType) -> [Message] { + return MessageDAO + .fetchMessages(type) + .filter { message in + guard let desc = message.files?.first, + desc.mime == SendMessageType.audioCall.rawValue else { + return true + } + + guard desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, + let ids = desc.data?.first?.value?.splitByComma(), + let phoneId = storageService.phoneId else { + return false + } + + return ids.contains { $0 == phoneId } + } + } + // MARK: - Setup Configuration private func prepareMessagesToUsing(_ type: FetchType, isNew: Bool = false) { // Auto download/upload diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift index 759c8aa94..530802fb8 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift @@ -16,7 +16,7 @@ extension MessageInteractor { } else if let room = updatedRoom(with: info, type: type) { handleUpdate(room: room) } else if let message = changedMessage(with: info, type: type) { - if info.kind == .delete { + if message.statusString == "deleted" { handleMessageDelete(message) } else { handleMessageInsertOrUpdate(message) @@ -114,6 +114,10 @@ extension MessageInteractor { } private func handleMessageInsertOrUpdate(_ message: Message) { + guard !["delete", "edit"].contains(message.statusString ?? "") else { + return + } + if let contact = self.contact, let phoneId = myContact?.phoneId { let p2pFeed = p2p(firstId: phoneId, secondId: contact.phoneId) @@ -167,7 +171,6 @@ extension MessageInteractor { } private func handleNewMessage(_ message: Message) { - lastMessage = message configuration.messages.append(message) var config: MessageConfiguration diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift index a6ecf9670..7350d048b 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift @@ -8,7 +8,7 @@ extension MessageInteractor { - func nextMessage(from serverId: Int64) -> Message? { + func nextMessage(from serverId: MessageServerId) -> Message? { guard let index = configuration.messages.index(where: { message in if let id = message.id { return id > serverId @@ -29,7 +29,7 @@ extension MessageInteractor { return nil } - func messageBy(serverId: Int64) -> Message? { + func messageBy(serverId: MessageServerId) -> Message? { if let index = indexOfMessage(with: serverId) { return configuration.messages[index] } @@ -42,7 +42,7 @@ extension MessageInteractor { return configuration.messages.index(where: { $0.msg_id == localId }) } - func indexOfMessage(with serverId: Int64) -> Int? { + func indexOfMessage(with serverId: MessageServerId) -> Int? { return configuration.messages.index(where: { $0.id == serverId }) } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 9b8e78de5..2fd3df634 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -11,7 +11,7 @@ import UIKit import CoreLocation final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, TypingHandlerDelegate, IoHandlerDelegate, ReachabilityServiceObserver, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { - + private struct Constants { static let messagesPageSize: Int64 = -15 } @@ -51,9 +51,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H var room: Room? { return chat as? Room } - - var lastMessage: Message? - + var myContact: Contact? { return _contact } @@ -162,9 +160,9 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H }() var initialMessage: ChatInitialMessage? + var penultimateMessage: Message? // MARK: -- Private - private var prevLastMessageId: Int64? private lazy var _contact: Contact? = { return ContactDAO.currentContact @@ -174,7 +172,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private var presenceTimer: TimerHandler? - private var originMessagesIdOfReply: [Int64] = [] + private var originMessagesIdOfReply: [MessageServerId] = [] let processingQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier).message-interactor", qos: .default) @@ -219,15 +217,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H // MARK: - BaseInteractor override func loadData() { super.loadData() - - if let room = self.room, let id = room.id, let kind = room.kind { - fetchRoomFromStorage() - mqttService.getRoom(id: id, kind: kind) - fetchData() - } else { - fetchContact() - fetchData() - } + fetchData() } // MARK: - MessageInteractorInputProtocol @@ -270,7 +260,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H // MARK: - History - private func requestHistory(_ messageID: Int64?) { + private func requestHistory(_ messageID: MessageServerId?) { processingQueue.async { [weak self] in guard let `self` = self, let request = self.makeHistoryRequest(messageID) else { return @@ -278,8 +268,23 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H self.mqttService.sendHistoryRequest(with: request) } } + + private func requestHistory(from: MessageServerId, to: MessageServerId) { + processingQueue.async { [weak self] in + guard let `self` = self, + let phoneId = self.storageService.phoneId, + let request = try? self.historyRequestFactory.makeHistoryRequestModel(rosterId: phoneId, + chat: self.chat, + from: from, + to: to) else { + return + } + + self.mqttService.sendHistoryRequest(with: request) + } + } - private func makeHistoryRequest(_ messageID: Int64?) -> HistoryRequestModel? { + private func makeHistoryRequest(_ messageID: MessageServerId?) -> HistoryRequestModel? { guard let msgId = messageID ?? chat.last_msg?.id, let rosterId = storageService.phoneId else { return nil @@ -319,7 +324,50 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } // MARK: - Fetch Data - func fetchData(messageID: Int64? = nil, isNew:Bool = false) { + + func fetchData() { + fetchChatModel() + fetchPenultimateMessage() + + if !hasGapInsideHistory() { + fetchMessages() + } else if let startId = chat.last_msg?.id, let endId = penultimateMessage?.id { + processingQueue.async { [weak self] in + guard let `self` = self else { return } + self.fetchFromStorage() + } + requestHistory(from: startId, to: endId) + } + } + + private func hasGapInsideHistory() -> Bool { + guard chat.last_msg?.statusString != "update" else { + return false + } + + guard let id = penultimateMessage?.id, + let lastId = chat.last_msg?.id, + id != lastId else { + return false + } + + guard let prevId = penultimateMessage?.prev else { + return true + } + + return prevId != lastId + } + + func fetchChatModel() { + if let room = self.room, let id = room.id, let kind = room.kind { + fetchRoomFromStorage() + mqttService.getRoom(id: id, kind: kind) + } else { + fetchContact() + } + } + + func fetchMessages(from messageID: MessageServerId? = nil, isNew: Bool = false) { if !isNew { processingQueue.async { [weak self] in guard let `self` = self else { return } @@ -328,8 +376,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } requestHistory(messageID) } - - + + // MARK: - Sender func sender(for message: Message) -> MessageSender? { @@ -740,8 +788,10 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } func deleteMessage(localId: String, forBoth: Bool) { - guard let message = messageBy(localId: localId), let messageId = message.id, let phoneId = myContact?.phoneId else { - return + guard let message = messageBy(localId: localId), + let messageId = message.id, + let phoneId = myContact?.phoneId else { + return } var messageAction: DBMessageAction @@ -771,6 +821,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let messageForDelete = messageFactory.makeMessageForDelete(message: message, seenBy: [seenBy]) mqttService.sendMessage(message: messageForDelete) + + messageForDelete.status = StringAtom(string: "deleted") try? storageService.perform(action: .save, with: messageForDelete) handleMessageDelete(message) @@ -788,7 +840,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H repliedMessage = nil } - func processForwardMessageTap(serverId: Int64) { + func processForwardMessageTap(serverId: MessageServerId) { guard let link = MessageDAO.fetchMessage(serverId: serverId)?.link, let linkedMessage = MessageDAO.fetchMessage(serverId: link), let localId = linkedMessage.msg_id else { @@ -908,7 +960,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } } - private func notifyAboutRead(withOld oldReader: Int64?, new newReader: Int64?) { + private func notifyAboutRead(withOld oldReader: MessageServerId?, new newReader: MessageServerId?) { guard let newReader = newReader, oldReader != newReader, let index = configuration.messages.index(where: { $0.id == newReader }), @@ -1007,7 +1059,7 @@ extension MessageInteractor { } } - private func readMessage(_ messageId: Int64) { + private func readMessage(_ messageId: MessageServerId) { guard let rosterId = storageService.phoneId else { return } diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift index 6741129e4..f15d4013b 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift @@ -13,7 +13,7 @@ extension MessagePresenter { // MARK: - Unread Counter func prepareMentionedMessages(in room: Room) { - guard let mentions = room.mentions?.compactMap({ Int64($0) }) else { + guard let mentions = room.mentions?.compactMap({ MessageServerId($0) }) else { return } for mention in mentions { @@ -22,7 +22,7 @@ extension MessagePresenter { unreadMentionIds = mentions } - func mentionsCount(after serverMessageId: Int64) -> Int { + func mentionsCount(after serverMessageId: MessageServerId) -> Int { let lastUnreadId = lastVisibleMessageId.map { max($0, serverMessageId) } ?? serverMessageId defer { lastVisibleMessageId = lastUnreadId @@ -72,7 +72,7 @@ extension MessagePresenter { lastUnreadMentionCount = 0 } - private func nextUnreadMentionIdentifier(after lastVisibleMessageId: Int64?) -> Int64? { + private func nextUnreadMentionIdentifier(after lastVisibleMessageId: MessageServerId?) -> MessageServerId? { if let lastId = lastVisibleMessageId { guard let nextUnreadMentionedMessageIndex = unreadMentionIds.index(where: { $0 > lastId }) else { return nil @@ -87,9 +87,9 @@ extension MessagePresenter { guard let mentions = room.mentions else { return } - var unique: Set = [] + var unique: Set = [] for case let mention in mentions { - guard let id = Int64(mention) else { + guard let id = MessageServerId(mention) else { continue } unique.insert(id) diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index e4a306091..bb5521ef7 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -28,9 +28,9 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract // -- unread mention counter - var uniqueUnreadMentionIds: Set = [] - var unreadMentionIds: [Int64] = [] - var lastVisibleMessageId: Int64? + var uniqueUnreadMentionIds: Set = [] + var unreadMentionIds: [MessageServerId] = [] + var lastVisibleMessageId: MessageServerId? var lastUnreadMentionCount = 0 { didSet { if oldValue != lastUnreadMentionCount, lastUnreadMentionCount == 0 { @@ -402,7 +402,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract view.scrollToMessage(with: localId) } - func forwardMessageTapped(serverId: Int64) { + func forwardMessageTapped(serverId: MessageServerId) { self.interactor.processForwardMessageTap(serverId: serverId) } @@ -734,7 +734,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract private func getCellModel(message: Message, repliedModel: RepliedMessageModel?, - readerID: Int64? = nil, + readerID: MessageServerId? = nil, links: [String]? = nil, mentions: [MentionInfo]? = nil, translation: TranslationInfo? = nil, @@ -838,7 +838,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract return nil } - private func deliveryStatusFrom(readerID: Int64?, messageID: Int64?) -> DeliveryStatus { + private func deliveryStatusFrom(readerID: MessageServerId?, messageID: MessageServerId?) -> DeliveryStatus { switch (readerID, messageID) { case (.none, .some): return .sent case (_, .none): return .unsent @@ -967,8 +967,8 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } } - func getPreviousMessages(id: Int64) { - self.interactor.fetchData(messageID: id, isNew: true) + func getPreviousMessages(id: MessageServerId) { + self.interactor.fetchMessages(from: id, isNew: true) } func hasRunningCall() -> Bool { diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 1aeb02a2c..7f267f39a 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -75,7 +75,7 @@ protocol MessagePresenterProtocol: BasePresenterProtocol, func saveImageToGallery(with fileUrl: URL) func saveVideoToGallery(with fileUrl: URL) - func getPreviousMessages(id: Int64) + func getPreviousMessages(id: MessageServerId) func didCancelTapped(_ id: String) func didContentLoadingRequested(_ id: String) @@ -113,7 +113,7 @@ protocol MessagePresenterProtocol: BasePresenterProtocol, func scrollToSelectedReplyMessage() func scrollToSelectedEditMessage() - func forwardMessageTapped(serverId: Int64) + func forwardMessageTapped(serverId: MessageServerId) func clearEditMessageObject() @@ -195,7 +195,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func configure() func goAway() - func fetchData(messageID: Int64?, isNew: Bool) + func fetchMessages(from messageID: MessageServerId?, isNew: Bool) func sendTyping(_ type: TypingModelType) func sendTypingStatus(_ isTyping: Bool) @@ -226,7 +226,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func unstarMessage(localId: String) func deleteMessage(localId: String, forBoth: Bool) func prepareToReply(localId: String) - func processForwardMessageTap(serverId: Int64) + func processForwardMessageTap(serverId: MessageServerId) func declineReply() func member(for phoneId: String) -> Member? @@ -268,7 +268,7 @@ protocol MessageViewProtocol: class { func showContextMenu(fromCell cell: BaseChatCell, convertingModel: ConvertionMessageModel?, targetView: UIView?) func scrollToMessage(with localId: String?) - func scrollToMessage(serverId: Int64) + func scrollToMessage(serverId: MessageServerId) func scrollToBottomIfNeeded() func scrollToBottom() diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 2151b73ad..40deb6b10 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -935,7 +935,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw presenter.openSharedContact(contact: contact) } - func getPreviousMessages(id: Int64) { + func getPreviousMessages(id: MessageServerId) { presenter.getPreviousMessages(id: id) } @@ -1198,7 +1198,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw scrollToMessage(with: prevInd) } - func scrollToMessage(serverId: Int64) { + func scrollToMessage(serverId: MessageServerId) { guard let index = messageDS.index(where: { $0.serverID == serverId }) else { return } diff --git a/Nynja/Modules/Message/WireFrame/ChatScreenAlertFactory/ChatScreenAlertFactory.swift b/Nynja/Modules/Message/WireFrame/ChatScreenAlertFactory/ChatScreenAlertFactory.swift index cb0929915..03942e8ac 100644 --- a/Nynja/Modules/Message/WireFrame/ChatScreenAlertFactory/ChatScreenAlertFactory.swift +++ b/Nynja/Modules/Message/WireFrame/ChatScreenAlertFactory/ChatScreenAlertFactory.swift @@ -102,8 +102,8 @@ private extension ChatScreenAlertFactory { func makeDeleteForMeAndOthersAndCancelMenuConfig(deleteForOthersTitle: String, delete: @escaping AlertActionWrapper, deleteForOthers: @escaping AlertActionWrapper) -> DeleteMenuConfig { - let deleteForOthersAction = makeAction(title: deleteForOthersTitle, actionBlock: delete) - let deleteAction = makeAction(title: Strings.deleteForMe.localized, actionBlock: deleteForOthers) + let deleteForOthersAction = makeAction(title: deleteForOthersTitle, actionBlock: deleteForOthers) + let deleteAction = makeAction(title: Strings.deleteForMe.localized, actionBlock: delete) let cancelAction = makeCancelAction() return DeleteMenuConfig( title: "", diff --git a/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift b/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift index 71c4f9715..676ab754f 100644 --- a/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift +++ b/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift @@ -19,6 +19,11 @@ protocol HistoryRequestModelFactoryProtocol { lastMessageId: Int64) throws -> HistoryRequestModel func makeHistoryRequestModelStickers(rosterId: String) throws -> HistoryRequestModel + + func makeHistoryRequestModel(rosterId: String, + chat: ChatModel, + from: MessageServerId, + to: MessageServerId) throws -> HistoryRequestModel } enum HistoryRequestModelError: Error { @@ -48,7 +53,7 @@ final class HistoryRequestModelFactory: HistoryRequestModelFactoryProtocol { let historyType = try findModelType(for: chat) let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: historyType, - actionType: .get(lastMessageId: lastMessageId, pageSize: pageSize)) + actionType: .getPage(from: lastMessageId, pageSize: pageSize)) return HistoryRequestModel(requestInput: input) } @@ -77,4 +82,16 @@ final class HistoryRequestModelFactory: HistoryRequestModelFactoryProtocol { actionType: .defaultStickerPack) return HistoryRequestModel(requestInput: input) } + + func makeHistoryRequestModel(rosterId: String, + chat: ChatModel, + from: MessageServerId, + to: MessageServerId) throws -> HistoryRequestModel { + + let historyType = try findModelType(for: chat) + let input = HistoryRequestModel.RequestInput(rosterId: rosterId, + historyType: historyType, + actionType: .getBetween(from: from, to: to)) + return HistoryRequestModel(requestInput: input) + } } diff --git a/Nynja/Services/HandleServices/HistoryHandler.swift b/Nynja/Services/HandleServices/HistoryHandler.swift index 98abbd10a..6c1a6106d 100644 --- a/Nynja/Services/HandleServices/HistoryHandler.swift +++ b/Nynja/Services/HandleServices/HistoryHandler.swift @@ -56,40 +56,20 @@ final class HistoryHandler: BaseHandler { } private static func updateMessageHistory(_ messages: [Message]) { - func shouldRemoveCall(_ message: Message) -> Bool { - guard let desc = message.files?.first, - desc.mime == SendMessageType.audioCall.rawValue, - desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, - let ids = desc.data?.first?.value?.splitByComma() else { - return false - } - - let phoneId = storageService.phoneId - return !ids.contains { $0 == phoneId } - } - - func shouldRemove(_ message: Message) -> Bool { - let status = StringAtom.string(message.status) - return status == "delete" || status == "edit" || shouldRemoveCall(message) - } - - var stackForRemove = [Message]() var stackForSave = [Message]() + var stackForDelete = [Message]() var systemClearMessage: Message? var isHistoryCleared = false for message in messages { - if shouldRemove(message) { - if !isHistoryCleared { - stackForRemove.append(message) - } + if message.statusString == "delete", !isHistoryCleared { + stackForDelete.append(message) } else if case .override = messageEditService.getMergeActionForMessage(message) { if message.isSystem, message.isStatusClear { systemClearMessage = message isHistoryCleared = true stackForSave.append(message) - } else if !isHistoryCleared { stackForSave.append(message) } @@ -100,8 +80,7 @@ final class HistoryHandler: BaseHandler { ChatService.clearMessages(before: systemClearMessage) } try? MessageDAO.saveMessages(stackForSave) - - stackForRemove.forEach { MessageDAO.removeMessage(message: $0) } + ChatService.removeMessages(stackForDelete) } private static func updateJobsHistory(_ jobs: [Job]) { diff --git a/Nynja/Services/HandleServices/MessageHandler.swift b/Nynja/Services/HandleServices/MessageHandler.swift index 3bcdb44c5..2b6b0be23 100644 --- a/Nynja/Services/HandleServices/MessageHandler.swift +++ b/Nynja/Services/HandleServices/MessageHandler.swift @@ -86,29 +86,25 @@ final class MessageHandler: BaseHandler { } private static func deleteMessage(_ message: Message) { - MessageDAO.removeMessage(message: message, notify: { (msg) in - if let id = msg.id, let action = MessageActionDAO.fetchMessageAction(by: id) { - try? StorageService.sharedInstance.perform(action: .delete, with: action) - } - }) + try? saveIntoDatabase(message: message) + ChatService.removeMessage(message) } private static func editMessage(_ message: Message) { - guard let link: Int64 = message.link, let newText: String = message.mainFile?.payload else { + try? saveIntoDatabase(message: message) + + guard let link: Int64 = message.link else { return } if let oldMessage = MessageDAO.fetchMessage(serverId: link) { -//TODO: need to discuss -// let mimes: [DescMime] = [.base(mime: .text, payload: newText)] -// oldMessage.files = mimes.map { Desc(mime: $0) } oldMessage.files = message.files oldMessage.markAsEdited() oldMessage.status = nil do { - try StorageService.sharedInstance.perform(action: .save, with: oldMessage) + try saveIntoDatabase(message: oldMessage) ChatService.updateLastMessage(oldMessage) { $0 == link } } catch { LogService.log(topic: .db, text: "EditMessage update error: \(error.localizedDescription)") @@ -126,7 +122,7 @@ final class MessageHandler: BaseHandler { private static func updateMessage(_ message: Message) { do { - try StorageService.sharedInstance.perform(action: .save, with: message) + try saveIntoDatabase(message: message) ChatService.updateLastMessage(message) { $0 == message.id } } catch { LogService.log(topic: .db, text: "EditMessage update error: \(error.localizedDescription)") @@ -137,10 +133,10 @@ final class MessageHandler: BaseHandler { if let desc = message.files?.first, desc.mime == SendMessageType.audioCall.rawValue, desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, - let ids = desc.data?.first?.value?.splitByComma() { - if ids.first(where: {$0 == StorageService.sharedInstance.phoneId }) == nil { - return - } + let ids = desc.data?.first?.value?.splitByComma(), + !ids.contains { $0 == StorageService.sharedInstance.phoneId } { + + return } for subscriber in subscribers { subscriber.subscriber?.willSave(message) @@ -160,7 +156,7 @@ final class MessageHandler: BaseHandler { if message.isInOwnChat, !message.isCursor { ChatService.updateReader(from: message) } - try? StorageService.sharedInstance.perform(action: .save, with: message) + try? saveIntoDatabase(message: message) if message.isForward { JobDAO.deleteJobs(for: message) @@ -180,6 +176,10 @@ final class MessageHandler: BaseHandler { } } + private static func saveIntoDatabase(message: Message) throws { + try? StorageService.sharedInstance.perform(action: .save, with: message) + } + /// Play sound for incoming messages if chat isn't muted. /// Play outcoming message only if chat screen is open. private static func playSoundIfNeeded(for message: Message) { diff --git a/Nynja/Services/Models/HistoryRequestModel.swift b/Nynja/Services/Models/HistoryRequestModel.swift index cf54557a6..5373aa866 100644 --- a/Nynja/Services/Models/HistoryRequestModel.swift +++ b/Nynja/Services/Models/HistoryRequestModel.swift @@ -23,7 +23,6 @@ final class HistoryRequestModel: BaseMQTTModel { } struct RequestInput { - let rosterId: String let historyType: HistoryType let actionType: ActionType @@ -37,7 +36,8 @@ final class HistoryRequestModel: BaseMQTTModel { enum ActionType { case getAll - case get(lastMessageId: Int64, pageSize: Int64) + case getPage(from: MessageServerId, pageSize: MessageServerId) + case getBetween(from: MessageServerId, to: MessageServerId) case delete case update(messageId: Int64) case defaultStickerPack @@ -62,7 +62,6 @@ final class HistoryRequestModel: BaseMQTTModel { } private var bert: BertTuple { - let topic = BertAtom(fromString: Keys.topic) let rosterId = Bert.getBin(requestInput.rosterId) let action = BertAtom(fromString: actionName) @@ -75,7 +74,6 @@ final class HistoryRequestModel: BaseMQTTModel { let feedIdBertBinaries = feedIdComponents.map { Bert.getBin($0) } components.append(contentsOf: feedIdBertBinaries) - return BertTuple(fromElements: components) } @@ -95,8 +93,21 @@ final class HistoryRequestModel: BaseMQTTModel { return BertNumber(fromInt64: messageId) } + var data: BertObject { + guard !self.data.isEmpty else { + return BertNil() + } + + guard let messages = self.data as? [Message?] else { + return BertNil() + } + + let list = messages.compactMap { $0?.getBert() } + + return BertList(fromElements: list) + } - return BertTuple(fromElements: [topic, rosterId, feedId, size, messageId, BertNil(), action]) + return BertTuple(fromElements: [topic, rosterId, feedId, size, messageId, data, action]) } } @@ -132,8 +143,10 @@ extension HistoryRequestModel { switch requestInput.actionType { case .getAll, .defaultStickerPack: return 0 - case .get(let lastMessageId, _): - return lastMessageId + case .getPage(let from, _): + return from + case .getBetween(let from, _): + return from case .delete: return nil case .update(let messageId): @@ -144,19 +157,29 @@ extension HistoryRequestModel { var pageSize: Int64? { switch requestInput.actionType { case .getAll, + .getBetween, .delete, .update: return nil - case .get(_, let pageSize): + case .getPage(_, let pageSize): return pageSize case .defaultStickerPack: return 1 } } + + var data: [AnyObject?] { + switch requestInput.actionType { + case .getBetween(_, to: let to): + return [makeBetweenRequestMessage(with: to)] + default: + return [] + } + } var actionName: String { switch requestInput.actionType { - case .get, .getAll, .defaultStickerPack: + case .getAll, .getPage, .getBetween, .defaultStickerPack: return Keys.get case .delete: return Keys.delete @@ -164,4 +187,23 @@ extension HistoryRequestModel { return Keys.update } } + + private func makeBetweenRequestMessage(with id: MessageServerId) -> Message? { + var feedId: AnyObject + switch requestInput.historyType { + case .p2p(let opponentId): + feedId = p2p(firstId: requestInput.rosterId, secondId: opponentId) + case .muc(let id): + feedId = muc(name: id) + default: + assertionFailure("History type should equal p2p or muc.") + return nil + } + + let message = Message() + message.id = id + message.container = StringAtom(string: "chain") + message.feed_id = feedId + return message + } } diff --git a/Shared/Library/Extensions/Models/Message/MessageExtension.swift b/Shared/Library/Extensions/Models/Message/MessageExtension.swift index 65a2e134b..ddc0e02fd 100644 --- a/Shared/Library/Extensions/Models/Message/MessageExtension.swift +++ b/Shared/Library/Extensions/Models/Message/MessageExtension.swift @@ -8,6 +8,8 @@ import UIKit +typealias MessageServerId = Int64 + extension Message: Hashable { var hashValue: Int { -- GitLab From eca76848d882c049f8ace12e28e514f358d9990c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 13 Aug 2018 12:07:53 +0300 Subject: [PATCH 26/32] [NY-2308] Fixed incorrect timestamp on schedule message in chat. (#1089) --- .../Interactor/ScheduleMessageInteractor.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift index 27ac6ddc4..3a4ded37d 100644 --- a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift +++ b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift @@ -54,12 +54,12 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP func sendScheduledMessage(with timeZone: TimeZoneLocal, date: Date) { guard let phoneId = StorageService.sharedInstance.phoneId, let info = self.info, let (features, timestamp) = prepareDateTimeInfo(with: timeZone, date: date) else { return } - let messages = info.targets.messages(from: info.message, phoneId: phoneId) + let message = info.message + message.created = timestamp as AnyObject - mqttService.scheduleMessage(phoneId: phoneId, - messages: messages, - timestamp: timestamp, - features: features) + let messages = info.targets.messages(from: message, phoneId: phoneId) + + mqttService.scheduleMessage(phoneId: phoneId, messages: messages, timestamp: timestamp, features: features) } func editScheduledMessage(with timeZone: TimeZoneLocal, date: Date) { -- GitLab From 708b13038f72f98f1ede0bd5dbda821b13cd75f3 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 13 Aug 2018 12:14:31 +0300 Subject: [PATCH 27/32] [NY-2515] Capitalized text in TimeCell (#1092) --- .../Message/View/Views/TableView/Cells/TimeCell.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift index 19cc21e05..46f9c483d 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift @@ -10,7 +10,9 @@ import Foundation final class TimeCell: UICollectionViewCell { - lazy var timeStamp: UILabel = { + private lazy var dateFormatter = DateFormatter() + + private lazy var timeStamp: UILabel = { let v = UILabel() v.font = UIFont(name: Constants.fonts.regular, size: 14) v.textColor = Constants.colors.red.getColor() @@ -23,11 +25,10 @@ final class TimeCell: UICollectionViewCell { func setup(date: Date) { self.backgroundColor = UIColor.clear - timeStamp.text = self.getText(fromDate: date).uppercased() + timeStamp.text = getText(fromDate: date) } - func getText(fromDate: Date) -> String { - let dateFormatter = DateFormatter() + private func getText(fromDate: Date) -> String { dateFormatter.dateFormat = "MMMM d".localizedDateFormat dateFormatter.locale = Locale(identifier: Language.current.rawValue) return dateFormatter.string(from: fromDate) -- GitLab From f921d0b1fc197bcf668ba1c7d86a894dab97e644 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 13 Aug 2018 12:15:52 +0300 Subject: [PATCH 28/32] [NY-2561] Divider is absent on add participants (#1093) * [NY-2561] Add separator on add participants * [NY-2561] Fixed wrong appearance of close button on AddParticipantsViewController --- .../UI/SeparatorView/SeparatorView.swift | 1 + .../View/AddParticipantsViewController.swift | 33 +- .../AddPaticipantsViewControllerLayout.swift | 17 +- .../SubscribersSelectorViewController.swift | 7 +- .../View/ParticipantsViewController.swift | 11 +- .../ic_close_clear.imageset/Contents.json | 12 - .../ic_close_wheel_clear.pdf | 3664 ----------------- 7 files changed, 45 insertions(+), 3700 deletions(-) delete mode 100644 Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/Contents.json delete mode 100644 Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/ic_close_wheel_clear.pdf diff --git a/Nynja/Library/UI/SeparatorView/SeparatorView.swift b/Nynja/Library/UI/SeparatorView/SeparatorView.swift index 22bc15912..4a620639e 100644 --- a/Nynja/Library/UI/SeparatorView/SeparatorView.swift +++ b/Nynja/Library/UI/SeparatorView/SeparatorView.swift @@ -36,6 +36,7 @@ final class SeparatorView: UIView { // MARK: - Setup private func setup() { + backgroundColor = color snp.makeConstraints { maker in maker.height.equalTo(SeparatorView.height) } diff --git a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift index 9fbe0a3cf..f13014285 100644 --- a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift +++ b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift @@ -61,7 +61,7 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { return button }() - // MARK: Views + // MARK: - Views private lazy var avatarsView: UICollectionView = { let collView = UICollectionView(frame: CGRect.zero, collectionViewLayout: createLayout()) @@ -77,8 +77,20 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { return collView }() + + private lazy var separatorView: SeparatorView = { + let separatorView = SeparatorView() + + view.addSubview(separatorView) + separatorView.snp.makeConstraints { maker in + maker.left.right.equalToSuperview() + maker.top.equalTo(avatarsView.snp.bottom).offset(Constraints.separator.topInset.adjustedByWidth) + } + + return separatorView + }() - lazy var tableView: UITableView = { + private lazy var tableView: UITableView = { let tblView = UITableView.default tblView.rowHeight = SelectorCell.Constraints.height @@ -86,11 +98,16 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { tblView.sectionHeaderHeight = ParticipantsHeaderView.height tblView.estimatedSectionHeaderHeight = tblView.sectionHeaderHeight + tblView.contentInset = UIEdgeInsets(top: CGFloat(Constraints.tableView.topInset.adjustedByWidth), + left: 0, + bottom: 0, + right: 0) + self.view.addSubview(tblView) - tblView.snp.makeConstraints({ (make) in - make.top.equalTo(avatarsView.snp.bottom).offset(Constraints.tableView.topInset.adjustedByHeight) + tblView.snp.makeConstraints { (make) in + make.top.equalTo(separatorView.snp.bottom) make.left.right.equalTo(avatarsView) - }) + } return tblView }() @@ -154,17 +171,17 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { title = "delete_participants".localized.uppercased() } else if presenter.participantsMode == .createGroupCall { title = "add_members_to_call".localized.uppercased() - backBtnImage = UIImage(named:"ic_close_clear") + backBtnImage = .closeImage navHandler = presenter self.selectAllButtonVisible = true } else if presenter.participantsMode == .updateGroupCall { title = "add_members_to_call".localized.uppercased() - backBtnImage = UIImage(named:"ic_close_clear") + backBtnImage = .closeImage navHandler = presenter self.selectAllButtonVisible = true } else if presenter.participantsMode == .createConferenceCall { title = "add_members_to_call".localized.uppercased() - backBtnImage = UIImage(named:"ic_close_clear") + backBtnImage = .closeImage navHandler = presenter self.selectAllButtonVisible = true } else if presenter.participantsMode == .admins { diff --git a/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift b/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift index d53d76df2..d452402ad 100644 --- a/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift +++ b/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift @@ -8,24 +8,28 @@ extension AddParticipantsViewController { - struct Constraints { + enum Constraints { - struct avatarsView { + enum avatarsView { static let height: CGFloat = 44.0 static let topInset = 8.0 static let horizontalInset: CGFloat = 16.0 } - struct tableView { + enum tableView { static let topInset = 16.0 } - struct controlsContainerView { + enum separator { + static let topInset = 8.0 + } + + enum controlsContainerView { static let bottomInset: CGFloat = 28.0 } - struct doneButton { + enum doneButton { static let width: CGFloat = 82.0 static let height: CGFloat = 44.0 @@ -33,11 +37,10 @@ extension AddParticipantsViewController { static let contentRightInset: CGFloat = 16.0 } - struct SelectAllButton { + enum SelectAllButton { static let side = 44.0.adjustedByWidth static let leftInset = -2.0.adjustedByWidth static let rightInset = 6.0.adjustedByWidth } - } } diff --git a/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift b/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift index 3922e1007..4d5c17aa3 100644 --- a/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift +++ b/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift @@ -67,14 +67,10 @@ final class SubscribersSelectorViewController: BaseVC, SubscribersSelectorViewPr }() private lazy var separator: UIView = { - let separator = UIView() - - separator.backgroundColor = Constants.colors.separatorGrayColor.getColor() + let separator = SeparatorView() self.view.addSubview(separator) separator.snp.makeConstraints { make in - make.height.equalTo(Constraints.Separator.height) - make.top.equalTo(avatarsView.snp.bottom).offset(Constraints.Separator.topInset) make.left.right.equalToSuperview() } @@ -391,7 +387,6 @@ extension SubscribersSelectorViewController { } enum Separator { - static let height = 1.0 static let topInset = 8.0.adjustedByWidth } diff --git a/Nynja/Modules/Participants/View/ParticipantsViewController.swift b/Nynja/Modules/Participants/View/ParticipantsViewController.swift index 78d398bda..40ef3ce68 100644 --- a/Nynja/Modules/Participants/View/ParticipantsViewController.swift +++ b/Nynja/Modules/Participants/View/ParticipantsViewController.swift @@ -39,11 +39,16 @@ class ParticipantsViewController: BaseVC, ParticipantsViewProtocol { tblView.sectionHeaderHeight = ParticipantsHeaderView.height tblView.estimatedSectionHeaderHeight = tblView.sectionHeaderHeight + tblView.contentInset = UIEdgeInsets(top: CGFloat(Constraints.tableView.topInset.adjustedByWidth), + left: 0, + bottom: 0, + right: 0) + self.view.addSubview(tblView) - tblView.snp.makeConstraints({ (make) in - make.top.equalTo(navigationView.snp.bottom).offset(Constraints.tableView.topInset.adjustedByWidth) + tblView.snp.makeConstraints { make in + make.top.equalTo(navigationView.snp.bottom) make.left.right.equalTo(navigationView) - }) + } return tblView }() diff --git a/Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/Contents.json deleted file mode 100644 index cfa020576..000000000 --- a/Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_close_wheel_clear.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/ic_close_wheel_clear.pdf b/Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/ic_close_wheel_clear.pdf deleted file mode 100644 index 583dc73dc..000000000 --- a/Nynja/Resources/Assets.xcassets/ic_close_clear.imageset/ic_close_wheel_clear.pdf +++ /dev/null @@ -1,3664 +0,0 @@ -%PDF-1.5 % -1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - ic_close_wheel - - - 2018-05-23T12:33:26+03:00 - 2018-05-23T12:33:26+03:00 - 2018-05-23T12:33:26+03:00 - Adobe Illustrator CC 2014 (Windows) - - - - 256 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q== - - - - uuid:32433c3f-d46d-4542-b0db-d7a2e136ea7c - xmp.did:94a44799-036f-e744-828f-48fad356141f - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - uuid:d1c078a0-2746-42b2-b0d1-25aedff8fb1e - xmp.did:1b6690ed-28a8-c141-9479-b6a9cf6be651 - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - - - - saved - xmp.iid:94a44799-036f-e744-828f-48fad356141f - 2018-05-23T12:33:23+03:00 - Adobe Illustrator CC 2014 (Windows) - / - - - - Print - False - False - 1 - - 34.000000 - 34.000000 - Pixels - - - - - Default Swatch Group - 0 - - - - White - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 0.000000 - - - Black - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 100.000000 - - - CMYK Red - CMYK - PROCESS - 0.000000 - 100.000000 - 100.000000 - 0.000000 - - - CMYK Yellow - CMYK - PROCESS - 0.000000 - 0.000000 - 100.000000 - 0.000000 - - - CMYK Green - CMYK - PROCESS - 100.000000 - 0.000000 - 100.000000 - 0.000000 - - - CMYK Cyan - CMYK - PROCESS - 100.000000 - 0.000000 - 0.000000 - 0.000000 - - - CMYK Blue - CMYK - PROCESS - 100.000000 - 100.000000 - 0.000000 - 0.000000 - - - CMYK Magenta - CMYK - PROCESS - 0.000000 - 100.000000 - 0.000000 - 0.000000 - - - C=15 M=100 Y=90 K=10 - CMYK - PROCESS - 15.000000 - 100.000000 - 90.000000 - 10.000000 - - - C=0 M=90 Y=85 K=0 - CMYK - PROCESS - 0.000000 - 90.000000 - 85.000000 - 0.000000 - - - C=0 M=80 Y=95 K=0 - CMYK - PROCESS - 0.000000 - 80.000000 - 95.000000 - 0.000000 - - - C=0 M=50 Y=100 K=0 - CMYK - PROCESS - 0.000000 - 50.000000 - 100.000000 - 0.000000 - - - C=0 M=35 Y=85 K=0 - CMYK - PROCESS - 0.000000 - 35.000000 - 85.000000 - 0.000000 - - - C=5 M=0 Y=90 K=0 - CMYK - PROCESS - 5.000000 - 0.000000 - 90.000000 - 0.000000 - - - C=20 M=0 Y=100 K=0 - CMYK - PROCESS - 20.000000 - 0.000000 - 100.000000 - 0.000000 - - - C=50 M=0 Y=100 K=0 - CMYK - PROCESS - 50.000000 - 0.000000 - 100.000000 - 0.000000 - - - C=75 M=0 Y=100 K=0 - CMYK - PROCESS - 75.000000 - 0.000000 - 100.000000 - 0.000000 - - - C=85 M=10 Y=100 K=10 - CMYK - PROCESS - 85.000000 - 10.000000 - 100.000000 - 10.000000 - - - C=90 M=30 Y=95 K=30 - CMYK - PROCESS - 90.000000 - 30.000000 - 95.000000 - 30.000000 - - - C=75 M=0 Y=75 K=0 - CMYK - PROCESS - 75.000000 - 0.000000 - 75.000000 - 0.000000 - - - C=80 M=10 Y=45 K=0 - CMYK - PROCESS - 80.000000 - 10.000000 - 45.000000 - 0.000000 - - - C=70 M=15 Y=0 K=0 - CMYK - PROCESS - 70.000000 - 15.000000 - 0.000000 - 0.000000 - - - C=85 M=50 Y=0 K=0 - CMYK - PROCESS - 85.000000 - 50.000000 - 0.000000 - 0.000000 - - - C=100 M=95 Y=5 K=0 - CMYK - PROCESS - 100.000000 - 95.000000 - 5.000000 - 0.000000 - - - C=100 M=100 Y=25 K=25 - CMYK - PROCESS - 100.000000 - 100.000000 - 25.000000 - 25.000000 - - - C=75 M=100 Y=0 K=0 - CMYK - PROCESS - 75.000000 - 100.000000 - 0.000000 - 0.000000 - - - C=50 M=100 Y=0 K=0 - CMYK - PROCESS - 50.000000 - 100.000000 - 0.000000 - 0.000000 - - - C=35 M=100 Y=35 K=10 - CMYK - PROCESS - 35.000000 - 100.000000 - 35.000000 - 10.000000 - - - C=10 M=100 Y=50 K=0 - CMYK - PROCESS - 10.000000 - 100.000000 - 50.000000 - 0.000000 - - - C=0 M=95 Y=20 K=0 - CMYK - PROCESS - 0.000000 - 95.000000 - 20.000000 - 0.000000 - - - C=25 M=25 Y=40 K=0 - CMYK - PROCESS - 25.000000 - 25.000000 - 40.000000 - 0.000000 - - - C=40 M=45 Y=50 K=5 - CMYK - PROCESS - 40.000000 - 45.000000 - 50.000000 - 5.000000 - - - C=50 M=50 Y=60 K=25 - CMYK - PROCESS - 50.000000 - 50.000000 - 60.000000 - 25.000000 - - - C=55 M=60 Y=65 K=40 - CMYK - PROCESS - 55.000000 - 60.000000 - 65.000000 - 40.000000 - - - C=25 M=40 Y=65 K=0 - CMYK - PROCESS - 25.000000 - 40.000000 - 65.000000 - 0.000000 - - - C=30 M=50 Y=75 K=10 - CMYK - PROCESS - 30.000000 - 50.000000 - 75.000000 - 10.000000 - - - C=35 M=60 Y=80 K=25 - CMYK - PROCESS - 35.000000 - 60.000000 - 80.000000 - 25.000000 - - - C=40 M=65 Y=90 K=35 - CMYK - PROCESS - 40.000000 - 65.000000 - 90.000000 - 35.000000 - - - C=40 M=70 Y=100 K=50 - CMYK - PROCESS - 40.000000 - 70.000000 - 100.000000 - 50.000000 - - - C=50 M=70 Y=80 K=70 - CMYK - PROCESS - 50.000000 - 70.000000 - 80.000000 - 70.000000 - - - - - - Grays - 1 - - - - C=0 M=0 Y=0 K=100 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 100.000000 - - - C=0 M=0 Y=0 K=90 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 89.999400 - - - C=0 M=0 Y=0 K=80 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 79.998800 - - - C=0 M=0 Y=0 K=70 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 69.999700 - - - C=0 M=0 Y=0 K=60 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 59.999100 - - - C=0 M=0 Y=0 K=50 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 50.000000 - - - C=0 M=0 Y=0 K=40 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 39.999400 - - - C=0 M=0 Y=0 K=30 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 29.998800 - - - C=0 M=0 Y=0 K=20 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 19.999700 - - - C=0 M=0 Y=0 K=10 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 9.999100 - - - C=0 M=0 Y=0 K=5 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 4.998800 - - - - - - Brights - 1 - - - - C=0 M=100 Y=100 K=0 - CMYK - PROCESS - 0.000000 - 100.000000 - 100.000000 - 0.000000 - - - C=0 M=75 Y=100 K=0 - CMYK - PROCESS - 0.000000 - 75.000000 - 100.000000 - 0.000000 - - - C=0 M=10 Y=95 K=0 - CMYK - PROCESS - 0.000000 - 10.000000 - 95.000000 - 0.000000 - - - C=85 M=10 Y=100 K=0 - CMYK - PROCESS - 85.000000 - 10.000000 - 100.000000 - 0.000000 - - - C=100 M=90 Y=0 K=0 - CMYK - PROCESS - 100.000000 - 90.000000 - 0.000000 - 0.000000 - - - C=60 M=90 Y=0 K=0 - CMYK - PROCESS - 60.000000 - 90.000000 - 0.003100 - 0.003100 - - - - - - - Adobe PDF library 11.00 - - - - - - - - - - - - - - - - - - - - - - - - - -endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/Properties<>>>/Thumb 11 0 R/TrimBox[0.0 0.0 34.0 34.0]/Type/Page>> endobj 8 0 obj <>stream -HlMj0 :N؉mӡ.zvQwa -c:D>Kz<ɞWΫÇf#E >X@%[@&qD1 ,p DJqni@NOg!buTzJ UMNcSh;UznPe>tnSY__F/EunH=YȻ - mT6pdLIPox}<=,Jp:קW\? -endstream endobj 11 0 obj <>stream -8;Xp,*>JPW(]\SI%<2~> -endstream endobj 12 0 obj [/Indexed/DeviceRGB 255 13 0 R] endobj 13 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> -endstream endobj 5 0 obj <> endobj 14 0 obj [/View/Design] endobj 15 0 obj <>>> endobj 10 0 obj <> endobj 9 0 obj <> endobj 16 0 obj <> endobj 17 0 obj <>stream -%!PS-Adobe-3.0 -%%Creator: Adobe Illustrator(R) 17.0 -%%AI8_CreatorVersion: 18.0.0 -%%For: (Desi Karavelikova) () -%%Title: (Untitled-1) -%%CreationDate: 5/23/2018 12:33 PM -%%Canvassize: 16383 -%%BoundingBox: 6 -28 28 -6 -%%HiResBoundingBox: 6.91000258922577 -27.0899973277592 27.0899974107742 -6.91000274462749 -%%DocumentProcessColors: -%AI5_FileFormat 13.0 -%AI12_BuildNumber: 18 -%AI3_ColorUsage: Color -%AI7_ImageSettings: 0 -%%CMYKProcessColor: 1 1 1 1 ([Registration]) -%AI3_Cropmarks: 0 -34 34 0 -%AI3_TemplateBox: 17.5 -17.5 17.5 -17.5 -%AI3_TileBox: -265 -395 305 355 -%AI3_DocumentPreview: None -%AI5_ArtSize: 14400 14400 -%AI5_RulerUnits: 6 -%AI9_ColorModel: 2 -%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 -%AI5_TargetResolution: 800 -%AI5_NumLayers: 1 -%AI9_OpenToView: -15 1 25.56 1626 923 18 0 0 46 112 0 0 0 1 1 0 1 1 0 1 -%AI5_OpenViewLayers: 7 -%%PageOrigin:-289 -413 -%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 -%AI9_Flatten: 1 -%AI12_CMSettings: 00.MS -%%EndComments - -endstream endobj 18 0 obj <>stream -%%BoundingBox: 6 -28 28 -6 -%%HiResBoundingBox: 6.91000258922577 -27.0899973277592 27.0899974107742 -6.91000274462749 -%AI7_Thumbnail: 128 128 8 -%%BeginData: 2039 Hex Bytes -%0000330000660000990000CC0033000033330033660033990033CC0033FF -%0066000066330066660066990066CC0066FF009900009933009966009999 -%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 -%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 -%3333663333993333CC3333FF3366003366333366663366993366CC3366FF -%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 -%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 -%6600666600996600CC6600FF6633006633336633666633996633CC6633FF -%6666006666336666666666996666CC6666FF669900669933669966669999 -%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 -%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF -%9933009933339933669933999933CC9933FF996600996633996666996699 -%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 -%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF -%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 -%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 -%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF -%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC -%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 -%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 -%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 -%000011111111220000002200000022222222440000004400000044444444 -%550000005500000055555555770000007700000077777777880000008800 -%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB -%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF -%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF -%524C45FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF -%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF -%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF -%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF -%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF -%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF -%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFFFFFFFF -%%EndData - -endstream endobj 19 0 obj <>stream -HOfGIwuuu&+EЕAeI`܁l$޹\gspAFno{kĻ:;L,gqekpplf9ʃuzdoՇsо/ǙT؅/#ל6gVTW[q`xS9}>#Ie}ZBF -A1&fLmjf&m|L)EK,FXћ keU! Fͺݦp ȓsH3lbm4Bܗ[J*gHДuȫtom5!*m9"s?G<-+d6>{о'ͼ=/b]1$eĚ}ósXM>sװ?kk}+G_ۗj=]ŋiT8h2=b6&>jNcdޛi533An gk]{P^#]=Fؔ0BCѨ; 1p#^#|eiӯW0HmQ;0Hc~9CAtEcN[p6lR %O WCƃ.ohɝ!=<1L\̏E@4d~7tkR'Ԑz4/fA4"38аc s' @ДɷaDY|~jaa*Qf? DC PB&88& b -Ѷ02)*BVTE;Qՠ:l3 -yQd)')׌ K{V@á% <,~c:N5};hTCo>t<1&No -z]@.ZpKcc`i6"QNhhZu"xQ1Νc>Rb1e#)эeک{iPabfq.ia%װjxDSJu3@' 3=H`gC-h' m%ʸM -ahV!)˂}EU/Ӌ% 09{+n3=/te~7!QOI* /z dFR#K)YQ8¥t kڣ]R1GLt N]l['d< ڲ,g˪Mi!C7X!C bBlt6igBu`֔zHȎoJѨkV #^4)hD#p7Ge=$J-tK'#+D [A +|y0 K;|ަzGSM0 C>[΃ .!X+}c* 6f#f̀ਃ6`STBߎ3HB /Q ux!^AavC$\^S gL\3| }Zu-yOZT-Yi zYS' UKLe=ɞ$4&eDP6e#%i]ުվ/L`\QP4Ԧ$Dqk98FEaȹjXxb iY`HFC@V[pcٵGS p;x3hMW &]'lȵA;~bL!.M+tƱTr9|f)P*!фȑv}y)u*-/dh&ծJznqqRWWU" A"> UHZZ|t\Y\+Smn4dwݡJTaܥuE\*~K@Gy$SHl>ES-ؒUR:H>Iï|?=g_b/__yo;AGGMmn!6g<) 0o툈.'La q8JԨD3)Օ x.\W6IFJ-%@t. ~צ`V.DL{1?Y:kq2\}Ԓ6{(Qpsc4R;rK7@mQ"`R*)\0(R~{!.fo(=U]uCkFϸ@̸=5K1|YΜui:R A*+(nN&'sn3j XÒ !WPf:H.:F8]<8,qd"jJynTC+f󹙖M,HF7 c C&)u-FO7MU],9~H]҈|N yca 3! )](`(JX^n96ʜ` <;"ճ?c,bAA#'B),pMB S-G~6K2eV#ayL9 HDB= Le3N.[VbGi9~~xqCoH7@(\"x[ $P)[8LZn92O}qjP܆[l[L Cc=P@Ê7+7$B;oǸCQu<an,_*3do -jDX &6oaɔNΚB:NHU .OP.N7a=`< njAq]n2x2b9$[PX~$Q I w[mbcj|;o'yT# o蕺D#|x>~SZpߓo0NT*m - /_'73U"4~#&[@6BF; "DWN]9Vȵ"9脞{<ODxAG9 Z}rA@CxسslἬeYevr;qc iK*geGb( Tt 7l:t5|RX?ޝl^ͥ2wBt'i[Aׇȭe2xHUEȜaU0lv/Э>l}F%=jWVI%_Ӡ #~ܮn޸#Rl.)Sxh*s'G 2uq!y5%l×$mZ¸;=H:[eHZW۱RPkp3 z,ŲzgXD FŽxFNв5(bR@U|d((tVFxvrf0,+ q60~?sK7pAݼ!pau_3B> ~cog^"`W>4xK Nk,fn Pvo8h}H5Bra&);W8uXahL7DLtMSܓeD[oЉ"ӟ=FDP9i uQ4auY,r֍oWWZ;AtP3ު|ly8\7ƙZ،̍BdE.PqN:B`N -D <#%p+#ϮVe12A[Oidw]&XF5#QZ\X3rts4>D.1^*4l1G# s +HفiUpx=˧yh -ɼ)ٖ!I7LM[ۺ~rgr՜&1nHqLcYvq<O3D2uɴ{T7l]\%JXwޣvN {Syu9jTi6[Cb:&lD)1m ]8ەc~&FYJ۷{4y"0 */oru =^~"#*ݚ2`|2}I8 دS5KAw +Y (MUL -يȇN/)&##Wv"?/x{|%=*ߠ5-$acBLi81_+cFUÂ${`;TΈv]B@m5b$avh(n=-b"C=THv`c\(q*&yl8,yJǮgQ!!nK[Ŗ(\8>98IDɫ&_0+\~) -4DUΞsrVs+ʠqi\0xwsS/[c]5a03ue%)Fж-!1"F][D:egRH_AJjs:jZ$VTB1 - G2;1d愐lrbǧ+hSlR '1O vD6D マ?xP?@/Mg"\Qt'gFGު#E6N%'HK !r!3s\ܴn -u@}=*e ',"Ыj%*5\RDAE΃ ~Gz,†IbUPG 6DH2?L葎#RkD]E0ʦ(DkaW j b݅dfO&$7Z?4/pp^u@nwg3RCM -AqN9B6)׻Lm1cC A0ʩ}AA.]HN, 'P|BA/ YWo@^&=rY+Ԕc+,{B%e2 }s#^X"(ەq3;| $zk:z\/A0gq"xUZD1SDluB0=:ɬDfy]oox=hȰ%4rQit|7]ÀVnmN'Bʹ;JonY˞lSIana94Phmq|H.ݙ>lOAdcJ#lf̬F JD\lO.8B{iX jcui^f:Jia㺿]jO#BIոQh FPI$@,mA-͍CFTF3̦7_*98Ye,vNYl:ʻ",=P˵.UgK6b^a:dF h-փ+bSJ̀ -c~. kPUȥ3(Ӎ!w|[m}h=6j7jX+"C ۮ#X2Gv:$)F\(M u tBޯFNt2*"FtՊ\&gzf]͵ZzJEWS#}RL3U"X OKlIEU %DWHʋh|I\H6;YN|ӓ:AbNV\b[MD`:0cDliŬ*0C -tښCҒdʼnRx\YBlx)u_>hE4:{eBӂQ<EMؾg^nCa%ERWŃBiq,' 8iapQ{Uk%,u -L't/dzM+ cɔ|} Y/"A?D q{Re%(}~,pqr{R,>52b,DЬ쯪s;T~gARaqr`G~N@6AQGAc\a'Hyl[$wt& ő88neèI$i)i\Xb9N[J.g ?٣.lYC0%q@ `N ?LbԨ:JJf(X(Bcm;sL3F?&m@I'K5_뤁TcCHBAFyO95?E%X:\e3)Z.tv@]")H_O`vDZy -?,aML2Tqk*#dw"UJDj=vDt(pw`R߳;ˆH9щAb!}# -"T03Aou+$dBF\>f#%"L :*5GMu (`As6<*H-aIޙYnt9sm|CNڢ -_B1-*G'b0i 6r .3\"0B%uZp(seͰ,"7m УF+m=S7I -Bޕcz jAlVEީ0e[cK2Ǥ3c-6E?{igkVⴲ2+ǝsp8|Hd &uO9kBRH(A5gl~3qb -A"@}Թy'M#B}1)k4>gߴz'@Eh}b'UA$865;Gy/sps05dK7S"5A MH[ -,[{)# C<'P= &$MA?4g 7Kɴ$lOPqœ612)#8EP,[%?; \E3fS@!Iv4I\p9iRH겴3dЮoTrݫ4X <HmcPh9@ -? ')I82Z"Me  UT> m3iCJvZ9ΎfTJUwFgd8V|281<1(*|"SP0>l eNV(sRwe܋`0\ -\TwIa*CVYuoӊrx -f -_OCSΈp}ҋ k2ԧe2fBvj$TdC"hy場,w\lKOW {( H ă{~l;ܟCe7V"ϓs -a('`ƒo1=fCA4퇏_o?/eo}|Wo߽o~?aß/;wK?|~X,FtB~6?B9*XMBҽJX?Sraay=>2l)Cz'HY$7R'Hw[w=JL1NLphtV e;C{H/ Քlo7ωr -<(2E~\Mα9"ppZNH&\Dc4' 6"&SHBreq}rg0 '咻C 9^ R9,[o* -QX/z'#&2ymDFnJ6.3(~ɜH ZD?#2@kF\s)\LT:ŅW-jgGޡSYVԔe8a Uf| ?-TN$=Aq\Uv<PM,`['5QB [d{Gr>xHAݡ|\ -, -֒}߈t%-xS5 -Sm߀%F6j8&+t&]@p2L-5Gnh3Ώki6fKofpe[Aa*ReZϴuarnTX4>ד@ݨ 'MDw:C]zwG_1Y|Pg N[Ouph7b=KUSщzqn!z}d X:y9H3ѓQULӋFltC1Q6Aoktt{,2)s`Kk#:0rDt 1Wv0wG9ZJL<1y&ߗ{Nĉjg "_:2Os3x_#23И@]i -'q]MI7":hDG^j]-+>Z˦ -?Cea~ l/WLqE4ㆡ2јɘ9?,+ltp2?ٵݞLZ@5`ʺ{P/$́*d;|:jRd.5<pY译0ہ+P_'as$vpOIn^N`FcQ+𹷆rC+f6)iq"eo[ϝ`c߆LUV=+ByD0iaQrDMaO, :pozC9U 0A3̍gp\n)Ẉ1f"Llf>+qP0ucV~*Ӗ$/^ApGLnݧQk<ȕFEhb`dLgt&NfRݱmΪ#SP/=^@Hd'`EL=O6|nC0 `H8APY!׊WY!Kaוeiu/D@"+kTs0Yv~9󨩞[}'yU3M__}a-S\ƯXiIS("u k!׹:[,*~`0*KaF8XԹ"(j=>Xq -s:_ϧdZ//Ծ ,S++K\Ƿ5v&t>1סI &b8+-4i?+"B5˭t_X㘧6"HE-Y Z?KqȬ70QDr)ѧyUr%8pk|zFydP\ird^:Z2X6ij1|"h?IЊOcFUqit4,{Ŕt[(o .U~{0ɹf0/vB]PaaFfPTiJn}W[h=x:)5t4LJ*`$$-[.0F-0GBg1QUA@56Tb "Gcu^*0^4Avbc^E Xa.ґ'U|<o yZx>A&X3πb-}PdT$ -%kɸo}@p{z8NrU-ƇI&2?7_m e8EsTƑ = A-ДCubK(&zD+!R x H5+X\("[c8 ՙ]24˅б70 fL1麃79s= qنd [;as/Ew2[qT=&erdg0 ]U%ot:U eig*I>Յ(4h~Z= ]gr#&)BAG "ܴ-F c'ypbhbȃ($l:10}-' (\SjV?Hf) -6[FSmpG|2gtm_}b+ye].MYr}XV?JmĮFl¨dpCS|6fpXD+k@ !m -ѝضXӹr?W.t7uyˡb ]K\3w2H p|}FSbF3dk;y=FDphVh [:)QD;uFx斁4֞ypZFE4 ^ZF`K|˄ :2#X>"k>"}BydN8d"ѠW70a֙[aȅ؞ˆЕ 5b}1FL <F\-zR)˙a)c:Bu{{T[\}sM;QT?PTaP-96^_awGz[wܾÝ\)T%taulS߀ >K `/D AcPrv; J y]8>\cNE: -߅ K2,Bi_Ycbfp؜ub+4bVcaD:H67A LU%!3b rG'3y@} 5'qYysq=;|ս^VVq.ߛq Bd9SZ/t]KQ+F%mƱ= 72v;p@Dn=;G>*^ -m/f)n9 Xp d(BoZ,7x";$=EL>^BF d K+xzimvԯ1I} (1GM݂5AGpBb)n1'B 5E-7Vw}߸*}09L&GM}AU 7%,vEzsHZ *Ԑ%[rYm!LgKKL9n}Eɏ#l6]v(WƮ9łV@`R.MjBD)rGICC[uh#n0Ucc!"Zq\z%0P瀰<Cv¦;7 @D --I٥g<:@QEkAG܂a`@hƈA)IlZMmof -v uF`6tp<v pK.#Or$Յ-À5ݲL S[E,57/({nn9|c(4m-v?];۾hr^ -EmU(#l%rDqKsm(HŽpU#mrnP \¸66;DG!zxh[%faf @#_m)mdKa=!("߹7`FRaK䂥ރޓO0,1 -tۦY/Eɞ@np*'HЭΠ6ke=XvcN8*4| U0"J2/QH8zo]̏T -@0*@ψ?T+)1_=i0 H<"DQAx=zOqĭNOŲj]EܚbN Ͷ;#Eu.x { ]]HX4y5s4y{ Ge*/J0R w4?, -R@ob80<07 @=Z~v@UYr2 ~2Q5o.ޥ3ߣf<1E 8LP7Sto!#C/%q2gz80Wd>N}vYJȖrB#"Dyg%+_Ec{Ef/t˅AӖTh)-} bEXgB˚9gm`5m=qy&ĥy"5*#uYNT' #MiŕJf#'7f>W|63@=2SX/bZ߷fa[o -i߫XWֳHN(92(,{szfO1%+=xnaq{8x'qݣa"Eϲ" -O*~ XE[3naqgmAf#<6\ZAjݮd z!uoп)gt!thd r LBY# S) (hm4î&QU04Шj0QUJDp΋F[ Y>[3ai~Bn}v4^<4P*4CkI)rʵC('5ֶmY] <RZR1'Ul2 -ޅdhNpX k$H4!G@2Rl'[Al*< -ފjtoBc -*\y\Ìy; :h3z. nl|3*S$3p'[!6䝡6Z1k,бTssKDpRy/ -{[ahmhh@VCe^lCh;/C0%jϳnx͇W,:j<5?]|݇,O^x?ܾ2;S~r]chyƟohZ}u{Ӈ}ۏo_ݾ}sǒ'ջ|r}޼㧛rȺ<H;!9N  4GჍDpyyDyG- /#ED@U(rFg76uq[#&r ŤZjAvƆ߈]fs\ȇsm9V@$?vm\cuDkE|H - C Hw-bkT.Kpx #V/ZIzl` Bw[,DD;D1 {ʎ7 Fe!7?ޝn;PH`߽{;#sʈna䰨PƠճ9 Q7șxz=+f?-^Oz)x>Iˣ`o$|a`4}]YD>Dxq0tz`]QY&| -)iG,)H9bH|o{?Ur@bO/;'] 7 .5M]" i-QPf*!&J3#`"ZM/evHpDAW MLbw`L2s%NĆ ^x@@"ڳ5ZX@򲣍}rbC[)q)l'[f $dNL< Q~hҀ9 8cPjH䊢a"G|aD- -Tw)"Em.6Q²,t= J%.m -jR\ r -rM^*4DLE1RefV9nN"0:K -ܳYoTA]jp#!b`'u7^u xoz^h!\6ۛjfb G[>)\NHڗ^ U|kܔ)ѹ#`+8F> @9?asbw\bz x*OW tFwvnNю y4(}SϕOg -%씭iLyEhE(|QcH4ixx>l8$aIU0$0mFp mb*hI. j} L ZdnBPm_Lt!ܥFe1$'pa̪8K ќ -n(ýIO9̦y&h<gWŢz|A0>5 m)薣p(%AfrD^H00?| ' 6r ^˴3&=q&SQVkx!(ؖ'œ_25Ï ڼTBQl haKU7̣rnڣ\7=nM$^IݥDz! `V0|.w&AP3  -x!@soq}V(l<"T9GBtSPיb+Wl^^ -KՖңTwLsƩ!?IP~0I"3 {Vo],E2_FT*^Q"*щ=ܠz2X{煔7Ĥ F\IU`<1 8:IxBw%{(,P%j08Wd+{B(t.2LҵΊ͔:U!YQG"m FR?Whb0CuS3`Y lv~ږ96 R>Kq.b5g)iau굣kI( -L}i2_n S"n 7K^Z;BB@!+"m ͳXU"Aۜ [Da VKgL!d5`L8#Mk/2{ɟ<& J -1_2 @b`{yZ)֪~^!4#!ҨH3zȃ@ZR -=Z$|KZwe}~UR_B- N&iHԤRig[42(5<)GXjҭxR -~= -KnOPp0k<&*YX M0ꦯӍ fir3Yx/y -Z - *+X.C`+D}xƘY" Ƹq'?K1VZ"vDtIQU~j 0ʴJ -mgdXPp_KaT5\ǵq/+Sa_͉6RiYs`ax ㅞA_JZK5$idЮbȷ}c:m&hkyÌm dj!\7;PJ}l-1x -HC׾e*ac6HP 4YLb f"d^L[izٷz[`sW=P֣!fTVZ'X41+w~]ce:?ƁŅ6t9 -%$~m!O$1UUg=|F7ե+t5`. -dzO֍o`T~0Z4rnp]ev #@V$4Y~dJ3<ې)#^ǘ+FPifTL4m -$xP8"FDP g:b CoCiuK Gj|&A$:稹Feû-sT}=`)+xxDɁ0$n@T^;كIB"u"w(X!09a-_(z tDP.X FųaS.lٌr`%!n6|"pFn -JfXd/DjSJ衉v$P{[Guz0\itROTc*58r1EU ш1*3vle[IȐe3OcXHk"HG7"bqx,NB>6^7"R8}7I䋞DJp(HJB6F^?/|lX.>@Phr xlT -P;S]B/oc?n$ZpFh֮<0i}Nߥp#C+=RLJלReP8 +Op5. -x[ EXFlΫ\36JSDiD|N$G -EmKkvw\:DTkdqCtOe XBS?ae42R D iBymze8)}i8h7zo~-cHD'w:~SJ#` "@`#+R)QBWNIm#4VL9lD^F M}}~_`㫿}~|7_{w#eKPD)e!`+4҅!,l$j=8V%ixدZq!RJguqDوK## [_5ND1I97_?:IV#ʻu^y2~ζX}"RE!x_ mD)F>)g+5>C ;:ayUQcM,Z,/K3/sj65VSˆp R=g,ЗQ.<|{MJO>HK`zHrP*u$j*z ШV|K|^0!XY *JbT,U\,uCNJ-<˱C}ۖQɴjM6i^Cp h k>R@yar$`.Pf շwEsz>{*d!"fЀͬ2";Jץkͻzo/V9/.y"kA$ FXBY(Sr19,DKNB|hpEukLno ER*-4ˆVU>T=ȼ48+A-WYWiA4EwLOU `39#VזxBx a0 qw۹Pݭ8]q5 -gډ@o6)ӰWֻSc.x8GrD.'hqMe2.\rhUM?VoG^qvǷM8ʤNݠ)S)5]MZAx -࣫ېT -~ne֔BgS9T.2#]UI<$jXD6x2x~Ъ,ՠgaOtOE&>0ٰ -(xW9 UeK7[ MP>Pʕ xsC5γ%#:lI:sbJ@diǯV_|5gW:Fa#z%x5`&: -ݘߖ(d1tmW:OGY)⟦W3nCS|aG?Jק24$`BB]vJIvNR$< L$l-$7z@mϨ LH>o d.W%uB|* -b6OjYAaTՄݻP-p@O!`p.+&JJ=%sKajR+~SIæ|@$yS =h3ц*)|K:Ga#uJ&R{pڴ -`#IJL -܀H/q*cU8Pc*] 2r]8Z*yбxCP]H< $S9 ū9-P8מ#qQ5P?B;bO͸LqScޭ20ߎ>ȅ鈞 x8o}u;7ekGuѿrM22bȈ)"+A$ Yk"f_ծkP-˔4iD;} -[ TN " %$Yߋx[V Xkx S %B#J>TF4A?/L/Ա ‚ojjg.L0Y`P_- Hx ̴{DKEhK^3ьz5$. EuLF^f/31O7Kb;!)2|qv8[bB=JVD=Һ!8¸azW 5J,1Fpѽ] @UYP-c+fSy,,Rk{+I؃yʆ)W"5[ZNQР,T0WbѴ65PRE^(LD3 1N>vӡ IuFB'AnT7-L$PIGxce80AZϵI(RMcuaU&cz4}㠈#穵jhT/FD>g_",Qj7cz/Xg@[9DDRP#B8j>mlȎQխYºŴx[Dg5'H$ۛ4Mz(3%y*7=РVX7Tb>Z#|KenaV`]!pأFPn,%)m[/w)E0N Ȣ2 !(Mg|(}$!~9qW9;mXY-K11+uMzW)&¤|̸vs?kdRMD#z{BCB+d;/8 mX9NDH}m#aF"#"dy"v g+Ѯ} ;A&M\фMI)l5()I%։4cv$B$-`8a A>IHqFsUıUO`5(AEڭ[5%5O?x'`K Rn1'jH4Յ[ 8hCaX@8X*.𴻉 bŊ&z²H:3ϱnpFX9G ̆Qt$חQM+ Ln<%X}m1åBt )#؉u zcd@``g'Dqfflڱ!&u﷬0o[׉t^Ppv(f"JiQS~ m k9ϕy瀕9{P%猠D27줁<(N`zX.-m 1$:%|ๆ-ILޡb<bUm p!FвeL.(#ḇy^-Px.CS藤_ 0F(w\.PPNLK$ltɟ0e!=7T8,-y} :měXp 56 ل:},LʴkIh 0;Mqb|ge>stream -H͊e$*2^I'YHal{A!Rwɬʊ?h#}k,z}{\}y[Z<ەaò hce52dhlsl{l!  s5!fOs'Pu/jkĊKD>9?uu+NDvæq^s56I=9j+du$gv{ϋ37jR}AKȽDo=T$`@kE6霁Mm]x_+[ll3J8b(Dz]f.Qk`sj5|Z͹:AaV!rx!V])s*!o^3(ѳbP -`۟I# 0 N#Vl(ʳt?V]˨A(zGT3{/U6mSb\a3'mt}cD"1 E'Qt:[2¿ئT]hV ""@rNj=kwK}ސn%qD'tOWB@o 'kڏ` j@h0U"R d9Z5٠Vmjs_^A,l,M v;CAy*kDҪ?֡>H 56DNt3!9jAn *3&>{U-2[g k(O](Gl'.Z6 -ıYN W -naȃb?/_O=}q _??/? ςM?jۏ?d./f /$'?XHmFE:$j4Uuc0Ӡ[jP:~Ai҅C5嬍v$=V{FZx}(Q -'N`|u!-r\ЁZ9*(q2fwhc!NMb=Pr ⪖0YZDKפ"rZ(4M_ 4 &·dKbN  /ÓJ*XɸZ_h%!%@[J{ E0#,`؀G3XqΨc>ƝYr?8;n5-}pTS@ :ˀ2wB8 O-}yEډc͔(~Z^Lkv)eA@xT_1Kxӓ Md(XHV!PG( Xvu!JՑf̀c Y -Xtu"'c 71JTxZ@#Pnt>,dI!;8|4ΩDZ!q2AZ(%|A$Ӡ2cokoN̦vq5uf=wl*Xt'鯽PB2BߛUU[**ˬCY1[XTw!ۋ{X@H/uՍ\zAQ7 Ss]aJ+[<3pag2wH4؋q^*l/z=lL W - R \φsTH+cВO[Yޙ&1Jory HPҠ(1|) z}?j1ez`bB](g.L̈́>*_6UYLk+iԷT(649I*kCB0`JF!0Bؒ>1TzBP%DļS-T v0N/怊 .C pč[F.wR\;rMdDpiWY;Ixf\ L~u-@e?Ⱄo<.3P5Xu6Vska w}AdJ -3fb[ 8 Mb.ěfGuZ D]uk:Vhǹ4,;3 oݼzRͽ nEVWbj;tr=$_;!bZ*iuЩjk4Hxr&bAX/ʴ6>% % Q&`:ۺvlP;&wXAic*&UzFX (?Ⴃ -YTGd27axwI(Ͱ"`l3A/Ќ O[@mfqh&w*yCJ2 fypC/=FN$Ҝr `Gm!(Ւ=S'KEƘ4lUa`PAR].wo7^pq˜`^"nmC۽|>FpQ\Y(P9#irc^ѫBiorb^ Е5iI>ntkZ 3h/wQQzja[tz7hI~hw 0N=Zg^cТz,:ΰJ``SB%?(_Wȋ$XF0%v{o)C+lA62%D鐅0a-$7p (MwQbqtfn:uӬp-8IP&*]=1;=L1-_w+> =Jrҫg Ciazih@i؛z_oA= wםB&=22wB=RYIUZO;D:MC-+ -,~c_/6p2bӼ#钯 U}o PYy0K.UuCH!Qn[.lכA 9z3ꊤ @q,oMlT@^:7ĉ.4 7u0DA@曍-BO= -q*ߤ=%7i^ܨrM: -W-5j3u[4PB `K -N]yoCq_~ƒc5XG/egPzDNoSDHM0B_f|+yy\OЉY#/>̡kF4M;sH闃[]/]p&P)O'zcʅ -?3,c ȮPjq5 ç/ZTs GbvyҀ;(EpMDdYL<|WUlX8% 7)Ã\z>~/^jK 5M=Xjz~ a -$e,~n$v_łգI4ά8+to_ 4lloRI~~F`q z+@ q7)/s)wA[(A'ṤA9;;(B˽YҊ|n}&t׊]2q-C I ƋDv H3CylgzJ;>BL$OƐwHEȱp= A]zfyl8umB\X8axjX ȠIrW@{)}I lrP, n QEx-&cj`!9M˾ZFo15HAb£"[{S Dޜ-@u/Π]wvۮ/V8!WVzumyzqq˧]/8/K4YYruz^i}w<iA )l${VtI}ۅ1/^X}(ߖnq!hw܈"EZd”T|R }8YVAB?K5Fr(^JthP$l  g_[AG+̨۩|XwB3f UӤ+$X[}Aߖn\:BVM2k\Ap9s F̓˪,\nH(?w h7HG -}x3 b8foG!,g`Sh!2;R,Ԁ3rU<1X2wBuE}(!tVAa>$?JR'_Ug} &hpȰEkf?gp\Bf -kg.X-1úGrB5u.kgF)[F?m6l%taQw]$mU!V@Q]rݳ%tDлf51@ -*0āl;>tR|╊S]C.zF$f荱`pY92fdp]FgVkhD"N1zuy޻tHp04U;;DCgR=;K`@q} U5 VΠG &(R.{)j#`opG6nuqL~~I8yz -&ށ:*Rt6(;WtЅ;e)r.RTXY=UC҆_jIQ,zjƪUIIV"n9F TRf_+lt^ml Zjmj̵$=1XBَ vG 2jZ} >׭qȣ #BA1xۻ=ܭEZ^4Vu!+BSj{ R.)b!R@DHg5neMcӄ%J`%j ~ @ -}q+ؑ;gfq-xD^-Xu^VCvaIe[;8$8+\jFxvrL;v3ݸ]gt@!-ًPjDzq~Jؑ"HGn9Z)~J'4i(j\@qemV?sf,ovrcqQ0wwoԾ ǡ@Vnfu6#^#glzIs;glev~tX&G8(̰V|9?ڠ |끃8u=r˱^Ô]B<Y8רkZOmjB8^Ɛqipc}{H`NpPQ*j(?W9`[ C"3\τ9JURg qۂ8t/_Ru\ ٥E^p?6P>ru&ێb`xݘγ)\h$>|s}Q38 FxX - AmxZB#̴P-9a&sp -9@}лAqT{ߩOMfx'E9m꣥ i5,iw}CVC`\{ -ߥJ܇~O{JdGK?uB7&59tpˋ莻+7/ ݠXLZp7>I1!bZ.w½7[ кEh1`,/zxx5@^gun00}(P$)1yToSQaLj*h[}*c&H֫.r ՘ -D#9nd^'T@mt -]U5C=Q1plvHsgP XN]HRiϴ+Q y:۴9yEB"8>Udjo4koBJw:br8P0ڱX;Ö -Hs?A b ҮBk;)VvWA;}?V.LQ\sգe>kW \M^\=N;AO*h@y^&] -qۏ]X-: -{ZW6yQ֬;k"vR}Z!4:\Ms;]@?g͞&Xά! [=F8L  -̼3]6I(|Vv}05ϏK>%'^ -͗%+ 5HlW >GYk~OhWh/޴XZ B8B |YPVrzVۧ}&p -OY mWR -1|̶(}k٪$hL˴`޽x0&Fz]XLFī]=f_gWu? :ކKA%~(d?3W`DL-Ӗ ǡ|fIWc߽ƾuӕ.%N74XSaw3XDԣ{}MƝIYrg5ՓݼT+}qvX5Уífٹ ʕofx댠J;vjdރUfnwqc)A]<EJG?|* ~ 7/#^6X/ÆfuF+f_-l{;_$y)7·Sh)[7')U nx[YTAK1i:96sb%?.+YVݼ]A+h ڷ5QS71UnYAAL H Gjddc=hȍv ?{9qgԀWWx+ݿ>7+saի;ks -`av@KCηEF׺mbEgrqZ()*~*cUIᬠLp6$):j 1{>j1'p+> :9tV\*I rMpCEkSFV/pH$n33^ݷjq>x@bp{@tj9EVq Z{PO_%(޲8g qDzG\ps'΢ ,&T[LT4 q”"C@$/5=õMCʺLZvZ*MńL!+*N L;_$ZSTVn8¿|g_qƒNPѾxKFUy y]oxQMʮl 7܂/6bv&*sSq4[{pT|L9G_-Iܭѯ9TOe *r }?!:j#:<=b2 >L\>H4AF\ -(کeJGB YaV@+ U mʰ4p| -ܛ[N]awk' uD'U]n =o5˻WW hܱVӘ>@ -: KE~GӐ ~-1)4/n}>`6YX,~x詯;/k$:Z/GѓfC!p )y½O kS-fm cUg[EU62Θ?{pZheOʞݙ^Q Cg@yƵZΙP2٧Vu]Ҭ~"IvrHQ+ciKx{S9"ܚ Ht|>Src:&Qa@t$k0S \:IS~m9\Zj.{ϡQ}۹՜:RJOts^Ȍ[lLP\qB:D_9/-n -gg; S`r^PD*<&pfZ=B'[nǥ0+u8lqkYn`n{PlF_Jgb3z:<]N{NvLD<|j=mlbrr%w x{ϔf,Gq Q_\5{*Oڙ(:8D!X*z&Mvrzo(^q%b:32p  31Xܣf#`]ۭh|0,?Sk"\3keECJ ("5_<%_̳sHc"x`\OImv6) ][96x}Mw^W,o$}V%x=.0`,U8V5o`Q˽UMSl爸'6?Z_EY -ڭAo7Wxj(Gv+qǸxhnާTxlp^8Xsl~Tz/%m8Avva|-4pLK,nL<.qc2<~;wr;B+ܲymF cx^{&c |'"ij_b񩹳rq={b%|CYy3uG'z 7Z߬|Z'G8C8`y5dz˹ --Ilsȍ+zMj 5G+~ '`&WVq`d>~tX*\cV&Y[\6 ߋ'º_;Zͳg)Pa^V?Kj0<^[+W:.#+aR%s"H<+SNnxKrY.3YW۹18"`BcXAJa^dk ^, {Kt -`ho ۞!l&c*. e9='qCȠ4΢m3gOorS!vܗ"&j̢Mf+"kd. CS"}BEuW:KwvF0ǯۍC_`'.yيo'7XيUl/ewhIOp.gLxKCk¾V!NjBĘtrY-6zc֏QN뒏!$Ƥ} -/zvYNs~H`V6 `;Oҷbz&~*۱X hGB ZT0x yGl߿~VEx/imWat̩!aen!? n*Nᅣo!G{4i 6V]?k{?M5zw&0` &tp UH kNѓ~?~Alu-G&~%T(9 .b1 N}?zx|75*]q &ŏ~?@P_J&`f(ycf -]4Fi[7ϒGtp*ՕW{z+dVlOQjv.,GGU5;=[>l<m3'㮲ƂDɣG8tM*OLJu-Z}VOH]+p/C|-,pil"{t%@ -a7$[ȷin5xoo3&UA^ Gg=4=QBZ'w RZo啖5'bhI3_KO&D{%US( 8l !!t !a Nfzv:nsusIa&۔SǮIwJi3ao_=<cG\0liZ t3iKZiT6VNΊ r8Zbah:<>̲z-Sz8ʽRߤxn#Y^+bJҔޙX-BNiniHI7 [ǵTMeB(<%'q:ȓ4NJ vu9A?0 >̰Z{D )cUD (`&_A6p0 `LV= -!ʉūBFfKY$×BĎ2,c"-F<E\ZK$V2.†z3$'_3E=âT0ۦ(_# +ǻ,%YLc8Q+gZ+|`Hs:5")L h% 9Jߏ8JDZgyமM(,hݮ -'MU7c1/<ǐA5,d  W^(9K(2.ΩXp"Yi9D(LV"h\%BJv&mn#~6p<8drO%m x;ʂ* -(8R-zJI4T@n5pǸHi8 ".ANkZq9MhbZZN귈9ت8w1714 X9Aqڶ#)H #a(Z +qx9ArqêcāFixn m~FOۆr. *LNPkI)[m8zR鎠O4XHGCH `8G@C3N:h[+p֝2)G&dh&ߥHRp0(SOP`5V hy$V"MyEd8h, OT<HJנV0H"0Z&R1JM_huJBE %+V959I\Y+5RCg b 90h*[m0%fʃ`!#(!hbrR@4MK0 l-[fyF̺B8'2\a -%3n[[hU<=aZ%FMs+6H ь"NP^@#ˢpAhVaI3i|"%IGf)*=0KB3N?9o @, ѱjkN4BN>[[s ]VaHAH?GovJT6T ,AKkkj2Krrҕ]t/n=٤`uQNؚdl'FbKϿ>`ig{6,$#rbғ] [K@~":~Ϲfo|~ް?h>p41gSf<$~sc??&g}[w=Mrny0}O<71PL^}y㋱\"з=g}WW[Nd-UWWv^_rw*+B::{ W/?~s?ٹ) B,b/&##Oopⱍi/Ο޳ԁťk۞Y(':=۞ټݺ-̀=eOaz 5vfF[3;޶쒯KYv_ ?۸MG;^WkdP=uu?<̓ՙ -+B:<\B>-@Y𞗓a+kQo+k~k.8'ᖽG}uk5>RR9[m'(+vwbJj=ߍyoِܵIlO;P,_嗃ƽy{xﷷm9u"]^!akTC }gs~7ϭLG2e'=y -7kS]`6|ZVsEk>y0PoxKN8rtD^ԤG -I8 _w~i ˬ3`.I&IUPTI"`7dKeyZRk_,c@!0ƀ6lmj][C\t>9dqO8-լM.3`P ć˦^: Sxٮc;x:?9Մ߫-/| oԊ\U/o6 NF-G8i-Iǂ逼(wUA`:2[\ª%65`Aϧ}|{< K~C7bPt=F05e04,nײ1&|S$Vi)RS̤6:mՓq2a |Trxw،g+FtYw)eOχp)utL'^>C{ "ӿVӏ88 ;We Y#k{ v6+ɟ,y[eL-(EE$3lр7s y]La F>eV31E_v !#e!#ޖzo1xdwIb߉%ѩBPՖMsc\ՃEYcO66>aBML̢U.[%i70ylwj]^> !a`q'\m|"bxa<^O*w SPՋ萢۹SԭEԩZдn<~",讒kѳ/cˡo!Vǯp -{)|=-1 -uȈyF5=$ǨVHJ6ШZr6tIbt^ӑn ]ELӸ?nϩS+}|dǞ1a ܿrcH:S]0x0 4VMy<դQحtT'$AnJ!01WkzkI>1֒_;M,")UV{O $fD@`6%:5$Iǭ{nqY$rEl922oxnfʆwOMg0]oqMZGz :n@%\R7\L =%^\7.Y!K!]2§]n&eՍG|c2>+V\\UIcl -V}?j?X3 m/W}Sݰ>1BdTGD̒JL'm4g!lwQpT7|T L+&Po!]YK|?ox;;mnAqD$8 -{!9s8Z_uw -X.$fאJ -&eML֎0iI} y26cmm|ffA~jS∬n,ZW3Et\P6U)rMWmZQn9 ꅅBG'|n`cVr -_9y{_9'uåܸ¡JF=߼4wgZjN}|9$#\ʖSx}wRrQ݄{I/W';wMr\[\M- .*+«3oտN~M-[ѼWncYӑ棻3ˉYaI/JJV RNqC| Je]ֲ#afc~:~T8kzIg'|^245UE+owz:c+e`w_6lPa1)2h Ч*Ґ^EiQ&W#ɠARl90Jq@q]ec0!EL>Ǧ胹BlAqh`Z%g -Bݴ_>yP, y͠Kgn^9Tf-t\H=šmaa6ch;z].(<|p:L6ceN~X Z=dVSr2N*Ir.Z@Ǿ?ffmʭ>|[G7YqLϽ#6r^%0Gs -t1~wvUn4e{{so`Z|eẙc H/iہ~<19Vke; ʆ=(W/+K`[9&꼰[/d=^Z|%T]9pyM?P|gˆr[.=/~w_T$U} \z)κeNvj+.)y^ErطU} 95ɥeIUzI|mk\Yq5 p&~&̀v}?v"s#C@uwqCfj&SQPv| -uV̸eu!-0oڍ4$p7r'Wd̀'>~Ćlthja)a4ɲqDد7FXR@2Q Cn2~ ?C0'Ll)>:A:7d-G.u'_rxUme=H-[IMvEIt?eę_03US37TA0 -@X80`ɻek_[j^0$,ƀ-[byxZ&#s nT}KVPsj8ҟ*-h+Y3Gzg -x[ - lҵlTLPٸ9y> ~T,߿87j̿Q+\xs9~OO;~8&7u(Cu,61 -8 溕#w:?I%(;.F Do}[KK!d_-eQ - ͽ@BQ!b 7%]3T:vF\׳aLQ`}lB[26`cT ޾?YXBz4^ѶTD)a0*>[j­%RqPH灔ȣZϵY-^- b婒v3x@~V㚛RѰv!l,Ő(|7-&ĴI%2!IS4)v+ RD\IJӹ@)0@ w)D.@q4=pf[zOֻa<.'f |'WqG*UJhN -YbچT6mD_d'^\c-όމRBw{gNyf%r5FU21N uRRV r8]X…U9`ǃ;(hT)Δ|lhtYp Vtefi|{4aZʁq,۟ͫw n[\Ȯa266uw38Ѯ͗]'KAŹ+=j33y]4\bVe% ]pb)wPipv }  7fdoMwG$DbU¦F -z.qImwtꞀmy}sZRSn?_fcp鰈MMKLKyb9toiQ)@mbbn 6j hm=V*#dU=!iVsu"Nceɮb#N>[kC[}eaYBgoKne%aM3lW*.iYz\$cHrUM+ջ6bN`K@#Ոݐ[@9ei(GӻUKZT22Y" OCqu$8U08.uwLMV{BfkI~ ߪ؏m_;Ɇ&/xK6E8XOQBb:&jUH9.4_TMuskMMNwXmtV "Ȗ@}9Nv)6: -*k儐 ,gɊ%f.{>8¢pTB5|:r{⻕^MꭕmT~O`c>dǙEqI'bg[*̹[sϤ܆k[ϧ - 7Ő,&t.iSw̿{SMըV")t] =SrGq6g1nP1^a Mg8| گ+OطN"cLhY-f¸ .ݜi?SJzz&*qY>WRAAoYNyO-kl4fy5USY1 -sez[併\D{+G~33إzQ~߰?'qeGH9qJ[N5 *Lոi}"o۳zpތW{k:+ߊ7}}\˒⦄XWi[/bclgF^[Kjβ!1'R¬ҦZ€AW6OR~)k4G)L1I\ɃE! >~u.퀾_0Sc®b\. SaQg!j}v%;b4Vl =ĵhm{4[s#S*l(-c}ٖr aI֫KLhbǢuD)IWNXgqTHFn⎬ -IE`9N4#EdTu[?ZVJ[9i7!0->e=;oS+J4t,{J6,nøcu5d}vpG9)Y9:x_7/YrUP/yo/|q.o,4b*e+Q]jMޓ\@{|>vÿ;ajw%fJDP[i啈lN֥g+HS1!?^0-R~Av!0^EeʥafxwI!FivUG)zs.3sA7e٘|hEɅ+8bDܣWc]ppkFViY^N^!\\9;j]-FGPAdQ02@eRT).noM}WNmL-Bc3 XVMU3&$ɺ%-WO4¦UI@P^n}jU^Mf*(dŠjK1bГp>Cа{=S&ގ\%0ۍfbC-ʅc^rSwL&j#q8ZtZsaIy^)2]me (*s\8 -z>9o7l/g~"<60Q=/h-ABuOz*jG/ 13:"06/:e5nL +s !R\8噮4ђjG~[؜\*wB NfP3d4iNq0OSz]HAv||;+ŬdiK΍t;o2֯ uKqtf+{q%Fa/ʦ~oCOa}.4I_2Q; rޅvٍBެS!w" Clmk&vЛK9-l2ae#6ș͐YQݍVf`7tvѴN4% da#`OAvRvL(zk> ~#BA"YD%u#\Ju7oS\J5x\kD]v"Kb3þi$Bڮ -oɾd2.IJ4[4*)> -}&xO&ؤyO$vp]~æ"󄸢;B[8UH, O۠NwWȻ=lQtKnwPs9zZZVHoJu A>ȡ;b'ZET[':[߶wOUR%>t-&\x'(dz4NԢ')-y.-k#n -7kSCf`}4g-Mʓ \ӃlLLlW~WYKCRy^7㶵cFIj۪-܇k.jS7xm[wbeW0?L^6+ B0و+4vC\ܩ35L^0qsmJl pEu6Uq%_MheuruE{ML8Mʉ0q1D83$L 60sq#ӽ+u,tKWB>y nɺ !gi xL)ޭg|>Pz?l%DKGeg7%7N+`-P m*Vp#:W޴HSn0q@lfķP]K;DFSFؔ].=feҏ -+'@!!{?8~[]x=wH;m,0L n޳G3fY+.qLu[7LZgo3cS\x&C/(f-rH~S -<rW@v< -V;C.yp^uInѢ(z'Gn$qOo{Ox-t*30S$ 7LD-}w:݌7&?OU7d$S% m&\N`S`?trXu܌,=mym ъy^Q7uř6@ ͘S {p>_xO! V_i+/}xBq Sv5)kmOl(ve_OvOg3Z-:LlRwMhĔU^ǿsoۨZeM7QW?J&2.ȚO'}CW Ǻmʍ}̸았Z-ʃfVm{sѠf:x]Zxp:i$`vC;g5gn+L{܏"n ު}9 -*Tm@@ {bx,B BHw-Nhc{ƳzlüF3wsxM*\ZiZmm{/H j/ȓn I>9vA-'Ucl:WP*NtJ8֝FjY)k"nLd j9P-i@ ^f!ihu?Mc 6 ꀧB50B:GE]!F KV,v<&=!^zi B`UTCJփb1$e-Cbt=}EIQ6iHk -8sC̵mgE:}rb{]MKN^ > MbL{1BcLc+>z$5օn9/BtD/yڄ:x"f0{kEGaP#䣻o~9]8TH72%$>Щ㔵8>:7,WpmQ5L*4nK;/f.4}ȑQ5[Wʾ)2L/}X/q'Kܚ@G9ՄĂ#p>,_c"7d1qE؆N R?ݶ Dd6D"+ndM/UjQ)8UȘP`:*5'\4>vtYF6 u>^/b6N~1!B_xEd"4h_\-y|p!vmtHHB0K >l6dC੷&m{ktml$2N!RWY¡\N}MްRr<6"F;AWa&s7dзKoN6A9qj}+L7)g3_ڇOeՒ^S _=jf|x vSœz6a1g{O6; -d{%#&4kb -ksmCnq.TK&-LpUE%^OM]' !LtKc\6!2oZ12m.'/Xߤ|ÍIk$6jϰ@PbcSGԦЦ>>c\ϙz>stream -HUiWio93siiAAE@@JTR@(*vGە-aR*E k$swL~y{{[tLqI+TKcv.pҤkət5U%<3lr^0 _V7mfvk-sz6s5 -Փ16:F!-M"|9C.p -ROª*ԕ_]))ܦ뮗V4-ƄY~Q߱VspH-e%hTBF 7ˏ6l.mC&RӼf_u]W% zI#@I;󺛥T`F59{2&C}KM=L=JZ71gGiMEz0ɘ~7 ->ufws/x}G*Ak󄶗{x%tX5$w1rh;&l b0qO2qABC?0Gt4`?3 V6-7rLNDzO D%DY!w-Rtcw|0\}sDa&j7rə_19hB\{]Aಈhs56sOE{Ry~YQ&(\ria>Gb4ʙQV 29B<"(rnMOTYҬdHSvV#R>\./V%IN?7Sq! \ -qGt5YL=S(|eVK2>O4&E=eT6V}nlնg߻ldPD1M* -~AAbj1bo؄UWKZ彪/OQ|hUy*hJ]~Yݴj/z&|0ఔ"JP7oJ*01Q#Uc2Ry^^+@kޫxρݞ]ٝWխ%JYu9/E~>A>joA*_y)2vuppc䤫FXuL aq㭢\XTU^`PQ6K5d|еj߈c\ _9ʲ 9\̉ - 9SsA]_7~Z>}i}QC3Ր]+y]̱s6&9ߢ@G rV#r!1fbDuyJ@ᦡ/)FmSi;8p=GE%D6lU'M$jb6 e1{D l }맔IG%ql i¬`M]qV1!Чh*Y[ȋ8/j -;J& ҂؟CqTI,#օO"FsײvK9ơ[U1b -!tGl}OXZƬKa`?-#[P;sO>VHD˧qncumڒ^{jiNa>jQ}@+Y\-&)D&gS3Ojb͹(è͏z=~3yM\ҁe0Ar&+K&rNvSՙxeLqI}vh6sA ,z5͕y(si`^ۼN{1`]qԿ7mzG~lt;<~_w_*Rګ4Y?xдSAp l{ ]&It8kk e{6EyTe"y)  $Uu iﴗhu eC+;OΫl.4Ta†`=a]06$8')hg(ct>]!!螸@Ů J%׫O{N!n9 ]]CDJM* !$.Bˬgpݻ+<ダ!-HOgP.v`SLC~ZsI,~]}#c?ML*nR> y%!xܓu>DI[ }x -Y;Bz5@&Ǚ&XC - 靥dh'Уl}!%1E/A5 doSPS]brex}%y-o`?C ɱQF9!!<7+*E[[)q[c[†g?*Ҽ":V O~ֺZ2M1ir[NAxa;(O‹N`c13v>Q"MB]w0ˮvc ::$S0GGIYGK`HTs 6j-*km!.;^vF ik 䬼v!#zK"l-Z{u)%Xϋ.GPCjfN[݁y;aB˿b6()AEG\v咏׹ -ڷ@D&PQU?ońD{= dcU1wvW/ -d|ro"^хі0%2FOamb"M> eW1#xxw48+M#$8˚絗֬U|m^4H o5F؆>F:1J[UXC ,qxUF!.Zy0!A%).wD=y1fc#K`e#B EJIB$ۄAVB g0{\rs]sǟVA EJ NPRJ^Gیu|Dxq%F]CRqV+YpɝNw֙XV|̴N5YЯC$r$t8 p)) !pׯlo Vj)糬·>y(zp +odIw* -syi̩AL<Ѷf*Ы%AB=zy-t+MǍx=Mۄx+*0aN}0 -+fǹ\+ߴΰȁ;ݙ.jS0CT3/UH>&Bp*:Jc_T҄sGFk+Y۝X !i|lx1c)$9N2|;v{_QJ2\ѵ@ ;iSȊ;}~p:N :/6!m8V,όc#r?aFa 5b^׋yu9'J%I:]dbD'h!&)%0@ׅY.bn~"ٜrAhhzI{ F0-t_L ׋iQ-Kh1c^1{*Y{^Cuҽ"ݵ6=6VG\ Ex`CD }ٰP[9.Wrԍv˅Þo|N_k:ҢD̗.vU@rbe,r Wr# blrU< -5IYƈ@2l b-9 gJ+$͘Xb%&DDPDzaFQ "]`M29{#{?y6;]Er|ԭy&' 7V~/ۣs]TNO|mSξYR.krĴ* Fu 9oq$E9|߭>V}o¶>Bmy*HӈVp Bovhug|1!ʟ*ATUx̓6n/JCCs;:TaĶ]xbnuQ25՟Ob+-TaKBOSGYzwUuqV:ۭ<jV=%+@El/Drq̲~?M=>%O mzQjRGԅ v`PC;{6@Rԥ'l;4Moȉ_ 5l0ߌyearVPE8%\IB=HSP0ѱt Ӓhۨ!wu AXW]=7%`Q>suǻҧ7Ԕ=#W'+ah'Fk},ea@Srf,;J6ICvюe.2F;x{4B8ҁ|}0BN>QuPGs<$0PGDn()ٯ]n4BsCݾ|GCchExx|O3M4-9,GYh?]jbN83Qڊ Iag8fXp$X#x$FGyt =܄K5+b֍%JYi#wQ\ >BŅhp<b"6{aE^8-TŁVYj*vz[p?g3'$} 'rRsi_ZK"y}xx6Bg';_%OV Z69SSSOI6Wou)Ǔ1RuSx'`}SZ3`fU\kl/~vn8fS2dd u W%*sKl^/T^3oYwZNsGMж`^\͋P%;J3xyhh,8t6? oZ2+1Y B6Ͼ8ã׺q!Ǔ 6P'>Z]V&uArv|u(7朥#ͮC)RPMv].raijF/:pt"27uoU+y؈}#XJnqNSm0 -!J[v,ԡD /-Jb pB͟qAk`)h䅬'w6%1je†Ԅ:12¡f*1bc}c 5/ <(Ou1 %t" V<_ w$ tW?2>jo lUQLɻ4wK"$-tum)+s -blj('3ltl^jbۧWj26YVK@g]Rxﶴ c1nTW|OFWTgw"+\͝ ~%ZbNX{B{|l*ٴgM'&CS}zcdt֮jd|b@HѿZ|O&Zf x뉎fu>KXp)F[>R$}6Q _=6Bڦ89{Gsܵն4 SȻFMn]Puj7p C-֒8fr&doF-y+k:χg]/Փ'p dBn  \lo?RW; g S\Oiniٙ{w]S4g$hMtEB% -(E^D`Q M@TDEPLrʾ̜9soN ZYȰ:(HNXCFa녉x NC >uë;;_]*TW{M[tGǺbm–>x&fVN_K-wʚ2,`h-)%5>uBP\xQ1*`푸Mk/!LFc bʩ_y[,Eϩk!_?q[]@I?<=^h@׵ s[P Nwi@?-o9 \հ!\aj/*%3y0Ns[ -CVs" hNpfH+>/Eۛl u,gVGM`_ ^m[@-.%u!7 -tr{[5Z|L2Fw)ةLKW- .ۯ"WGZt6@.t"*`›GA=:C.*d_凣Z"Xt}W.Fkds<6>RnN=֑a /'.c۽1wqF62jw6EC%P(l|GWK`8gWB6`n a+`G=[=*1u=,Rdx{|*hK -hĔI|}8>NC&vs I.|w1gOh{GZx8)ٟY1.P}MJ{&rb͕u1-3Yn~b9-@ꙺTLᶷ!S ΅I ?#oHJUܚs\lk Aj&jK GbkZ#FDHB {ՈV j{DAK88{~9{\u}9ކȓnaPk|DVp(kK^bqwY?ESɚ F5<}kE@V-_8hF$][ N|:(>FGPy4 jݑt=-:N 2^QI4<{7GC,2oUǃTb{ -RYktIHAY>Ř0s[݃9G-{ueYX[.DwMI튨R7}STץ=i?ZshPGćជ%W7^-yy9$?v)d׫Rv3i}XMF}JvZ%z*i[Rp1T-ӿ r⾿:w;ߧة5An&wXh!5cԨ{Uw?9 PMv~xgӬ:FMR|Teއ{4D'+ -C[kV:횼TZ=^{%x9Y:GMR8Za[*}R].T&x?^۟!< F꣺` ^䇎+EfL#!N": "= X X_>[9)Y)oFYܠ.Ϣ6rQ*,%$cS`]\CqN ->5F.7F?1>zGtXQ1&3,4Q458Iw-rfWfUt=fe xҰ0QJ\Gᯃ` ^ mpSPI6s2rۢv0ƍ݅|4f7tc -.OɤKH2|-pCYsWiA1z&{}{P}d_Q]*%W Jfۦ[JVͅ9pFrA${iw֯M!fUmC}b$hݔt *9GowՇ-IB曫}jI.D+ -.ؠ,>J/ xY#Asgxk;\K~CMfS̼|>';oT"^ ZU-lHw;!;m`%2 MI6'pcg&"8wGX.6AOřw!tJ^/{)E zze7\=BCצZ? ѐjҷ71vг6Ew+ P1G,dN&^cOǍw,_e?EԳ9av_E3\r &LBEad`?ߘaU3K+ɂ zVË>xڣ x- -# h,l*%/'L3V%Im<>f,!ρS`~h.  -B@+6 L r(d[(*slcx4'"|KS1Ř { BBA;@#K;;vNPFPݦ @IXY -L65 HK|{C qMWYH100 `< @ U=o[ɲh J% alǯQnKSbaT\$GD_W 8C| zwJ}.xly;5(;|^N{_iUE6e&H {+eV(BKcνppr_E/|r~缸u}7zQƨӴts*9TN$G҆ ~-fNc-}Y7b -# D~ĥ2L"Ŝp5݄wɠ HrP`~^w49i)n^72vj(]5Qf^A $Wrt"y-ȇ P97Eiyշ5`BPv\x8E sA""F DC^{$C_Q enR_J7beש]ih8^R1_2-["bt ! a av n{A}CA_,OU_㣣썶-ȝ9=TҸa1!EJgH̊.wFb.&[Nt,s^tF휑W؆V3# pq$RL%Hy5ur9lO0IjN *;un"ضԌ -#dDg|$c$j@+1_ˈG`_H /%`&P;7uOU?]t(kٟ=IͨWK<E8mXXAR No(4xIsŧeVW˞ܒ(2f3j]#XWeT?o;: |kH-*>7i]GNj[~|2x)90F-DHMH^1p(tlrbEvh0}a欏(K]nf8J\+ y<Ү?ahP[ƪl (JfK!lZ)}gs?[2f: -֦$RA ? oezS+|Ҳnf& PЕ+3]pQbX/K!Kv1%ˆNǰm aUख़7;l86*Dǻ7JʼJ0aW`R7AcT??r%n({^d: 7 R]h, 'D,6*R]JCCY5]cKʺz-S |es۟ RرsuTvˊSsP?-[6u7CkvM9!~^Wޯ*N`Yy=ըxhUMiOV hL8]{UN z[#5}YҮkźtPq>:u34Sv2~|0elL?ۚV:'' y9wL$7P>+mY iwZ rrޯWU\7Y~VѦXOF Uα'uAd*\V3B.j}To'O鴏VfWKg/rUr(9\.k}`^#ͱKYYL4k_S󆅡s'9,5gMkGRe`Or2lU}2JL?0gfi?̴/sҙM:$m'V5F!l}7Ġ ˢIә)>y׃R9]!^F97i!;P9Y`ON~? - -]-<_s/a^}jlv Tys@Y@.2nȁXVD6Y/Hp0֡q-밻nA0I`P?FҺMo'H&H.B'ImR:S] |Y M 6f}'jF9/-xMlGYz v [G @9@ܴR@HM9UvJv]e]yZEtLgڣ:󀈞 z̭ {ÉR sTó%BsKgP7쐻*Ŀg?j>>5k!Zvdw]PV2ř=PzXkiguLrqY'up vl~qd*>잃˞͌ԂTDI|~f!ʾ9MaW)]"r-]}RvBotI7' ? {>6&OV -v⫋D -k*e@?MɂZy7VI[ABwDJupGfR{Ll٨/;䞼7Y_ʁP -2Wn*ul^=w]ŲO@ʻrX-FfWs>Jʣ[IT&=n& -|Ł&q[X4c^RKcR%Q}9 P-Lxu(0?&iaFe#`]ḝ KSՐ׌mգ2uOE%P=X{P#Pڃ>!d֟XA(XrPxNxhG>4L ޶'L\+dfRJHBLڮGd]O/Z'N1KvPV\ETȭ8Č ݋f_^q%<|iE˺XIe 4i)<JM=v MWС=32VǮ!ݣ䑫 qp0 ^Νg7z$^4kit/5hAz9)nU˘\W ϯ`%LfbwL ?r#ET_ zn~_7 -Zfs7c;n0XKPXM@@P q8o: -zzH9{˘;WKUBUJXe762VFD$I3ySp0K.&0ę.zP>52kJ=1ONmbڠ<61Љ˘X͘-,7+92 -Eg} F ̾O~Q٬??sa=;C;h\_hqwPWCć΋g̞q -؄ x:סn/baNy>Hܔ[rVJg@ -w&FW8] FSF !${݄ -!u vTcX'r -,)Kk贙;1OwW\SHo mQ*",a-K9dV*,M9:cꨞZ"ݍ&YYlŦSF2Լ+Wώם#K9l.e&Lcu x'(+<N VՆp z8\%܏%2zyT1 I-q(Na%IAm:zoOAi$jCJ.!%'].}ٟ\| ZmGvWF7Gڜ?~1;gR&~j,vPA!n -;.fɮoBR -IpRłgv9u{z/rFvvh4sOr_yRr3 )#,lbN: ew✛A+/y9HtN.9nap0Yv'Tv~C7[lgeo^W񞲛;TJ:pM)xKMAZ8lCxٝym&-|l-X::fF>,aTsjz[ZI~r)T1rܽWT7BKkX}G -ܝʞyp/_bс+~OK !ؑ*J}E/ =!Ώ 5z9LyNInIwPU\xj~ו'jPL/ <\maa姗*tu' BI# lcix7x3cSQdv THnO8Au#Fln{;x+cd៣ 5撕.YZPry۬_u յ:61a F tNh>g@8}ۑ":ZsXM,coZyOlZOS,`R8Ee&`Gu֐32K.׀Ld+]:m"ڑ; -SQݍI ) #IO衮,-aS ^%I;RGeoNUH^l\Hjk~^* -HIk;V1By R5Bߵocq ˷7PNsesQSV;QZLqK4,%dgG͈~sic_zQ=%ζpo$wI6ħ҈Q=+Խ-MZy`}L!2zmLp2ݚ^jMӶeTJn^:u׶Itk< -6& -PSC,j.zyTDkvycڽU5%njalp߿I*rz X5?_83U/ڱ-sormUWQao)-'W, -$>[NwxVoчKvc_|22rJRDOښ7>;^Mia $TD>w!\zQ]oE%M[ =s6ױܖ<T2@g #3zCBȵM j~ -Ag+NIKv yg6~}0pi~̫}(aelg A~% !Uz홾5 xqCsJ0oe;S!cPC{|l - 0-dV #c'r&\7ʱedԲמut1'evqGֹ\3Ңf2]mόxld$f`|.-묶M~jmJqKasj6(e60q M7µN,2#b|oV+'K:Yg2rї)Ry)g4 .FŇ=wU+@e{~Z\zbNS*M% -4q^ykNmRh]' 栿y4ʾ_eÓQ[}H]c;-i WZ䜁|\P C OlmyYoyгiq3a_ӏefP b@JZ< 45 ǂ.:a]'+K/P8֒肠X'!F -S -n՞))'l`cI6| Wz$FeF !d"Ⱦ`*$)@oŞm$h`v9 # fTs} -jP7 -{l}$eW^i Q|&V ݳM6Vڲ;[7߿afȣל|S9У U>ktFO<8],b3gER NsX&JHE'6%$tsZ@j?PJq8@y谳 46ۘ,!}Ut?"C>#m0PzNY/5{d pl{lDҾzļ`!fRJ?!7䝕w˴8$L 9Y8|)2ј,nxh"č=h"Y`S\GqK;LldCV(zmx)n#s˸XreQBLZw513++igRM%MIl&N4j64}Ce@@ّE%6h5FqTPDDqa-99s7߽syBus4G袊QT\TnJ _>>t Nhe3EÖpġ aNɽަKuBF -zGJ\]ֶe=ۖOGoō# <ؘ[| g6:""TԟC:>`ꕮHUIRRgɢ{f{0i.YmkߞXuD(Rɽ('}9tiӟ] -t'_6G 3cr}#ٯ瑶LĆղCF.DCa`mKO.P0ax,hY {PדjgӴa" )wh`. &5n96Ao舭LCGQyU䦊/;0Gٔ~T܆qGfz -ƠM92~KޒqRS_S΄Bs|gj4#iJWJjWqsVP핱kweܕWȸ=Eßlccح=D[&, Voh[!.ekKJ/?<hh亭)ZSJ3>G)!lKKu 7=R Q/Pa\Ow"qRx~ 2L=%Wz1rMْ=Z}׫$W`K+1ń<ƱU$z6Ϋ"WnC -g+M<_Ghys4XbŚ7C /i4m5r!7A||GML2괠J(0[Kȣz 2n^_Q,*sp.Y3A-^tqbƭO+,rcypORÕ|dsC`>su _a~_>\tukLJM\O' -Uj2=LR~^Iv5$.~hڞXRLV g<~` -1Pz􊵿4ʧk( (u9&k #Cn鉃aヽ]p`b:zm gVG-B~)j[AHw âG s,[JLwJ~1"@ߛ8 -2`'} !]SE^lyY3+a`0mEcA\9+Q/DD9lă!\fg=5}WޥIo6%u)'&1)'9M{*~sPC j/qdZį"ўXl!iGA(Mz&SUĂq\c-'eƻb8D-_Q~а~bI9dm OVI3ܒ(Z/"8D(4t/< o\q!:QdSk{{&(~-6ĻSAx3>mhkIwlx=u[[Lu\}w_󥚑 G -hk9&6l>;O-ܶZ%4w|*F%:zopk&*>Ut烉Ip+ 2e ր»TƦMy_K'/uK¦ήcP7YZծ9{&!3LZ8, #eaG^?eVFIL;I'=Nj$f;,Ƙ%.q bYE@,&qMĸ QAV "lj줦?Oy})gfb{Á&l vv;JXc=Ω8YTHl;zc(ȲG[i^y㝠7CWW慵W= -VWӅny6dk9*E^i&Zミ1uQ_9ȨVR4gWޜ><ݑs&ǖ }u߆xee*p.ph(`f0oaﻥ{.)I@ZR{7ⳁ2P榴-qˠ4Axv{~DE̡r| -xZHfܑE(kH 6"dļ ۻWT4 & ]>LR!#+DC0ER}]M+1 -ys,ؾ= -{l{vz}2l- h!½]W&,0߃;GߩXJ΅-ZPW?d[ɉ`~^U]+ 'eP$n1k-ݒ##y5Dh0 ȝEã?6ACw϶GEv`{jb6{ N= %6rY'͒ ]M}s hm,s0Q5=PrR"@q(Za%<CRes;@"W U'"g"ZBs)=H1@)*Tl'$d<$j k-.*c]Rxhbc.[vM@7/olM7fYoÂz@ƃdHh)ǎޏ -nɲ$xdY_]qSZ;t(wN)> N8XyZ;*jdf)ւmrcf\q V%.򞞷W%GqwxhKV\~-/-4ԱȪ5 1;7sb|u bq@*3 tad^qyg ے3ʴROiy?m4W0*Lw ձ)|ia.fw^0FٚZ7D{7Oׁ0p0_##*{F75kU۪vXHk|dVhx_آK#? aטsvy&14QF_pJ鵻jQwpkpo?H + LܶM;HL}#ՁTg9m^)RS`k<=Ukٶ1FVɂ̳.)3E-\Ťb6 -}GM*H[>=kͨSb<ȯeI&<^.Pm sϘRvi"!|f,.xdҧѷԊ0w&eVֆȥI/, \xUrMz;O//#6y` ?{ziӞcib6I\1 ^e q8D ы=DP4ioO|ޟwN..*ߣvz);3sq34qh6o'.}$"=Ӱ"Vf$LEw+ 0hWt<*tv!k/#oܛ>}Mx;C -눵 Bm@ AXyL&Yh\+:jǩ3*5(ffݒ*Ux%qcԷx+ݘ=06(u5$jzv׾г:-ȼm%0L!-p;pg3 RIHNP -9'4a!iW[pbJbz:H#6}g -Mv5y!w1ܚKj&άoydUyn bieq9Ai;)Df6aTM y= pIeحĭؚK>`OHӜkݹ񎬃7)#Z"VhuQ=֢_4]Eex^R|E',p0*N(<(uf"죃qn>dO5iGF4'_w*_w>O[E$m-ǂs82{ e簚P0[ [qLB-mР@`%6ţg^6fgS:Vg`UʋGV\YDZaq@ojCMXY@w$ koHwMW)W59fAQ _xGllu4؝](nJFh_5xIHvfD,ܥdm)_"UFR}bLOꎬc*V:lOClkjB -V2{5Bup ]vKM!-sΰj7' 烧_ؠ}b&XYbW&bM"qTU -dp2L ՁgʫI}sR4{ t^'(2-K.8o.{Q,(jP}Z̡%UZߞA-bAk⊫Q5z'bU ->/\ژ]}Z%9%?WW1Ulk&(*^K#o.U͢QXs'6u$ĩ״ܜcj>7a?ϓC5llq5Tl@-9W׼wQLt=IdYt⦛N*G/Cfm)ADדjlyD&-- e'L^D K:w%X \hw5+EFRkT -n|?nt~J3M\M䢗6լčb -ދł E)bAJL)3m$9!/fK{(,sNF,z1!{z6(A= $/\D۔ v{WeP['L$:2J =&]U@.ี\UF>SC }_}f F?#w{@S7// rF}`C>Yc74ۆ բǛy\1[QaޥaO4\!OJv!bZGacI5I?< }iM0oE}[Ơ,R -]3ԃ X}VXFTz?+7H}"ha6 -O8</QJ>ۗQZN\`0,csޠ,; 3 obov/#) -&!𯞓jOnQtH(=MS[+ÿ<5#Vȋҷ&RP[ -FlEP.:^z*rOuQ=?3 _f~5FVm.a"T<`gA%A¿/G$~u^UygCFm; ϳOrJRWłxJj7C:IopCieC6չ:vG?f\jl1V+T4s ߃LkThL3/=ϴ"β~Pz4;8dj4>=lrwCA^ZKOnP%KLy4] J hl -r*sϴVr~qj ,`\m'g&YBQ?.+jjŮ7>]O٦/hf~ [ED9Nҽzq}V!y7= ~rBuQrql΋+z ȎTOs4R@sB..-0Jnm&ݷ![%a[Wz'yF鑂0G+5]aexWOYȐCo1Zڭ1d_: ǻit˩%N&m|`|myYZK@#FaeDqaࡃnA)n/m^ܐ7>E/]Y)1<)`{EXj>'8Ea?aw|3,d5MȽ)|t0ٖV:&t4DJXMUbujgc\AhͧZ%`->C?){5G ܎9Yr2Zg9(*GD]IKR2"("ҹB:|)@4ih 1y3=Iz*ɩ&g9k[ϣi/տ8F'ϱh(df.[y$G9Z@iek.y-s"@[^ysU?*1rv,|]K>SK*bT]+(ԜizS<~n]]6.%d"K-˥В8ɰ*njy|8I %8u18 >1G+@;rQ@|jlW[wO31MkbLjj82FLT4q -.D-{bT $1w~߯92 -b,}av>Y.H -:UE6ʤǀ~{ Te97KtR^le}[i?7EL؉VTQr#.T\suh 8B˻y4!t׺ɯچZWeEϾs@Ͱ0Sc >åeT4| ?7Q^:`VXؑR@XiSQGϏ c3Bks] tjS}+}hj̻kwJ5e ȌKRXn#щ0rN@e(͠2Z.(%>d_Ǧ9V\-ƽ 7i[CUW^=l7Z{p/)Bk3_<&&t<5` c7K/ӷUzͽ b Kmnh7v`k!iW`ZK|smJ!jAAiv1cJb=0K`yZZpv9xmwi(fcu.\3=L*;SۉMw/0;dS6G2zk"½@+wČCU=`i[h&/L}t\5p~l}eĢ,*xi)vZ 01I.+.\ -vd+o,Z=&Hïr*N51}񞖴',7"&kEDz)잵:Ҋi\]IJ۝&38,]A*Deaw޵}5 !(poNLة^1fvd䎜 Zz.՟-Q.-d{C#uN(9FrŦv\9wy _KuvA5FN eQqQJtG~b2po 䜩(N5%ߩfոXH(m(]ſ;S YINV|ұ+=:|QZ@SF76v6NV:4a&4PcSU$얃i{7[[|jlt^%%9w?aͤ.GYCՉtt_]\TKURJ {|ߝ&n}=YDՔ'1Io+l!TjgeU}-;֒4VԮ&ar›qr}:nW^3X[D/$t@ A[d`~YVOc4_! 5ʲ$7>Z_c:j,HA榼E.g"ї5YgxM%cl`uUx8]WԊ|ԈP ݨU'3 -$:U"8.ggu0ǫvKFb(~]`8 6@>9n ;C%"֮y.(dx "֧ڒyQɼ;o [+#갍Ab86/+uVPpwx< yIqЃO 2ۑ)6SdAZR5Pw0M8ϢL1 Y), -ra# g˸w^3>ȉ6S64wybPm=!ֶ̟pj/ߍc}?٣b3>^nbP9&*3Ml]'./ׄ7Ů !~Z; WVI׹/,i93,O),}2Z5R6P <55mNKBJ(#:SENjOf$:莂bS?ZZj -nJ#(nT3/l}0ܜ\'S#ZNԣm9>aw Z@Go 8G_oCve1GFr։nwtȡ~ipSx%r]-N fto{Y dz*2v`LU=\ơh/S<š7;+޻ʞں([Bug_bd팊PrҾ;Y[܄} \܍?4rfbs%gstR7ꍾ([Ю[ %):aaby)4@D4RBܧ[p B'I%MSBW{rM這 _s2jZkIn0Yӟ --ƅM|e2fk -..jpv->gGǪWЋ*lu&yJTC5bnB] 맪ƒpWVvK/6UΈu#Pb=և0v\ qnl) I:cJH;wJc?>^/|&EzB_譠nɽks" E8G {1z`kG7  3\jB6x*ѩ۟ &MP3N'&i[,psa/²Vxb&y`1gTfT4 >}^"k =#ls{p ࢎf'm :HQRݒ &j=io2Lk20gK̥|$rW&qjZ8Z!Y]~-7ԣ;QԍhD->F>K.MBkmN - ?*sNv%g;rj^J0ʨS[ >nqxcVtť\6Ɓ޻\0)?W8ZR yPG97sIz>) _-nV^,J֦ežs1o -LRގ]d驊z4^%YE^}׈a޶LzV -~1ۘ7BI%4|%zWi~B̈<{0.aוԔ*u {HMZ^d~X5`{j*AvAf8%kYb:j,qcgߘmBM nL@^.Tc/ϰ+&&$ wGRCYmTJ3K>I63褘Ic `'4KEQ,D"" (v[syP3|[b8T_ -9Np3Oɦx&y8GZ+|+=Sa>&i:Z3jN L]xb|3]#P׺Ǚn 5;i!BܾUq:P=|[v5,k7)F͆*i>RG/ޛC5 -kvf:zKKaw * 3رE:&IάPȰxQO&%A튊5+pq;:ȥe|v=`NHM0g*X D0 ;NVB4 W:լ\<$Z$s[JS׹E?5 YTaö4|07J°AQ|6-(e஽2rc5RkUyndSH_(~dntosdWʡ&$nH3k}r}7/lj BRXy'qn44 CU;rZ:D 9 a'& 9J.B 4"/L6#%)l砏n ?MkNRҊ6B,=/7mxMFY[~i!ƾSuR_P FfOQ'GqaWByr<"=}ﱽ,bSQԖ{61ɕ}j@N{ :#3n W{5 4Iw(X";2s)v}DӂF̳0:&XsVګc"ٲXxb0Mu4TmI(<+Ҧ"Wx gl@=ܟa ui6R;wYVчyBG1M2Tke_Rd#m]l(hc)wࣶp4)ce$x}h`Ox| $BsjmSZ[;L[[ 9*&3BNI2Rʌz~Ez/o %EI|>zRcl!DG-xchk1Σa}o<Ӌ=Jk9E>N=H64^T%ԅGN̋(%#뾚7Ȁ~ Yun^bɿt %uƀX_~RJC%ʼnJVvO5=Xmҵ>J?fumQNL&g<܍M}r,n93E5#=~:+C|tt"N[{yQ܉%>T 疥%?ió]M |k?NH+KQsG\q3b$T2sNwaG|.kRv_ESz;nf$'Ogou4"C击D΄X$6|llG4\['6y]@(՞] `x5 F ,x:.iZZ5BɲɥfTK7 9%"z(9=6Xvb/]*Q|`0-voO3:gE+U*❮ -ץ8]fɅM\5Iˉ(eg]W>bSsf*M\S-ݟi{բn3k寈DddǮuT3cjxwv3VgsAWosNS[d9MMif.()*"erI1sEYM@@EvPlng̟r9s>y*o[y!/Clo>OJc s5ݕ+E5K)wHp9F~eo\1vf 3>Ҏ%}o]cݓج߬MA3qavZZC# }+F@׉XSpsf:tIlEMQگ.Qpwu"l-kmcR59U) -<쏾mΐ^z e_זrƖpMTgaFp͠|G*Z|{ُK -)ȯvUkؼTӎ9&{xx[* T] - -~U3N׎RPQ?q3ǵDw78T]r =МQ\0wNfaLOzzkp$i~_!79,ramO3*&v_yu’ Yc T|Hwhh аxmSGF,#ž\({g4WRIhX?][m&U$1l#01xhL|:r(Mٷ1i#j*9+$_]䘮5+9GH; W{BwkosHE*h%iT-ՀqZDgP;+:AflaSҚ~ldbbX<0{WzM8XaBZB*^[T%,l[NV1jW_O5dU 봁o -}䢚{E,q]B|T[hsh^ >GZf.k"ncY?A]T!&,hH6/B*ɩ> F9qzc_;jRB˽2i6Yxfc?u!oASk="75C3{#:<'CBK +^& XXGv/^{Jdƴޒ @kL b*&b`A.fFEl" @><2>ğ݂N.?c -_Eo<ӌ 7S-ZM[xJ_gfiw`$¾ lȢ -|a.}sX G;hqh%! 3gLWNpPm쀒Vw`z.>B kXAu(+9meq>W)@Go,BEDB`Kl -ȐŞT_c̈JH-)jᎌq-PS 69NV?L.1+܀O[W?[5Ut~Q:V4&KȐBH $@Llc^cVx_{all\` i5s둎t;N̬vgrʄ(ȩ /Mv1V0l,51 tKU9f=iy\J~+$d<Ci^9(3 c=V'lFL!'fy -[xi}-Ӻ3h( \1]H%?3M'xUr)nxJtI+?;I@zѓަtMĮh$oȝ7-9ø&:fWE|4e낈ic싱5J.F5Kw&`˟j@Z,|c=|Mp{ޞ*Knw?QÜrX#U -<*YAr-$]KT3U;}QV 49kG-8+u/8ۊh8" l'%7$au ncsD8׍/V'.93;=%+@*9唈eT=ujnUMD8z-ʽn`5 Du[~6/Z/Oiߎ,>j#d%ʝDx6ۊQQv3vΜXgYvwQϳ*cQAUvf<Jn%tSjQ՗\ @ܠϘnE'Hʿ ⱼC0VpI ->493~`'Ӗ2 -q_cE# @uFM+T턄&]dzTDicR>'ArML*!"iZ!=kP&x#0N#f*m)u0v71B M{_0~GW?5|N4oKLԎbH^7uY^#=z)71[/:Vj1p׬uwI1:=ޒm'o#ܪUc&E0iD '{3.&p:aLE@7XrC尜YB?Ik~)h0xaF) -?(Xi~+oiJ!^+0e%햟HµZD^1XHlka':ᬉwj?;G!UjSoynΓ2"2n}ZPsSߛf*n/zdp -ws${ij=T'c4"kd ?:w YW9fpV {$LQjIr?kom;qBTrIͪI̤}7E'Ꮧ]L-dT(lMQ.Kn95M"-8 n -3zet\Gg$܍+WDP=,M]D3m7]U8? A  E vԱ㦵ص'VdyIBڲD˲$RCĽ#Hqo)KAQ= L}^LWٓ{H !#5 [ -A7bS@m[GſF\k>ɠl:-hmM@;̉X'EyZ&[h(oJmM-j$ymy;~ޚda'&,}U"%uTŧ۫Zwn '؃ O9>Y@Gy-Y,>'NrjIDOsͺX%I}-짅u@rZV,:-p^6V=A6|6nrBMI/;\P"0Y4b)Ze#6b;P{3υE<4j@1hFˆ˧N!koQLGp~ͺ*at0ؕ>Ke^ θ3*wC;{7}{iyfł(ZfԣrV`qLA F_wj$/7$l]e/>y0ufCfP|ͦP `37rr}%͂BC4.KX :Fҫ6cu,jn6fa6 ;ƿ(ˌ1c02e/Y -,V"QB%KYR:f 39uιϫ}߫s$dޜ_ ?7jYNd:L~dDi^k a(U|"-IlK15 o#i4WkjH[zzv6 jTnB^$|X-8PcxahZ6h|Mi4cCL (!Rpym}7n+zs']>kP|a퇑&c PG5"P`sxPJ %d:}gFCŝ[8[FVᩕM䍉0ROTXTPǷs'9S߂R$~(d,%:TeHu~а`^qP͊]Y8RoyG,MN)j4*[ V|:8k%g *ۆ1YT nBPT54o"BAdǡ -q]sÙnہ|aYk;b}A0"_S<%1AF*Aqu}ȉQkUmik*/֤k5ӐarxZs,R^F\(j»g/JJl偄82Hqd@ P,[Xq䋸Ķʪ(fBs;3čCul^ܳMK/2_3Wn0w3^ :N9Lf0#2^c奝o%eNR9\_^y^:(%{*~pbOIPY>j`KI;XyamviVԁo84g;{A;A,b1? -&d]gԯ,E۽S83+.Ib##}Jm*!;OCYOl|B{0GF}8un`ll7w5CrmHu_> ¶ojf*\wp|- -b0ϾI`ƙW\v۳e{p) YX[,_e|G9&>Q7,Mzf/ oA436bJi3$cUO Ə١]y׮YVn6i:gJwEO. gcM-8 O/ -M=8`LsumtW/5&ΰۭ6yX*O kpY\;^2哜͇pgPdB=H+Ci%2)Q|ǧ':ٵ=uWc ;.kٶJY7&Ocwtk+޵Xհ -c@B - @hrJ -^ߠ~1#\g϶Ӷs4meYY3vhyH!*:*x rrxODʲ&?WDDd[!֯ /pT-vt4lvj =_I66M0sM z3RcW S7+j[&h`#%/Uqy3x\\3QS!>)9MHͨ8P)Wnn'Zsid=xmYV[|a4;:6TT2pMWC?Jx>^wZZt?1w^x<\x*,HV -x+(e鿱ftLAEZO>*{&^cdOG_v|q/6|v_>l Oujnʡ%qhŐz2ԯs!(=T"E`5)M Sg!NqD yDLQ޼wֱm{)= _h gr3C&gKT7R2)/YN4lz8q:`zSH햮bߵ?$m&61Ȩ L1"r{\cvЁNM .TFJ&,,bZiՙM5!%`] @ۼhr'>V aȜBn2G:7xЀ7i/+'PS|~SAsu>Jf%mOƱ&RRvA؎z_KyKq= )hKbH`Hҕ#ďCXYU_n9} #n֏RQgyO!k6/ÔAΓOԳQsnIPa5k4{Oz:G4 2yc8C灗pʼn1Y)abQϺP-cA\@m͢jl--.0 hH #[^ [ 6rݨk 鱍Vdj$i] -wYl5&}A[g?aRlNꞲ ɈeRkY+ܷ /!͖W(e1WVg>ns8Iz XkHV*urj[xcG4ڈX`\Mz|ywd+l|ޫ.`Gݗ IFCh#>rHA֮'@hurj뤐bl L)#0=6ia ,^ĩHxԤD[ CPwSӜy}^>69_;K@?ua7>H>V*RHOr[7|VFχ궨2pb@/ۋτS -qܛ`i3tZ@Կf74x$ -˙g-JM5rYgu)kҎnL)s3jͧEӢD4@@<a;#cVPrXcи2|~rMTל0pQ$XoxF}JMA!zl.X/n,,K!Pٷ1]ݛURO _iSA,z??Axg=}$? LTF_Tڽ9j7Lwӂ(F۹V>ZR-x^DyGډ7Cxʻ9 3;)nzXГGFbq!^PWsf$P9>ӳV2J!"+U -*4{7>ۊXqØ.+Řs:c!nـU()'(t~aTI _,+T2EʩZw -HՠDNCQҷ}|bIwY4B%Ģ_E8NA}No4/b7KHW!FsxmgLL0i?mRSS3&N2ds5QW1VxP.AA3$( %9Sx9RIv?Y>wWW{ozЍ_m\|Ll ޑrQ ӏ-oSH;Ga! Y@nH.9 |lNЈX <J% -`2mA*YȤ^;fwfg11A׽lrIusGcGbsU%w%pK԰ҿ8ؠ^XjhHٝɑӽYɩmqNglcQг=w\l6EvհB;`-e_|(lPw+P gٛ;tϵ]wuW_?g]@> L^xf«+i-}f"q Ǔ6ј_뎘z;o'c°Sjlhxwn)=?3H+De|꩝8rH9;?5}%Ƃw#+MngLFKUT`4ki81化?%-or[Ԏ:Mՙ0t^h=QYqET2 8+"C/zglQZ#JpaF*(՟p9 2^5 -Ľ{(LP];{=(yM -8-uk5[ޠ|M1eLNm,aJbꬎ\֒%𥄊uavC1P]t֤N8}e\b7U}i../zDMTNEd,LX2mMӤ(n%7,iBէ#ĮWd 0wng;njTUJ̙B QZzs2 -n;lO&6';#c$;c,#C)#7b"8&XGk305CZ)1EiOheob3ƿ1X~$'5"^[bBA -<P+#(a#'c$7}spVyz?XQ }ٚ`\8EUL^O\7{7 IGRtW Z+~p"ŔV%5\l"%Wԩ|ܱ}~Jƈ(=&ʹUZ&X'" _301aI!MIw"k<$I%4 ~ 4cbcW+}aqZj3jDud VSk #EDHWVj܁w(+~zuýks+zGuEjccOq%qi*\hSڪYz$ q\EPDEQ̭4Xqe@E -!i3|xsOf[bɹX~}8X/!'MS8Ё:\&)%jP3$1XNN-e..!b-yҴO욳e*HUp圧f.7m!^#U3"7 8f ˜Wηf[G/w{O m Hkj%Jp`di]VpuomH̐uD/;[b`NS LBL 2P;Pŭ>4uʈO-):6Uk>”=耭qrk˯?e,K(Y -rޥ -=WR-J8!\ь?[`Z!nŶ$ϯ y&r|]L/ KTILNޛw#0pOEuI>)~GDYHP:8y6X -3ä+TϥVԝw<ӣY2&f^<>nuW󎈒{Tr7ٷNP?uͱ,aW[(NE5*x+ /^D$&.%ecaWx6 t!n4%-y~kTANh3a(ybG^jtOfgy_#e#$ DCD4d -z$(sBuWTZᩲ6"@>6dݵOE Y -:U+lp8?[p`໬BR`~٠=F6O&*U`2*K2c?*t1! Dx>ϥG9ߧ #-l:۩Ǫs/#]ˬ0nnj_OsgxUU,sicuDIͿ\cܚ:[>Tp2vпj, -cs|89y]ꁔ'.fIw(}_=e7:pubR!|«KPwnmM$&^c}kcB䔋vґU`#RFCFIޗ@r\EXqα0H3+vȈ/pHyGI4=8eiC_C=,؇c(~s*ɛJB\-h"n?_љts%g6B>ܐ3>bb&=* qI^S @ju4)L왦+-`c(/*l<4|FUv@ -^~] 1^쳅iLPq(9{:Jg6}lwl3Yi: -3Sޔza~UޒcBݓ@ENՁ bfG􊦻&S{~94^/!:FMB8ŏeޕ>iU[4mМ25wbc <#**C*( -8SknOr~}}.F4;kWHIWavatui{3 B |Xi()i-!-WWx4z >)3ʼnVIA!Es`5m-Vdiľ"aF"B'>6"R,sMŪB/du׊:3GݝAx"_s -ls,Q ->y ; Sw<6FʻHMOÚs%`|s.޵CSF9 pn. -?ߨ=~%qyR™a[B) -t~gv 㙼 -ko/}ꐳIIARudRvd4Y纏Wښpg;I\)6`G"ԼL,-/Mׇٛ,Svc]qQx݅zyb'n^v^lGlf?,sPLSz47GF#E wwKRpK -e!N(Y -` L)pM/S8C܈9rzZRBŸҨ{L۱da}GQ~hWZKU%'GzIvr!gQ2J5+z?k+qo[ VG=^DUEQթNdLgIN+ V==8E[S= CEzf'r8/NRV"l\iM "$?K*>V/f{F5p4D-cCx{O.U5EHim$-CXƈaqby n/ua$3Qъz -Rc#XucgK۟ۖ ]E[g=QtžJѡ?w9%)R)PJ8^lc_63eIұ<%raj>SVQ>ȱkƆoڊyxo `F\(ku#˾Xt$BQC̳VW&2u8-F_'ɧ&Ҟ~0S"op>Q\z>慪h)5~Hs3iΚd'CH {y:SUUTûIN&tߚvc9 FJ;;chfb̘)XbƀĂFDQA)&jL* -""XAE(`l̟sϖ)Ѓ)bsU -F>3˄8 .18O}w 2Է;th`̓Idr(6' -.zO샵zޯyLmbw&.Eث:+~7#{Pqq\$%/+{qk=ѧLر]┒_H~|R y_$Jq;Ra}a%Za'n |*znȫ>a_NNlfOmb+춒usҵѼv$ A##6YUE[wy*)'̪t]- HU 㴬漈q.vm(]JNY?/ է/v4]hYUNYct#"75I8UsAq$b ҽ{m_P{c5 70MkyJ`(kiFگH'.tScۇ7|p쥡pF}G:F8>Mg?{U\T璢~oVFL% z9_pɿxg(3 zج D[QFji w^pn u- F#ɩ[Up<*.Ps(^keܜbGC^km6)4TJ>.aV{D - '8ȴGw:FN4`ޚ({: -ۖ]&|BS"mӼj -+Ǧo3כEѶQ*ƿpͲ Nq+@@i>kFt浩ŧ -xQҡ' lıWv@׾VEL '~[=[ 7Fiԋ'jNo{~aΖ(Qԁ|[rjo6tL̈́^A_.7b#)&7C*8ajM eb3`ʇ2:觠[c;i(W{ [}S=skB9;6 1s$TK/6Uj#qNu*7XǨ`S'7څ~zfG᱖ 4BM3_GSgSibn\7^ۄ7Eh -1R{aP} :(ڭ8X<qfl]}ʉYϻ{B:_bw!C#Lq'+Ba0PFHů؛"tãZ U <[M/t'u|CLtUǬtߝfZzVuXehY5@lxy:25qHKEa0܂S"xL#op!vG)3lm9++¯@\Z#cxM&^Z)@Vl$%8@@][ꗶ! - &m^3i]r:r{!sWaS$cZOA$&1dK.8WMrb ݯ+ǖ# rJ4\{ pn=ocx "H*S!O[5OJ}ä~$RN5C c 'yHY.6`"bkHgd*)8AdЅ `z %e8%&̧kaE@(ֵ#WEĴ}Ym0*n sn(=5'di-.$_M/:Ja[Ռܜ el3Nt~}WDž]׮HtL-qIIc^jU5׎T>aQ~-gzedqI9N1HM~4}`ZŴb7\鍹cι\"ئ\QALF&hj鈹NiUzegCpOˮnGo/eڔVO3]S52tD2MLeUTQӉfqTEvpE% ![͇f>Suys?ujyD %BX[tO>]Џ |duкԞ3>(,Xk`*9$;d0;wOtFnxӒgmbh_ < [Dd~ۤ z\Kt,zȖꢶz) VWeM."|\pjw=d=~^vQï~~at5B_w.v$>} 8p\eim| bۣ}&>ߘj-HpL^>J<Ҟ6ҹL+l +dPk{汖$Xͯ{U Eg't3kV.YkCг:~Qm~۵O!ɵS)6% dg9#$8ZvxKel1 5beX-4Nݿ̪fC8|r|a@Dƈ#nbkL)?ٯ TF42=2rIPIFF.9Jw*f>߫(F?vΣ!Vr %nx ; -G!ۅ @Qv(HǠ]MkӨL,0p/" TL퍽pdKV ՖCrQ S?S<,%X]13 Tmˉmv#ZLGRuwWqJRr63bҏt򉞆 Ȱy.aJ#?`౗x -Xc-vхdbw9%yӨw14wM*Iɥw_FuwVSjM-wLMa=M}3ن^0 DM<鏼X -_\֥8n5i5"6vкt@;<]Adv}Z4j}ub1mHSs9YX@ޛiH5 4D(gʈE^!:_f%r< pP0Ga# 2PNQ;Ĺ}QR切!>?bZ{xDW~ZݴMB/;ZVw] .)ҾL+>Tp5%F@{VyW'7_|+??k)J*Em~{8 nhHxŨ'NQӿBTWF*(؍Qt+䳟RbW*&|s ݟNNC=\j|沂/ӎgDm6+mˋorl -_v%"AZ+怾!<:ʢJc|E=R0 D%V񶹶 hI-rӽzCI1KB7%kxE熪D:7 nTL`Õ TFX $䰁I `yUӭ1lLiˠ|{b3^+d[YjV`a[}_s_Cˑ6ۭ!WŝE~_铉!68[s5<7Q+u=N2A}/ٯJZjRdFTGܳ͞Tze'n1[IT:`*J[L-g϶<ݷ K C6:h[tHy3w"y}zn7%+Q%,ڗ0&,`^8:;Y2}*rMϲ.PJ}n' oP?k'cf&ѣ,Lc_N*q$ie 難8>}L[3[3MSdjkvyejy!r~QDT0FMJPQ9E9$KnҲ'|ޟ~>qkF4`酭)4>wGJ-u 1N8D{(q]dCTv\ 1Y\G0p2TrثV1P6,~LD ˈb +/vU8:]K^7 *ww!=0 -Ů)BavyAP};<1+64YF,bDndP}hȇV>?SI6쾎RR*=5ȭh54 [aË5Z}<\_ɬ<0 -: ?=3YU㋿l2!a7̱7tȘkcZ1$W&|N$X[Rr^gLs -}ˣUDHTnLlԆd -[.5q5"/ HZfmxW׎YgYbA)mlQ+E op%Nw -?q7䔾gTTP^Xv` \0$9?(؅?XGaWBz~utO4 ¯E5Pf~i =dmzoTf|xaK[YMXy|AbCH NFZ u%ϟto 1ٛojMn|7A_]IS'#0~aV+>ܓcne,5.ٿC: cy -PJ-pAU#B -n. KNYF#>nD5¯7'[Ls*NcHE g?CnvcDC)ۛ#؍|8!1+܌iЍx}2TfxX[ eΉK' فEVU000HC˝|#@Jnp%%m>Iwl{@]Tߎ)vBpZcr]Y}Qt)f4}-LfH"JC@ q=˪-#?-4f[Tz>Yyf}퍛:H)K;eT7G|r|}v Bu?4=2 +ƌxUbO }h΅Q7|tRYvdnTtrKSrN 2R͸HZb:#Fg\\/H}|Z!7D;=shda:;:2$e' jx]zeĭCGڴ:\۱Kg^7OnAH176FjY^V ̓jпT4m[] wP)7V2Cˣ#dGW~M{ZzMX+4 _wΞ={dwf60f4NL4$+ Ui *M " -*QK,1ٝc?_>u^H,NU7 -{j~Omt^6u}muC?zs# - wħ4(9#]fUre=t2tP%7j] "/5@/onxR{S\'T9fEX EPgt: ?MPs'db2,BlhZ@_KPoh*!G n16g%;ƧmQc_+ _qKAF@M -*oe} vr{&v5TEFov2bZFmάA^*jnk2&̑oOw _}Tr v̶5M5a/$~?pI9Bd&g˶g+A=/w]o/Ŵu;?/1ʲҡ!-| lzЫ$yqq+u= yQ{ {x^ӳ !3^mkPȆ,>%ÿdgBG'c# s-L8 ^x=GiR.| -5SܸEDV-s{ -ƽiV4vwF  1 =b -⍭7+Fк7˸r41Y2 -=?G/H"39;Kw`>;/y?v-D!v/% ;[^jٞ$.0WO%1PDhư.a6:2NHxBO.KX@f׸}ALjVЕvP8Лq>&?++%qm_1R6lCfL/J] .mbcMRA{ȫ䴜gidSJ"@9LV Ö] kkQ1 T#*}FXc=Rћx%4**HXȣ-ٿk#CT34=G;\Qc..w_dֱѰQŌbBi,*BRyzMZ *m.&z&!=,%. -;\S6&!>hFZ}bS3wvW90,\X^5e!ѝ!.y-/qkbH|fdQ $=@v&\(h=Pyd'.ϣ17_|zTeuPD ;Өk:y!UGaž҄]LOET:ZRqLG8(Iq=I%ȊOXi#+g b:JOU;Ӟ?n oHG9.;?Mg:ΤN/)NK}’ZVvK0}E+BHHBH}btFy8眙sp9  -"~ u9~!Z*U3!\br/(%rg鴾_\~HϮU|*u-eM\\t~F+`d{Y gB#I3`laR&|RT?/F"rROo#~GhE[5)?QA!]臰L1OH]$߶IħM`.{ ǰDn~6܈``i[%QyBZUh,y fm"b,ė=rA#&M]h n/ -7g9EeW'gXׂ uyb}ނ693kIe-J_ -*@/(Fz^L8~2xrll}[J9#E@9\L둾 /蛎'd;i6)7gPS@b Z⥄x).ۥކǔS/Me[-d1ȟE7YFyU7ܔ΢:z̻%P ]<(ڸt'=F Jln匪{RuwsT3 -*^EyZnmY%;<9 -"윍[6֕ʚSqPț{ _cO^+_䫟po%ok)Z9-:J1szK a֬mϑ/2Q.!.W~_~ؒ:2Xe]NZ䌝Z"}ZONʽG@W[.̻KɧAdcrfFW\%u;{>0xjC୐DZ1<|'i0m?m]BmÃTvMԆVm y3>g5WžS8R>yCRYq˹4_|2HsF?oy0=ic 952k7얌۽r?l:P+nt,ozs&Ъ)|$cXMY%$CIۑy5?%Rn2l6H6p[5oFc 4\N ׼JE1Ώk9m%ݴ9-i, -i$Jy%.5e;s =B5xyLbx)Ãe'w@ daU=g\IGS(޿_;_ҍYS˄;TuJ9Iȫ^'W]f+/z>k/y-V"`ҕKHiE~Z[bTlRѷ=_o(~8}+SMr?##/L ž2M)R>zK# U d()F%/uJkn  =lG@'^R~_@!cmEzCåL;oVfg)$bBR-^<2;pba|{G~`M} }J7Ya5 @JCL{K %jeQ\0AJڙl;`N^;{Y`h?oS/YKFNWFm-2w$ -膄D52b`NN rOdҚR!|_Jerf\Ǩ 6a {/=B8UۚG貜ZE\@E˖5A%d|=󲀒2w²҆_d -zɿ6FU|lZ:Kx[BK9D1<~t?lcx]6%V7E ˇ`ÎWԢ-:jI,G `tڴUz _viim/$!N pqgQA4IOI?e1& - ̚,&ju|2ȫӪд$V`8W.J¡46 S1 lvz6wNUo>oĸqkٯ}EҾ=sc-YxU}xv+eqsn FQhR|7fPbFMN =I͓W7gx Ɠ1MHښ jL9ؘEДIQW5˪ȯr~2 ;Z9ͰߔvA=T>00Я=+ -j9#rM vm||/n VZ]. ".;Ѩ/dIʇ==p(h -jQ/)=>\lAw{`4:k.l @2vX{goU﫦  8-P n@{n>>1ZdTzL(>j.YLty"v.`]ܕvIȡ^T$ ƽ%9 ;^d=|\b[X7e%TE1.Q=sn)pCL #IKʦCZ ^{g㠓>Q֥ɹ:iV=mH9ĘE EXĢZX5KϰgO:{̄_X5Ħ6!9b܉H"e3 `Iut('''S4w -endstream endobj 22 0 obj <>stream -HgS[ff%oLv370K3)Bh@%[vm\S$QA !l*GGQdg?~;{?VD{9S`ir[/9bf#&@ q#9i 2O sns{12v^'jQ7(r|n]d{t6 䋼oD8}UF˸F9H-ȻNHqp MRo)q)L-I-c,[v5%$j~y.jon%u !i$q)s.9-igvfžK2T n9\ 4{tVyKi0(]y"10m2k|v7c` WܐRI鈓ߓP - }d{,@.C@.BjgtM<9kPJkUۅVèU:[`ݏYFI^fԛt_(꿊-q󠒙#܇UZm 3r.))ib0}縁p-!&=SQNeSͿ+\8Ic]J}5 :vWgt$Qq.q׬. =ΣBr*FAk0qlӉ=3g MSqWr^T - -ώE-)mH -rҪ PO$mM:X -MΖCTxJz*:e#v*P*M97 q)hd[td莭Z+ObNQcwClZ{Bl@wCYL`M叜W;Rؘ8u -KMhomWv kht9Wqu;_&L]u!Bxk3Lwa(5flL1}#ʲOI:D8=]W'RiH>V -^p)"@a_& }12NABrR)J^9u:c+q3eW-)zⒷ§K#UNo~حiJX]_?gyZ_}w]UZQ>BO{i=9g5](nyXd¦vNLN 1wz&jK0|7n ݉W( 1P͂#gD^ -Q#a{uR[<8G٘#:nݢf}G_A6P1&)noQxˮ.F*{Lrm^ " -5<8f`/e0R'i5}3-\ǾEECYVyeX6f!'[=ёVth#ax~/j ݈ĭ\ɄӜW!@=Bbާr2l$^ۨMuҽ*n?[ [X`n%K:kӭ_$mSqN@};6v1-ØٕoeѧDlQ=zHNIoEVo<2G*npK 춣zY,{d5).&?eTz`N{9ǐ؊hA($d!뽹 EpiG=TEI’}ONKBO?xy==QuHX}^{Q;RWtڰY_6؞wa="Lk=uC9wՔ)s(DqEyg7"gm{3 -kgˈ0e3Ҙ[XuHiY fB|i iSvFsvU nRq q"lH~0Kpe}(aKq+.뀞 K2KƄA% -'hUү;COǟTHŭ" x#~LO^$ݓ[F!5rڑzeIyH.9^ݎXuip{I'gUGt -3RRYdDP+8\B*i/I^f =}5>ʻ!J8t̪ưt{kPvR+jϚ@LLDr/{[3䊔 mETGK̊QB:\;lȗB̚OʯC/ĉ =sIql zX7Mٙ*0+1rk"N)$i)ĀVp^q[2>_oǢvXo$\r! po૒&Ma<1{e[lp -v* >D&Nl_RfV˯U! -~ܪ1O2pEݒgkR Vؒ0KC;9eoT %:ƤyHjU k|ěI]/?(w')k=:D/>J1 d雍 UF 6)2/;Vg;wv["k孼o"䝐 vvv oL C^B^7/ўuN|2{n9sK-rM -asyac7+8~>{>zY=xoBc!j ;`D};ͼfI XV|6IwL2CqMǽig9MpګEe\MX-# }&Y}L]zh2itY ~AHѵ[ƃ5=Ug9^`|̢VPߌaAE\4hϾvhmҮIҶkprY,H*0ͯ[geIj\o4JsgW8iw*>zɋ4¹C죁sEPUF/bm+Qz]?W&y r?>/}&)<X$юX_r_q?U霨:2Ppsy2Yy ʱު$B'3ZV%/b[6P|a=ʸ\M*6)}F.ǘVNK+'DBkP3|oA(Z0Kf~@~tv+瓈Eu̱_S΃yFoW[•!z(glc,6_KsKCm0_%MAJYmz|0VU=JX{O%znHG5/z9ؒx'}Qk?ݵcJ\.Qpڷo`δ>FfM΀nv\x8u]l6zoCkļ@GuEdfO{Ogkib/ Ȋ>kZUfIWsR|kxjmrZq12{hgV)3*y&p)j_/v#>mڃ\wF8%`Y17bɠ%sw)':A.߭ ֦߬͜Y0N $H6l @"D` $H6l @"D` $H6l @"D` $H6l @"D` $H6l @"D` $H6l @"D` $H6ڶ?ʰ_&%y1%1JbBRvaZ+TQDbPBٸ8Cs| -2cөg*{0qj8{!IL>#v\"bWPĠht|?~ϧt0X<8v"1NLHdm6#1AkATZ &H)X)LNcCUb© RY,JM;~m}]# Db  [12j}ߕaR[X57c╎ݹgI02BbSw3 RF7)!R䳊)cy+WI[̬AYN*f*&s^Zzo^PcrZ`BX4(!ִŻuIZNYk *CSJ[/LI 4Le:u1iֈ%hjA>TpHh>3-LuzAwJXt,=ru!5dVZeC)̨&kC"Y-&ls[cAB / _weĕ? jn&SYB&0`llް-!˒ekkeI1vZ{SK1xޛ%S39ũ{=wkYK{rq-kt}.}`Q>cUҘL"ʂw^:0[2g=ڦ?9ӘmjA^9⡢3$ɯ)v*2iȨo2nK!jYP6,iZ˘C=PXz`AlUmŰ5fm($4N#P>2r~&mL|co!jh]:es_;x&"k`..-7gYSIh~uӎ{J'U&eaF=zO,51[C%!*0H\,:^?l!s)jVL>BrKc=R?v\ tjpД[^ڟyHkj]Q-n9^8k-KO1Qe U5,5ӨSE"v9 {Mpop@.-`gCS|6xAA尹Z3qEġ!7 q [I/$uTDcġR.}s^%B -Qiioc3^uPE%ٛn=|onW\몢IwSIE O(H.ҋ|c1Ω$ Ӻ~0 TKүɘ]Qæ~0)v+f*ToN<uWvmmcjzj&n?,bNm -ImW Jjڮ3TrD0A{̵eKش,VaV\vY Hcl -dˢO?_-4iaؙwlOq֏~#ѩҲ'q0)(;4`w:sn3EB>ro.MvT{K-u<*F᫙ .FlbG5C%ة[ԂfQ 5 hEz~{fJ wW~}7]Sw@exhVUFՌB9 _ -$ǮrIB'L4 `ᷦx}F*i3A(L- 6ŘK[r GĬ߇?K?h9N]~wdyާ/;DmS~Fs^Ӵ M?_g}}$>&\ -pX'sEY k"h$΁~,@y7N5P 7,߲H:$m#us^iU?#!!O -ŠF877'L!%$/KM^=Lp .1~wM~zdd -v=>3-ȞE$, -E% Ku *H&:9\ =򏝹IT#&*CQAr8HfFx5߸JʘSǤ4.e!y5(LytEûܮ7/-: -5 IǸ,&c`/*.3&|y#F@"IݵW?xἹk{ZUnQuGBY"}*>dP=J*dIYGv+z?-Z$gŬńz1<& CuS^{=fw^s䗠dOE &at4bS>eCδc]j/i.ͫJK%CӒ@>'&@{Y\5шE7| -8 ڏA_o$h+Fd 嘪L p{9 ¶eswnr-~s0slnV~[vI?y<_0>3\*Z^>8|"}r^ ;+sݫ}rZ܍ӹ\XQN@Bcd*1Kt :Ʀ  Y>ZC]eeTz===}szz:֎ǙRO, "B}!I*-"lIHn,Q@n$_^7s<~|{V,5}QOֶXٕEx Zɂ3NU76H0 !C.YCJz4Ɛ0\LAN ZUŘR tD?)F'!%pS6Q}Ze!Q"Tvq#Lvʀo4}[/{R-g&U!o>!<#foP|:q֭'dt` -tT֛vq-V򒈴ms^pukARMEbD! -| N݅v(V\ΐ\h"xg*< -g|spwB$AE,ӇKu`uiS.FW)]5aqh0'F1fWgmˬKeUkʩm/ jBO|IBnФ[OYmu..i"\˜ vYvMyi1iԢ뒆aނzM%m_}]4q5#/,Opv߷^3DgKf5鹿s1"8;ZuY;V%sww8QP poJtu"ƭښc]x7V;or*㐷cgz"2>)&PTStY 9 +jNКkv)xVX(Gʮ(yK!tX-\ -gs=HHڏc:u!7̽5w~8&aH%t, - λ9 t<Ʀ Od@)uI[ -kԆ^Y2i+ƍ#tT-{ԦvuY?t:eLBJ=,<Axi=t.Q06 & Y6xMڤC |<:9h_hI!J}C !IDCy%m)[O\߷Odn返^5Cl]QMo7gAxgIѼtťu1+Tݗ^;oW6ݔ<woJ'/juvЗ|/m@Ԋ], S6nm!ĨM\]q wMZ9Yev?#n8*!%G YEuMtUX{8i_Kϥm0vi ʯy&>v`W}oCˏ+̇[SU -,<|x¤UTZTci`OnS!תH̃>ɀ (pM Eic>dĂrvί`|nVq 2谢>Iމi kT!6ѭSҔY9t`M-Q8\t40[>bLt¬ o'g$tb`m:GFx5)'.[Q(,;LEdmW)i ҫHM&:f4`FD#z:bP!p9 dՁaq18CXna ^'H_vF| Z ɯqZp݅6АͰkÄ2\ʨ"YoCR<@Dv&A OBoW*Q8sdI3c2|BAԽ4gRV[BF8ref=7sI$6Ҕ~ڤsF=F>M)pDq X!&;C`*ʄZ+cc ]llfzі*\K~syOf&mz(r$MpxKîd,6G ȭUbih&`>GrHu쨓v@9gC:^򷼲HhEૂ{rէbL{wLg|uC}G/1/荽50{:x֓ʾv:H!3z{^ -(;v_J?{.KًJJ>mw.';/&=&qW^c -/_䟋O<s5`l`"؝WV\CK\dm&Q}^յJDMd_3eTr uߡTٯGl[} vů^@&Ղ[o<^8 [4)!LFLi.k6F5ybs\-49%/z[GLJ51YfsM[FjڂӱUbjXrJ*2Ⱥ {LN͉͌@ oSWNV~S%ȏ_U#;&5M!eG0jBF߬rE>mP0IKkAF' coAg nM :4]Av,o+5}2)795&M\an=j Y8MquWB-c6i3k$,>t饣5.3XM Co ;OMN\xg5bC-^&K?WF)Mj&eiFFG v<Ƨh{V]f˜Gq1 d~{ 7`vWħذhg Zʆ14].: e#35pۚfV50IA?/k%OGF0e=ɫť՛~Yڧ梸N@L;_~P]9QNo<ΚL|JĵZH/$N7` ̴xqSp tscY=Սy1 aJԂԢv]u5Wi =c߶-|ք!ZdL iT{bֈ۰RXYXFp_(`u(onkB? -i3ڈ|AGyĝFj&\nsIș-bpJ NG~PH-7VsW&I~vg@}Q\&Kky⁧D.>'!癸tNw&tQBGf}r1Q#:8ŐV%ꯗanX҆ -pW0sSL YzעdhgfG- +Lc;O>6 7Σ]*f< [CV\zޛT<yxfԤ9wW{O8Qы;| KC*}1|U\P])LxfR|Hl=(k$nL-|:{$mF>b }_\sˮB&Ί.k0 rjA^Nt̞V8|v:gJoNpʙ)S6guB!!ձ]e; -&p]cىC Y K\ht%}{A6`a6<9F#_՟wO'bê.r$"$UۮZlQJ4QŅN]5i*a.ƶ,aP! ZmU[fkMZG^`>c0RRR S*!Rt}f.JID:&j"ݙVsX jEl(FՂ5}lȈqQ,RF5T"*(oP\s1Xi򨔴䀕[|&uk;KNG3uTe8J }!Q9- ->W=\uG덲TNPiƏ='鐢7VtSaG1Ԋ ([ިf{5U5H!1Eh\VʧzS']_66cV6snͭ'AÇqI&l2Q!@@zẬi@rhT׎4VIV"i>8ںy&4s٦?1iKukt)Fj_={7O%fWl?WGzi7_4!hòlۉrPuYR1ń=kK -~0M4LblgOƋ - ๎Jbe1>#Ÿ~-K1/Ga# -h7.alG$nc0fSaT,i؜/cR z?B*Vjl_ۃ,1eP|X- >Fyy*퀾ٰ?e3ĝń\#4>91a1a&[9GK9oNNXn3)N'Ÿ}o_."zs6b1\ౢ$7˛cxd]k#W Ai{)Ad]e"V 9 hGy8˽Wّy96xWBrseSѡ/I|__wY̊f)ATyYZ5ewr'nS+7x q7S?+qt: m.SV6 6ʻvtN;yg_P!FҾC%!EgƯLs!$E?,]3Kxj-5"(Ϯ{5UOΥy]zYѩW)N\9R{p{5zʭa":4Ӊ~\I -bXWW'<#e"܆ZD~&HÐ×^/l=YILӋLW٪*RtgݲfګcZ!vIDCmxVRJd6ƭ-n<8ܾO~'kmy'7ߎ q0ۍf*Jr!%%\;\tf&Kaظq#rU_RyU"8\:۴61_"1 6¡wsJ!j{"m`I-?EABq&M[1!Ì_1,dT|.T{]-č֌W7kbߎ~<ETy -2!ipy20Ocz]zvgN=SM;97y@޿]̬[mho[E`@(y1lԁ2.'吷f\%X4TLK?ov{ N`0`rl(M8ަ\-;WVO3–ߤ l4?e}3Lg$di'g1ƱlcK:VVJd6C(L 0ƗniW0ؒ%hj&~}ӭ8Y- ,XOɛ3.}A3P#o(í"jsZ\}QcXq/%To.UBs!ҦwE_U%/d7EhʸG,{tR;8@oH7pi|Cumds#￰ 6Pk$M7kk{ϯa#j> ,¦QD9ݜEբ,.p$=o!W~>~4\֞{6vl v<{؈q`s~5quo~YImVH!\8mJv+n/XqzY;kӶN4j؄]NGB -YL,a3!' 2&C4t0L4S2|:qeFC-xlY/ݲmjԏ7ݢqW@z(܌R/uK$f}3 )0Dҡ0v8f[6(F4W\4$R)!ksT$lƦI!c1ޖͤyLz6];>Lk7fde]"c&2I6!?Uې׿>1v0">/Զfj R;bp߯QLJbq-LJ[SQ`=Ԉ{\-p7(:{e;؜;`)=tK&xLNq tꀉS(B'M#\ơm<.FHeFŒEŗ _/ntބ~x E9)[ B֬he_~=Y0imYrwʡGR9D(ڸEcM0۩'@vKv9~\ ]*ȃ 9{%n0g+AGʦsV@[Nv}+b ×ʿ$AtL?l1!CY_Kfؘ;٦tc=p1/.K"IkB6n9.4;!d:L3i4uֲ]Gv-봭[$=X`qHƶ,]'NTXV%[7. @r*@.{//}yk١Շ~GJ̍%ef9qزI) ㅺx쓱uГM.նS>!=sEHdzrv;/Gewm?w$u6q$j462('sj"X7b3݇k |X!:ګcVia)褿f۶hx\J-sn$1s ̴ʈI - Je"OB5kysE)wW?[ŇO;@r34in_؅'<x=פQ PG23{mjfpD/ EL5]mdMB`"].xkM-k[mk?(T#KE S\|+ž65gY?W]$6ݯ_}&zJEe7H ߀{,ai$2JI:ޯ&z -qSB5azYD/x WY(wex1=Oݝ=$.3!^ .8|7%-yz6L!$b:,$}ܶ邜S|?Ow(MD34̢Z>Qy -޳bmg" f:0uY h^tTbG1 -=$!C93经|G?Փ2. -!eƆ蠉:d=X%,Gm_t9|tՏ*&&otϸ81eㄭ89L;^nAm[G,:%ӄ]/vl/Bnl#T-GqlRj!FUzjPac}/KzO;MoJԂ l uz)&Wx6p 2')qk=za8ۆLm 2ʧ}5Н'8ԗj6]( C^{qE4 /=>Q!XVH6'1>CZ1 lRN#Cbcw6n؁m1,јI;̐9re7ժxr泸FD^Et]@4nz6yeyw+#~.q4fc(iz3M}!ƫ|rP~DFpXz`| $86f?æ"gOn2mRGʒ69ŵpmDb RB*U!9>[9c%~S)b[l7ef]!^a=ԡ*Y[e*Q=]3hU}ߒZ&)!O؏,A mn Ʌ2|4`r!P!w5icB|&}&wa;(Gknil={?Qny! \$d - Oa tInnOmOv5S7^/@B-R!ѢH9Z 0ֲh) tPmQXt\}6記0_0=ݘhъ'L_XdmS\}03]xnjC=2 JcUIJpn}'[Z]iun\2_>ݱ\=\7nx'Ư-m[ZYJSuL/ QJ[oq?5ar*wEwDFN rtҹ!:L 7I}I䆄$餓Njf~]}lWbƃcU3Pn4= -uP6EKG.`jN|.| L z.5Aoq$ښY| 9b 8K|UR6 @SXIe!/x.l2ˠ{8kTI ʮ&GG '|+,:E7rFH= -^3Z1GE=1L,`U9ϭG9#?DdpwղO0r+\o/fg7k࠷0^Ask6 01DCVh=vc9j X;dsaBĮ Ok$znϬ_gBZ RSIcg'Y'1xG!Y  ->|-?˅ǟ1Q#4(a.Q6VSC $lPvؒ{8[ؾߓu NƏeHew:wfuFF%ǟASጙ hWXB;1lH˴UD[Z -ŖdeS3q"͜Oz4,䇤kdx3!:9vj)YqewT&iޔ"1FtIkԢiݛSI$j!i9b^6z~pwƭl]n!)Y>-n[U yIhuC] 3R_\K%3^tMԠ+ Rn@܂7`Wクlrhͺ mc.8?}Y3] -e\a=?'kcO_B]VDfeuGOUaM-ʮ('R3#Yrؘ d0- z̒z)h9&hc֣S을;4-m̬ yӐNkz3^ XۨfULMP!dTe|zaҩq/_/FO((W55uc-qTִhꑃuc8ؓ!Eŏ@jO kp73!eOngV̕g!I?x^{n]׵O-%G]^x sc2cO3_6O̳~<5J)'~pG)aI,24ia)IH#b} -mUegwJKbXޤ$(+O5iy=?>}7}6aW$b.2eFjX= uM9p'? euZ3!)U8W7L/1ʹmڟ\ޝ$lM)c\X"V}8W^q<˺ kNDiKߥBEzIt5B%/TlM':$_;OZ#,d2ESƾؗqEEM[Y'eÙ r/zU碬_c:0A)TWL֫QѯرM{ja iA8Gsp1n RV,r` Ӣ3ǐK(%aNMR'9B9Df,G Cr~weESj]=3Uu2%coe0]Od86yM.3v/qi9HP A )bNkL8p>UU:)/WuƝ򛻋k}r+ŭjelX-Q8)KمXQnorR>Y& 8i75+>eSE a\ 14T a~Fkcca띦6l.{TIR];w!n_Y׋#;9rTwF#_+ х GSX{umdF߲NUzw&$rpښR:s4`Z}^AXr0)-=+ӊV|s[Y7YŸZxnMUp_~g{_[c37)ݫ\l1 Ms#.)2OsJkO5 +ˍ@i!9& ;輸 SMYG9X%h{8L8SF>5.ϲ]ᕃ~6EVA]aDž?ν.,37N] |Zax0Vd8Jq.uZ*NV;e8K\21~"!y#YRqi[`@\3. rVaWj_($.f /#nNhץ&P!l6.m]vslQwRd5~K=Hc֦:^gH2IfQ}O%O!bE2D7 -x_GMc C5ovY$:dS.вY(-ʏm5:d3-ywʆ^0Fn#,#>:=*. -Qf!h `'aQCdzN+ZF#k0ЍW?^{SkcwFHBvqCR[M"og2*>4[r8RzgcTUO]F24 -;(ڛ3L2Dzo~s ^])8MKpA;@9RA?u! bb2n觨3UozO.|>$=uXz=yé0MC.eSEﲈKPǢꞂw>E>ȇ"_U!Umy触S R*MAd1lpK[j<,^? ,-.2 Ktv[i n33Gkloo*ݑvܲK'|5*}uqZ -v-:$ "-2wg]S-H;}@&*:VA!n?…oq`koa~H[ub+*b&1ao?9P>Y5&kz.(l,:Q{g6nKW=.",E;!,Bb<2䛒^rAuJv[OõCjN=*»g?~4\d}ƴL(*Ir)h [B3BQn~۞w#w|UQc%Z' \I ჿqߴϲ^wrA۝HOMK Q!XRQuϮddJ |\]]ZsCUm$3y^{{+z y=ZjD4Ѿ2r$/[/is>>LX#MtpwRyo-|([(zK 0KI6 2V/6Pv~#bQ!}nJO2mcTUApA9 [/c?70PhJ;$iis߲wuoncfLd;V1}5g^7t?#|e-i 끳bN' Y^ j>* -w#ݗsVZ A5'Uhl?JRuz>˄ԊĿTUAmħ& *&-_>o3%%XNYeƁb r&V-Ć$؏"G8_ {ݤͼ_Z_R'͢D;8ViC{ެr1JnWM{>VGE_/4Q 2f A*nIB ]j'+|o_/|spXi;i[nw.staҋfJ #-acٛ_[s-p4Dׁ^uerXk)cA;.6^ )AD`'>\weԐ[rLR!|Sx `6" xUAxe]%Bzɪr!,!2Fyz<ƈ*1J2QAs\ip c:b]/Zg{2@>|N)'X2I/%m##93ncuN8|5EfhEoKX~%=:LXH>/A3'S?6fx0T`* -*(28#"#ٔn217ʡsįRp=^GXAݬV*R?Q8g{#t;ѻ.+~rqD?o |[ϗl+hTf;{)U/F>8>TD'z)'lh !jڰ`t0}l`~#gcbm &yn*R^H;#Emu!kdGzEI=Jz}dm)㠏r>4!ܭrZD]343;uj9jajco2:`؄Ϳ<4JJՌ.ϯ5 -5kܙ13Nb,IbLL$ K,AEAEiREPCh"PƆ 2}8_:{w?𧜢.|eaae: -s -ëfzT{U .oZ)iI㉩kqČq68 ܦhշ!9Ŗ?a!rVqݚ{} _H7#Ѳ]@;C>ʽ{|cUuPCGyf>㻹ꚧlvv+"G?ބn}$Ũf{2BE>W>o3o[%q571 -VOG48e5Pros~\=KN%N v:9TF5/ݙ%;Gv"mS/Jæ(_6;P*LF1ߩ)z*b~gMXbL4a#u`p'Yg yvsP3sG=4D Rᇃ,=H3uę mE`LKt}.En'{avQgjӂK^LjǞLTDmb2^im -t*q(Iqs Lͽwư`Ys~Fwe5TBtlrk ]':JG<|8("`gg2|"O;ØX&8R9!b]Dk ƳHȿZa^1<ǁ8ÿZb]*ZMBL\nJnJB4p&snC'ʘ3-#(C[A.'#;_/OeVI42'gv|k[zTNNr/t:>)'GJ.黉G-sPy۝1frOW/b84GELvȫ/W1iZ\;#i%qӇAۃUO' \M/&vN6!mv*PjN*DF~( sH`vWD"}49E𩸕CK -"${𓱽UJ{uh8+-x"u(YUME{#H '䄃AB̅KE~呎9fն (Y?ސ hܯoe}FqIe1Pq e -صxv4Cl Zf+czJWNYS[Ci[.wշhpsiK%1ŻK?-˕$COq<!DJ7깊_ysooPr;piAZqm}{pUX&ߴL  ~(n'\EC=+F? (=TxSCe~u-1|0Z^zEYszh(h_-p/qa_s@E) -.o`]jFmfSǑh1=g0j%,5׭iwj W}idc̍: 2+/ >D_^5pffPbx;\Yֹ_=C_M]Ws#r![b|OIPz*?@v{oۡkDZf >q驴,ƿJ}6](gnE-B%g~J;];sݽlfw2$hlLւDA)"K"(&  -(6DHi3<)`V{_OA7 >*<ؘ/T'5aU@uwBFM䚄YJI~ r@+ȮeT\~/='jqk*^aKѕSqj0:{PBLAr>YS#t<.c^5򄉋hՎ[ȼVS@iuq6؀+Kl N&+sG֞X79_wk_u9LC  -U=psW֚uZVs$!G]2?8tV&͸C%Zs/藈WzŬL (iR+PU@\q]`"%)#&m6QoZF= 5yK*$u *H9Vrݣ\2D ?K(+u)]QPC(kO,ڈ_}Ƨg&=ם8$!vQImPOmx}]E)x" z'mؙ#o<@?JICHЩe.*һsл -vdِٜJqԈ -,{䐜{/[Lo?2`[E1Σ]Yxު|]4+o?4  -uY#(e?؅m-$܎;2AyFbvʄ7y/{̖PՒ*/a%!OU뻪BFteDh9P=^DoNCVBUXG9 Vк3/P2s#g;yK4lvevC\ɮ燋=5*?l{X~;Sh)vW%<O5u%'m -+oF֞"RNJ?H9֋:m[W "x&"?@nH^ˡ:vtos0|7~OIQALp1ّIQ[t1R!-:s3& ƈ^E߻1Kr _5Zm5궐V_w0>T!È˓N!QHi?[ǵn2W -nKڞ^ VFu - ^cwV$-qK~`ҫC}5 -4:0^`h;E-[óV[#1\$kCST̎@p`p>d$5{TJη$AVB$b  !vžf'I AіbjmZ:մܹO߷gy<{ZR>Z|vljI0%F\ y< K9Xjy40/;ڒ҈[RƮ֡,u\nI~4,*<Yr=EnRW8CV2f5<骹 -97S]$B3Tc3!-<hD`< ؓ -,O" H@j."Z?  ڤ?nLY2: -g*/g^A2  -Qa xDCP+3]O7u;C̍1s]詮wQRR ?*Eph//He1 N @t"u!NGCS Į97JϖWoi>[U US $"dQ ϛTpDEt/;n)_?rN'i}ii}6k] W7w`)(0ia߇a4O \2|~!tXl6ݚ*|!jk \pr5nŪo"4WlLcZ2!+ <$"pD  -8}OGpx Vߵ:SK}K7r\wӉZM;DDJ2 L!AP\]_$_ƒLX[̫S?mX/74K'9s=}X^LLPp(Kd |xX,0~dOc ByFroި,lsͿq:xaŒ`]1"g/ -h}9Q#@DQ!H+SHC$!BHiOu;ۜa)d<7 D(]*kbFNQD #aBFX06I'veO곕k FF%ؐ+vPQ-YbTqr> @^jhޜL 2xtG]S@_1Wwu#mQCDoݞfƄNej99I.E铃xf'$9:OD?6N6Wigf{ V, JPSps-~XZr+ -C ӨAT:x{EA ,Eƪc{m؇վʝ=?D۴ :ρHy| ;#R@v#@pf*dUt]z-|xZ_u /etFU m5L6%5dD72D1Ed"Ä́RPS뜳nEԉb}=QNMcSUYQ^px0=f5 X;]n@UJoEeSk,5 rJ-{A@6%w1BG{@\h ux _5 ->'+}|!bοV9pF@4wrטHPSy }%7*-Y4M t&n ~ײ0W}d>iZU<3E䴨)+nD.LkwhA=M'R,3UQyPTGbCfyB_7}˨bocx;K -*&ن"cNx尢X\"-`L.SyRZXXD{)%t]GwFo {bֶM\7ʩKmZl|ت -i^TS+k y<_EDO|=@$yҊ?uۄ˽-&=;.","ʟ6HDȚaj¤|?"w$?wGĿq/I/vG#_ay])Ǔb8}b/B9tF*ܝNTBjcȧ2+1"6 9){ZrxJZP0-m-#ٛ7O뿍!d @-UY,p^!Q@qj"^7m} 쫀wf*X&8PS}S -?۷>bߴīFqdLO7tI<Ru|u`7YxvŌ|%5.otƚ{ĒkԫJo#<*+yHOGCˣM_[,&$~8VN {rɆM4wRMۤwW3_Ĺ!ʴ,}VGvBg@0)ΝPt #}qH1gN.+OmooqeTRY3JA}TՁ𶾨#gvc<T{mN -)V>ecWe6t7'c,X5\H/Y­ Bu[r/ dZ>1?qN!ىE;7 )7-BweP8>ѻ3էv~(F^&3B^bIɆ^E=d6tj `Wr1+:\UӜ}n`WtOnyW^f49f2k\&aʷcjJ2&j72ƾol^l(?+ H[31 -ҩqjq旎NML -B-WbJI]Rb c^,9PͶuΪݿbfwZ7Q,g.FN!iV~N.ϏTQpo?_NŪrt^ͷrf8m;j@˝Eϴ!YҨV6gtx; R?)sZ&ĤB?KB?|ɥ!f멈-Xk2My-%0- ]-0-bLh;c]ܩQ5sתN -6ٓ\-=:"(?c6U7+aiL`wr'4 -f#=iqi9kQGM]$>[+Z>]9)8l,JL,I97ScjB퍑w#̪QqI^Iα˹sPT;#Xe.=(9{Xg_sbC&8˩icv;shk/VvƷYdd6sB -5Y] hڱ{f1ZП8'60n:4QqpOFP&LEt~yĴBD5x>",b\FԚiI(Ooʉq!xEmC37K e? g>V깜jt_9,gas*!wyl]B|%rczݜM/GwG͉OJ9e[%\u_?4ǑLکԺcl{~=5@fN_)/zX&.E6nŖ:n<_PA7M ~B}]vJN@]5`t/Ed⌋JYm)չo8ܾL.C-ș#w3F.tm HK%J9;%Rw =fr0hȭn-7gVr jA[0U?yYjड+~%zWrVJnv*E;{j LϪ!Uɾd̹E4![ QA3^/])gƱ'[G<zR." -%2c,T:s^60r6eZY=C4VIհL'a/)h)9jD*|\~>|IߣBVꋭ~x?\ O;r-^ի=/b7KA:TVC8Tc [ -vfG<6[ev~J'j}-e=\y \)2zzE  r\}= ||rs-d%u\j5<Hڑ7QY =El9Y!0U?њ>F/|uz2|uUvWI#^xͷ)EH H"\94P -O-ketq_Rw2&BE&&FW[;Jy02#?I} ?ZP983#c87QO7q#w $lqQh`pSU̞[tqѧFrաz2r~ӛZT0N4\RdgKkgkK\ZrHH9p5 ;_t˸I_Exv'kD6t;@; _fL'i#ṕubDhOfބJEL:Fc[]]&< _:`)g60KC6j{I wmwW}vI$ 2h\M)$ȣji.@9/9M5"(Y*׼Nʛ&^`nFfZhag U}k0cbvBRUTHu }ʜwqƒ2اpEٌDщ]vw5<"2V%ּFu d^Dͬ.JNX!_u&jb3/.hw 3B(0]~oZ]m` -wD>xu%bfMBJL e5Ȩ͐u׸0 ڢj>2jZVK[kRBUjšY bI%E:13;3&ByBg XcۦIBex4fa kHGKܑ/{id&}a=9'!-ɩC$ũdQESxpEU, 2c. -JVk::[/i BDC9AkV.S9G<.%o rv.)'?Z硼 -z'dK2ypDbؼԝ3דY-|U"Mn8 SaYFk..Sag&⊰^Ry%bw̮1 Gj. OGgjZtU4JBvڇ+pKu,Ič-(ag2v3n_:0pE5j~9s*ƄuZАyE1r$MݘS>:n%ʴL_Z`+l{OAy%Wiն 5%r5^ R  V3BJs%5rjѩĖX*}*j,涱22$&AFvwHE- )#b]PjO#_R=xvGؗ1Q#t,w "BjzCp\^c6uV#CƮ69ԐuPl_ElHGi5G^=JJőV{dyIy_cnJBjzss/Nm;u^d} nfc ̖7]z+b[C#qd2O&tTԈy/CܴO\Y8mpN\fDd<~$4߽jZnsBPN CI#g#z6"F솔^HˉKꔆq0y;י똆X,9xo22c{`{U}":VQ5_gqUЇW.Y?ыks_uǐΐ*g65g}Fa?%VFѭ Tݹ -)>-}x5?'.W ]P +52 _^m^?h<Jl{0fck<WW'kxpQ5ݿ0OMƱv&V8dżU<ڗ2Ha_!{pe!cSxo\ {rfneg4="-pP Ĉ-kQlzn o1NR]˸ NLR,>> spyXSy闑KR -ع@-}?H yL6]ΨK;H#{"ӷ6g8,I׀&>'oΊ_GLMYxu^9zH,q<·ƔEFƴoBȭZ7O"x{Jv(l$(§\0n1? - *aLCLL\ `L::"j6;Z"khYxc29]Łr.1pb2 }UIIxg\<ѲQ1C+A3ca_stCk,QRKwf`mOդ(БڵАͿj>I_z'w~eW^F+*F -Y/Arܣڧc 50'!BCp[H?1p9wd{d{"lyV7xU|*2tqSZ~;\{iNtVݶMNv(UdS֒uA3PWދys{Sp ?rqgZ1,vpӃ] sg^}M+|_` [U}~*$s{%{ -.[r/žشc;.:zqNtH{%i[}wawz'JL!_Vؔ}AX@6= 9*F 4l<"ْЫ]r"[XA3\'OfC9XlٝnI3 -K/K -&K/S0.Pq@q 3笴RsA6ơ'FH%~f): 1rqsO+!DtW}Q1GߧMW[_{#=6tl*vޕ1F!?/j=Rr}9Y+`\cꮊ_#z}js]*N(rq yEÝ1`{ee -*߻$]Oȉ`@ ‹ -Jr)zW-N -JY>)4< nI6;Ie$d h\d}r"Q=bQ hR"'%JԘV?=?t[]#):I;ux'zۆ;K;kv4gVQW}v<=pačakK7ZV[A/=%f`'[-Ċ[<~L$孍+}Y>uC^vu{q=LEdW 怎z }nsa5vB}^w-wtpn -N?^0Jk swJޝn.2v bT2J~MtmyM -kqÕוC A "be7TM+u*] aS(>sIK{C%Q*DtƧ&g{o{`*(nQ@C+ (wԘ)`X#ޥeZ.;'F--TŶ$y87@͙9 -b x{qoy !x853㻹KS9dv5NVQJw-~Y^ |_Ϟ֜GтTɥ)ZpR=ArSvt.գf;'+bdh!ԂKʶN9C4"A(K\zx pb{K%dUr=JqO`n+õMBa#U!3)fjk#HW."o4e[\> BYK2/zv!6l_cFf8O&:qff]H-YItEA%cv ס댴rv֩COFMF愴Vm)Ls}e삎^YxߐÒtC{?,h#rdWǃ\o!=8U5"Cxr;u=2ys<^mM_VNI_#v<ʞcYs N OQ`:6d Z? -qE8-q!3v)c l=DUolda^%9Tu#RW|°ߺf m- hN޷ >=hV%vU,TLˀUӷ!v&k[ɿd}-ѷa=.N b7 MY*ԧƤncgY*ZV(Kptg}1rN@Cپ̳қgkn .ly訞 hpKC'?)ؙ#:_YFM9&ek o-y+~lG(,͑Y(kɮZgTmiOg_! 梃6UuӋ܋C5QzrRO -I=+!fߌ[AuE46s[O OiOSn -endstream endobj 23 0 obj <>stream -HSwvgLӤI昍9L8cI?>n*Pz7*֢irbSvng&$nDWg%7,J7ĽKV",0ޝZ:dq@?3qFSPZ}t0-aϑCQpC0B: -دy9$IMwkB֮Z[a|Z5Ef]C;.Ym<@C5N\{@#k# 1ڔ7bw5p=,T8'0IRMv -‹29fvMՙ'F&5jrD8(9k{9_6ȄOa3y2JUF̉E]ezQ]uɫ_^h+[8cM{c >ؒKBLtcMtxB%@FRr;4_P^Gu^9E)u/RFL4;_7K:u j#&khx-3J5~kY|ms/N$Y:nk7Wcw*15I,ڟEd/ д:*gy,ll0P9^cuL|TIJ)\CNn4˰uG#-ΣD -_;SPgrJIrzi@/ ΈQ, JJC CZ -]n`^l :RD p]X3|+Eo[rOM5@[]ӒAALxfBީYBLg3 _2b@*y_娒Klp[nOßJS¥>rOc jJ$ю4J).n_ lҚfASf7"5L,61p>\scg[c.7$7G kɵ-RmDO hպ!xyM'=&Χ&W37;1c#H` \*g%ۓdoj_I(]ݝe:0Chu 4jQ:n灊 8((:׫z`wHh5䘆 *Lvx=\vȯfaΜ3'uT4+ș_C_*%v9u`9S:m&哇=|랄= 0#J^۱VH:u,yNm225%sHmDÏA%m{4:/F5TlQT7_ruy`e1#ݢZ)TwGb&.cHq=xn1?:A@WڏVDȝqN嶤_s-$DNar96$*Nr^]T93Pښq̡U -spxE2~'۹t -1Ø-@+<MYx*ܓP;9Ɓt &ϿNoM9 9 x2b#f.e@$Qt(h4ݐ^,HiDA{n`Rik^Wu&P; ˲>TvBGjvf7H lّQ^m ip5'kSk&?-Ɵ[YSRA\t|\`ИQ,]-+NuAWHʰZXzme֧hE@;ˬ8P*srhTlD>vAQӰ'&#F>rxwmH*nEUd؉o$) R zg Q7Ycjnz}T."jbExW^Ǘ-! -5qƳG {K I 6; ECAY MW O.DϏ,5rAIYG*,:{`4ָ8i/{H{ KZo?gӞN3e214KL'%(QPeA@Qp{D EETAQYI6Jx^7y{{=J+ kM[\ġᢼ@m=`5ipb2LȖ'hhX^Sq8 p^mþjx8<55}aaEh+̸߉鿹牠lև"n1/P > hz2>`wk?θWzsdSȷLrmS `}t{fw)eXΪ*\#]O]H<1c=x()ٕYZzmӌc )>n_CiWc@23ʼn<7h>5*ggj8߬m[B:V7}T jzuk]>F˱ z+y1^^E&]m/~4Z>&=:IB&Я -rW (%w %t -aEM]V¹<^rqDԸt|G>Yh&4N yrm 1*\Sp%;j*`g9W)nݮSf$n|?ۤ,)pKOQtW% -\:'<6[%4_IɚrS >%Z+/Nc*s2a'dgyN|o]$մD|[sx]fZEH.!ӼV"gCr? -ytBuۨwUF[٧T|ΥKr6[MD'PIz-,@RӱʦmAyhˮxcx ͨCz#*TCP'x92ZSMDnBc%HT}v̟Os\MG5u/%Lp*žm,Q\nFOM&-m2wVpⰦhd,C?6z0O;Z"5ͬ2]w2tmd@d| #g81;ӈDuvB'$vLόcM3hXg/6xe|kDG .ɡ|.6T<ݾ}GƩz+dYk%Q*6g`,jSN@{㴒Iwg/9J<&xd.ٿĮݙh,ZplA87pg&uRc'Sq5r{xCՏ5{WIJSLpʉ3҄F@a &%dI9f.Ģ 6~Ye!O~勭SLvy9ld"}|_C4J}9}.94SOE|rv~>6S1)yGP82;,b@+9!a@~X[y*"p25%׆ѹi{HƮۛ- rI1Y -JA@Ez{&{C9xCB˞dT?r'Am&{=Ƈ_/dbLLO.ql!Nǩan"ޝ_C=r>5udknwpYR,e̕VnA, -}JI[SAq}m )hD pXK-ŎilPC;@,uZ߸/2O ^Gy+º=WI{gnp^@ aA5l0Rj6c xOdvȥ:4A*?3QW5n%Dtcu'h iYr *jL(~oɲt:̡Sji9"∌ 2ˌS搊83*bZNsgztcY"-}%tСk0@/]6¥ԬQi; -Tw 9.=ǣK:a '?>86k 77^>6Tkn cn½$g>E\SxLt qls !43I-pD-NH92駋\9 7l`BaS.b/Y:*MCF VooO=$wDfz(E>>k:KÖ0oO[<=43OG.2vEuN6@'lOՀGaGvgVLSлNfOmAXxo倮9_Z?cS*f%w'iY륮`Kg铥΂^LkSxg`C7sG "@K<&dWVO 6JŽLX<6V mFZxZXz(}7Aٔ!ud@lr -xm#07MQ&Zw::H-I -Zn^Ss7%, ЋI@>Ғ'5ŷXN%kj=&'tbw/*`zt"B)Is c.:Rn?#5lSL;.q}uvnv_t"@OTʏFˎ2f44u@2;%?/D-,{ޡd*DKY.5R3 Ĕ0f"Q5BꙨK z.c3HM[˪j_IFi)QNl?):Zdh'3@SdP: -l,[䌝Ifѱ@˧ZSjshdF_( @}R7!+`ۏej@׾b/D%7W%hDOUbĠA$Zt!yk DGA1L-5j^wCgyflKsjaNznc%!kwRcU%Zb7_Mª 8p3 hpM&钢6$'և뭈_c ;ǫy`<zO8~|puKE)ಭci.5#;22:A#ɾ-~* rAGz>u}-} BKILS£mrpR7#_pFͣ2ǙŇ3Bጔkemcf[*5ʧVdZ!fk i(u ~<C3pK7:ЅYAqzAZ4׌EaF3 kF wR -!YH' D<.kgXo?gAc}{ksVUli-~8I΃ԦrnU+IC(UM!1~rZ cSQ{â&/&j8[NMC#G7< @?4/\^Jnksj!9>6zmSnsζU9d$~3" `45QR@axPe>tRp:DHq(9;`#-2R *f1ﴔD_r6n ^e6:ߍIl(L 8b+, -[?J~%$_boq Q>wg -Ju=;jsÕnJuQً S3nYTǭU!!~C ]IOZt94bSS]pGNN-_-aaҮ -!1LԌ?ʹ=tաiU !x"+DԖZ"" b]HDT9oy뾮՝͉L<0+utOeaW`w B 03x[E/&Cc(9=DǙ Ǩ~¢g^%Y,xpb쓍ؼGނg&FݿB"hE<ߐsOU%_4wtZrI ӍV9=Fx=ׅLy%ç|)}kj=I RRn:=-9æFoI> 4÷e -f̮a0?Ɵa۷5dZ}tD%صĜ][Etȝ}>i}2%* p4 ¼tؓOUa-e`>|S 2 Yq1NB̵6tҹQ@?i(IɖcJXw8/`yb_toSF-EFp֒3@y85ϵ@\/17PM,_lݵ|maR5{oM|KIvgOTݲ)eV$n#P%jj3ZcUfEB)8f.@OȱkO1~Yq2H:621ZL{9D2MaZ._'({ceȺx#T_=N/}Ё~҅}f¿u)wr޺:խa@*J vD&^@AT -LbbVE֥_^'$̊Q!ԤZ>b-]M7; -̶ 7¼k4|xڍPM&Q^s_k*>$4 -<֯jB"t9$IADjoV:hXu #/PdL -n.qUBNZb}XYWg[&!Ы'>M0!kݘ)աȰq3 LP@[r` :Uq_ QN%5r+A!՟Z=:&o cw<%cc$c\Y 2*Ƒ 3: ǥV^ZOV Tu3 nᷜ-r\Լ}T4MIPuukψ=:AC-):#,r鍂{{o -nGA7)MwNգj wC3fySN ƺ#gSu>ؚ̲8<]f, -:.]&!@()^"ZB  !@ "0YOps9;::Ҕ4CЪ:|_G*8YїL*~Gy&{sbC~q>"}s~xqo0Оn[(2]m#Htn;_n=f>oĉ .͕PRPe< Cgx-8#G~bb:lO_wլ .j.1w|xvj0j\M*,9 <⶞/iKR񡩣Ͽ(nN˽r9UA -V%Y7w!:Nɣ+JBҕ^L 2"#A -\T+2_v]hA<}tE|yQ3KGCCFIQo^M6ծUElQvI_ë++C/PY5kc *eOyO~|0@L8<|Ǯ@U=9a|u Y'SGyD)w'8קaGI53 Ef$:yg=2EkÄTpFq["@M׶G^m[P WFolI -leiA#hu.q7k3 / SQf*䟘x#S3yc^zdYeq_6l@gv'{1(~t -3afU]{AF:^daO!QE.{qN0*Ʃ@m ' \H ՑYg;m5q~#8+NmS[ gfYV5FΜe85x#1MEm3%D'11c--|} h$5^lVx fl8!{W*vZQf5І2}lL:H_!+}*j $ Dg؊Z4YzGK `徹DɌDD_B`b!$PTbD]CIrm7%%ޜ$|KC/5ʱ} O/3Pm]9;$=Ѝ,>YdiBDEEq=䔟F쪌UO?ۛh*T&1{4Gkzv>Q4XM-/t \r96{ $B/4(jc Y|h4;Uֳq3bc1"'TڪS=Dr=P|[Etbc*t™7 jvW#Pr{OJ/w{)%+w%nfwFL4.H LK l}gh7IZ#AR|GHz3sw@sxfSxsɚo'h 5ˡ&TN6*=u5/frn@cs:rSKw m2Tkgԣ{U=# Rζj`3 -V'b̊ ;oR>FGjH1MtLT UJCxE[0`mGq%q6g4֜iۜnƲլ)+!E@Ee߿,⊸Ȏ -_Yk:sf9<~ޝ~q=H, G9s?X8 瓤7~8!3/WsK&|A_,K Ϗ|χS1'Plĝb`獮#/>#bւu)“~%_uE˯-o -^YCڡ#|r#w`65@l ֔=i;aRDۋF*,a ^ħ.?b35ℑ@YJ" b* ?K,s,y`$\ܖ1Q6[(8^ &ŤC 7]nさ2*&ؒ?ӚHAr%X$)ce--0#FoBj*4.G9&"k?%'_ky}hP9ҧ`D,ăVPws>6!j"C)h/I z^MG%@kH ֈz(CJRs+ -zmxAxE'\)%kjvy~7.N W;ˬ㝥N A-dCIQ3QwpUVw6Tw/o].0VxҁoX, 3H {w7ޒ1_Ō{6nsR|C'Xn/ݘAAPl-NELe\QQ`ͯ MTpQ|ދJ bC 4$! `r.AUYƒ-☄G*+eNf^3Ń,Zss<WS4^?unw%9aD5@5;'wH~튌(b4|樆S -1Xu]/IIk(ȯ ~{C4fJx&Cf6p]mGoݥn)YK =/}}Kޮ\gg¶L"D y4QTsRE;NNzK#ljIKzIspAޜeyF)x7i}\zG9mDW#|̟z{a1j΍gUM)'}<.ZaٜkbG -.9@\y.yӅ3jۊ]y?eΎSCu-!b5]=cO|>"nʽAJEwyk8qpf0OCjcPr+.L%$vP~+en" -u,Pa({, Q#Q|ڈl.~ʫ~oy>Ne+T ԍԱl&c͖0|jճ'Eʃ|`tM? ^{vU|9VRlӾ.Ҋ^:Ѧ7wᒬuapG;~#3B') Vb[h!Fdr6MA^gğ.`zDOJn>̿ſOʠvDm%ms(HT[خCBqۘn֥inV5_yӟO`p~HR~L_a^a\:̝ye?d7i)T J%7\#v~ o}0Q``!\vmzn\ox6_,!=?-ۖwGb]u# -[S=(7 zD)ڸ.Hy:E;zΫ0?B;T4XE/U0k!I/9pcc뭨(z匴SEOU^Mlێ)Rt8A I+"ec2U~IvRvrnAm,(NЊLy@$;"#<v\N%7Q1࿩EjvǤifcoM;,b2r }aj/Z%us;HY}VL.'eʫ갛P!Z~xF\.{/.ҿnMRy ^'m%:?Z%&L.ED!Q/,b4\¥M۳Eȟ}٥ϸNRQ B̩p5EtLj I /Z]>b2%q|&^̚Q9FůKhŝOʘZ)+YťdtW+;sp`ɒvwpDjcmfۡkpɵ N/\W 6a֯1{fPD.YVT' \GxnX+n cnu|3 h%V\包3ؼT(ѭDCJ`u=BjUɡd{$#g[Jٷi5[2PJܝ9Ս9%{+s+G:4J}kFnˢiBOG.q0au杻pyV榾 V;11絮~^en!6SY;0׎CAG%7~$):qj@8]uvVjm]UDN J $x%y{/y9}_ )ܺv3e~&FF4`uZUJ+Eq#,>g5o191#E d$Fe54\6slmB_^?>k}5-NتQ6UΙʲWx2%&{:lvà2'F]eA(H {78\*_Iy,nQX';j*9fk*č9ļTiNbLdB_뺮N=F\?$T:)wQ&>|K)./7mXg;)vG/rjԉM\W &Ith!@bi L6Q_;jaJ]T[sςc'W\=ބo*b!GNC)Vgk `!hR6j+'8q-ڠ򊡚C'XELYvE/pQa$0xq"3͈EByiAogI?Cy2r|oM5.L&0(^m5}^03>^mr;^{lKdkёz;t)c'B~D_悘`qJ-؈ʅbߞ Lc@;$u?%cGcw)[xߵױ/6T lm91y oZ6̍;7%9%(v^H鬥 -bg(قKwz+ivA겠YY4r%o7E`r%#/BZXJs"yrbxbKC룇W>ih! ڡƏ]7U3$}j3.Y,Q_csb01;,w VcpЙu O45q{[IKxHUd0ЉFfϭcOޠ>u՟/Cex/#ˌXH̠le߲%T$$%_e,Ң%[Ju0,y\:~yP R qsk@U fEnM7*EXXV UҝBh@1'ШTB"UͣՍD=}OV@1'+Ͼ{d(X6_tƫ?OтV Ih-Xgq*#M"*ɒ|{ -B瀔d*'HArĠ(T='nx]?R<5P?v5kyAUrQTUB ʪĈzU@AdgI43 ơwG uIۆnw{enש0V['/[8T7dL+ - UPzL%̮͘">Lnó@"7:T5t-ĝGͻZϝuuEڠr\+{*?_gnO[$U>ҥI-MXm΅ĚV9MfȽYĵaf\&i%=Ӗl9jho5Zw7?V=0MSQv5 !JFO@PihTnIUt*00fQ_6aM?IDư![pًqRJJ|6TzXZ,9) -f"646>„ύM@giLwti]l4Uxh(zMI~+daDt9& κ;}C2ޢsWIT -ސ 5YChy\8 ~3PpDD[>$[ -gsnKp)wq^uW293O _꿨cS_oPt5?vNχY 78å`l'B x1-6WA 1POn ?W+Sߞ|rjԝGϰ31ۥz;ht xs'yV -j# +L~!Xݭk倫AK <^20t{^1Kl -ݞRK(-s;oj&c#h@3l /f_DMnW &"2vxz<']Rы\S m֚g reTQ"A<> LwYP K7ޟwuvwW0o._y#/"G/tByY ׀vf Jaֈ>U[ST+G|HT/iKS9 vz.!JU91I\bO=&fSygJojoE80SܝoSvOYO}[:rpf(7A]/ D 9p/'Y|(X~1|c=擵N~B]W)r˔ #g[anFHA^FB)N"Φ"FEPUdkُlae=Tٲ>>:0v)O4IUwuuWdӸ%i%q5&&⚤\҉&("(@@e]DI%dxn:ܽQImb1c0A#y l@0h (ܚشw(ĥbÛCuAn lxRnB6Yc?z+֝[YȄID=s؇dQ.IXL:7:"0 ЁMR_CL[E쑨?'ՙ\˨5ܓ6PZ!5 }4h}7ņMWCiRR#cce}[ч_k>N$i5m8;a8[.36g~_sh"A~u-6WP_cf[# ^> -MWsQ@`[q(U -^{`j޻;=S:)ѷ9M"f"䜙Օ1q gq3:ITYSsg¹H(\K1 #4Xg5 5.տzI[7` -;~7^(bZZm ƁanP#@gQ-v;QY nS˽ -Vپ2mawdTY9fwNd'Qm_}W?U0j ڰQZ!;&[~:~~="8㕳붤/bb:1x<`yE5rERKMlk ՗jnG2"M(a &i-)fT'|yXl^V쩉{*Rۄg-TehDƾΤQid{^W[H8})ǭ>I5 }/?MahhM“ pA%.~3c*wnZ64V[o>·c4qn_J| :!WzF㩖ZTzV~fr OI-q>hn-V烑 +Z#V0H9RJZ5$UjRw _S N`nZLho: ]3[h_^p!5:2h=*FXH𰶷9f`۵-.B2dA|\?*•Edu{5%AR3-M#kw s/#V-wzygͱ8-4Tu2x-J{hń~ǿIi>hYfEt_,QK-4p:[ڐP -lr?7B 1oMSegduu=SZp ViT ?5_m޵DϑLiE@ͅbơg?}w0ٙS:OVMu"ؤe2ȿBKΒ.Ͽ2z+WmTqfޘ6` -#a5FRکVJb? 'u]fk}p};"DjAwzZ[G֟ws~iPգE\I˚&A4JgIXIibw. S9`rǍޓUwQMOE$<;J }oI} -ԯ@z$H.I}1CM,dJρ' dInr߶^ٟ5O,:UOqy%qqnʬSj+5 (*,+P"ܲIEPp%to_rz>?lw&(<Rܾ5M &Qe F?.""Ebo ᎋ=g|ryza}1%y's۲WyJ6RRZ[W91`{ixU1wؗ'\3T:@Lͱ|brBߓ\5m `gJZuP7F4?79FMQ 5 -sBv(~Y'Qϣ$ M{T}+/WnFa|'x .4KCEqar-'m ѓM.ZrSթ"zRbLU 9|[cÅkMT Z:(DG2X& -B(u?Ԇy](?v8[50``Oz,'B8X{VsȟM̿VY%fqI SN/[ǀn6t5H+R퇴G|Z2lm 3Ug*Mˬjwx4tr/V;ؕV]KǞis%5fА^A Rør&lAx)ٶFu?%PM}x@&Ehq>wR= -!F>-^݆3R|8A;V~ծT7p:MNJ:Q1*cY唤#)!XZlobBߤqPml[f"rkG|a$Hϧ s;܄t6*gd~n$YvYDiNt`n-:VBTo]&Iq@_gϝ-F^-Ҿ6m/=Dx}d}H<Ą\*ܾ^-3e3ўzxөׇSVos9:E^ dA)wjoSHG9]]mn<8NZmͻ,Y ꑪ$Zb_ǝ H$3lK9(.?N[F{A@Z=U+w>ud|GS>[0RO)q[Xt7E]!=F^Yx\lEmtO0]uGC4=$' -)3`7 B$ mw L13͓D`rg zW&8嗃Ʃ wVޕF;֏C^Vӻhnߧ]7¤"}-H]A%XWIޗ~D‚; 4UZ$ z..HJ['uH~, -."%L2s|:WW^/"\豸$lwmyV%=E;kNeYxƪb1^'l n?G"<qVZ㙚 -c$[B>h:"cWXZ_g2&{JwAJg`fۦj_F1~;E_KA\ɡGEtG|Uphsid<@D3 -Oqyw`aM9&DS4c\XPa JK+B@D[d&s&g? ?}4y3TyheZ*aL6Dȿx%ѷ@>xh -IIh@rYsJ(Hh1sppѐpQ4t"sW_S5N]0:ur^W- -C)@G{svG=wT_{`Erv7F񷾹z 8@YL0".w{f.՘qľDL8R2+(4o@hW?KkP핼бJOuJY0,Q+xyOUBOwA@Q "=fwqwr ÌQF۵i~4^fzn0<]g_o Z6;✀s#L!qal&h| }|2Hs~'y!dMp[$ޔMPuVwJEJSB;meȧ}ets+z*F;_dG &XTQqKU^^Lcs`ޫ9W](/`>k5 y߻i\7Zv39Ѡ1E?<)MVQ@C{emi>aSxJS鱈5[+s?J<&@k.95ٕ`䓣 nEEZ|EH ĹH>&(o{(GlhyZ}dt{{˧g,uW3qAOcua(j8S2NV>2-KU犲qI9jah9WԐ%ʂJ26t u0 WoTX,#zu~.+V4ӯ -N?!vG o-F򙴙x N?V@t#.HP/"-*D;Tmǃ~Ww 8O7>%?>$J:pґ롟H9dZ$UɿJ3̨_+#oS; 9V +۶T63bO3Z5{ݏ}i3,@ѩ`yoej;9Tƈ!' e?)X;Jdt2Wg5f9q -0#5qHN2:5 }uYzDMi%l#Uݓt?2Od>Ҿ)҈b /uǠs7xu7b|wKv!{r˳-4 qw&3#ɌtiXb# DtDn4ibAT@D E(ؒMv}wsuyr=ܲ&h$?$/*bhf-aX'`F.=2ssSKUkl_uO3 f` f|6/,w!QӒ냘Gz>|||CALL*Lor>s}fll,AhMEu:,06PCu S{lbUlܴΊv~(9N@%h( Gis"‰*KbG2~nͷctoj޹0.ş;+n?W[XF74T ˡK/My_EuqtzoɭwK+7 RǨO?r/ç⟢CZheo ӽ̰63J,π/OMր?_R*x4ʧcw5 Is`m{Mi)%né]'捊Aդv-7\ -lX7~-2?IQw~ib]EDY>prUq zrNT2:?ˈOvpCo;{qA3)Y%%ԧ@eg,wBЅ/1-#;A}W1A=$ ruI3hň#ꛋo -%97!1^S&c蠱"  N0rS{/k :]FVWs (Ϸqlb`U ‹C<43NN`IH{[;w|o] ̰\$>_zKZĥ}n_'Ą- 6x;&H}|wH%9%3+o(nS w_%(]-\Zj<84ʈ( dl_ǁ:P2E%;{#ߖ?+ób| -s8Gw!ORF@/SJnx'=eC&>wDÛ8{W+y@tvWƻq/#&!-dsUF3a>B[# k솾8IؑIjXXj ƺg8k{)~%ϭLٚain4ʀ -vbr\U/9fɜԉQd,N, ;,>3gCjs8hNۙl(5b>TxTm%!y};6 jwT,𖒑5B\G ;Lz԰Jgh UKPC~Zց)hlo=~l r&eQ*j a`L DC_I`%׭"gih/,Cy _{:O.ά=3PΪknlV,&lG qdjh9IӬ:(*sS&M<ɴjizg?.۲qj "MJߟ"3ɺ'~QbEIVJ{r^ n%)9d')beu﫯b2Uv.phYn)-8G26WFϿ* zzٱO .I=|MRC6,M,kChz=mE|t}y>8_'9g2mJ+M𧙮LLҙtN,ih -( ( I4;.,QAQ&Ȫt+f>SuyuU?"3؆&Owoz&ɺ*g9SME ku7BzOj_ꆹVG20B³r&;呴dDseDd-r>N7^9.m1$ݫ p|"T=]J9𐔃 I8h˺0xpAFMp͠#ZNdselHgdXDE( -PS Wٰ \ґX30'Zf㉺8S*/s (H U,ZRR'F!B9JQ^|_Q{ZecJz홖K -*w{ qL6[:1 ͠  -bnp3P,|{F?ӻp/"Ssu'Jmۜ .U1|G5/b ̦_dsO )e 6 0>+}m1p'Z|OҐM*) ۻS;P6<J; -g|R৛ ¹pI: -vfi#xsd/gM)?RˁVt]L]44J[%_?4>`mG+'LR9lPP)bWiZ~E;Fzb[G, NА5,HK,ͽ}w48׾Oq Xl0d !Sl ؟EDGF.@D}i'C+"pAՀ# "F:"l!\ ECrXOO -baXՃJ*Z]tkPܽk^s-4? KyrwgPĵ˲MSȻ+?ຘqL-7}hN2¯-P f_^~p߿L &7igL- -csm#[3*.5]ͱ^u)2^v(g%- m{Uw [J9Vݧf5[„u~u%'a>OYZ>g@ޅKylߋ5[̎%m=6rֹT`Ene$1ĕh;W= w -~16%B%ĦebӨeƼ/%}^oKPlMmg).;`)P~]CւV;U_ Q! jSPYd1MWcv`Q5JQqL84k(<q)g17@%kx%FjBɾ?ՔYa)lfD ȢF ,j"Gt6 ~= UZ190vOȩ_i5!lMgOm| /|ur8^ !,9Z x# P&Z.\mu[^F6w!A_o^8IcԀ^4XM -H11.fn򭵿)z@F1Ov&ʒ 52jVo}g!zVl|ؾ>>vagGta5Z?Ŕg\$)7 EyKRgs,<~7-zh,ٶ#e/ti~[l~AәE{H>ӹ$ T 3Cr} ?mvH@G_PkIUQ &@<0yNqZ+|̢+SsOW?-5溚S35Mu՜:LآeK"( -ZjZV. )̜QVRJ>E'Z֧䴪]1\7앵UD>9>#æJΘ -Ϲ\uUn}-8PwӘd5G13:AImoYWrPC$Uo}r\j/SEgY?BYFZnm&b 6\xԨG;!6ݔnR!>8.s"|W5(`1EtAPRCfYXpUmy :=,+aޡn舨HEq=&#Vƫf.=;9C3U]>0@Xp_ן=Cz:ZYDB~t `k`~Y~''J_&5&> V -)Bj5jZ}@F~rik0ɡᣝoрhOM{p@syze\GήtI^.kϪGѿ~?r]R)J_>>pL#zdMp2wU7vN?ZRz8wJ"_ djhEy:s=9ڿ̩Ҡ6A|KAlM z]q{|3\Kļ Y~Iя>ښUs 1F&@O)WM˘Y)9S[gſn>l22:&`j *r{U!ec0%!  T!! ի.I7ߩ;Tl9mRFN*ˌpڸޜ02&/ѢZz}HM )̷@?W64o sXIl]31׋B -KkZ:LF:ޓq4VH|/B:^c?>4]Jhs6y? 0CUm{Dr{2&jspݩDĴM.G{NFq=؟ZRuXK 1&>6eaC*ju `k+xE9P=[hsZ -Wjztomeχk) 'fQzɠglݞvU20Z|5M22j+J/ka37r"'?d?2_'g[l{WukPwhU"gk:NhWv0_Ud%:/إ l0%;/:V$U~ ;J4\eMhBڝwZyWaewYҤMDOۄcT{bҽ\s[ǖBF%{Ix5'uoЛ1ZZʃ|wқ>omII0*y;t d l]S 1LGI# )uvDM·/~*'q in[&Oo9 =ӿWyhH|%jvn=#"V'lfuff{T?ܞF|r+y@R.YpoD\xD: >?{ţ5jEIYD=o>0N\3;ϽB?y__u6r0ې{BNd`+ ~͞YDZ\W66U`hEҨhTqgccQ/!#?d/0s<{ or&RaI)., ☬/j 2FҲ1ͯ ixQ>ũ\KRf,hڦd-2Z(TE +V%Y @̀뫿QNz~!\z&<=nQy_\R"9Y.e\Lt<@|/zt"DCw)nR/"&RJB/nmrFpc\阎z9^Nhw -vo?Kݜk 97w5y71nf@hHgK.aop!ү'Z7,LMnjG s'v>M91*sZUwka \6)Y%GνiZ)f^p*EӸE%jrG+] Q -@Y;p~{{'aQQh$e{4*4 K)eoI!8<Njt(cu (Ӎ&@(J9i#> -Ir&UFv)sYсҁn%#b*}0;Pށ_5uWkE?|o#sw>*]kaI.\ ->`46t+;~Fb5\|{?eVo8U?M Fуw~S3O;KI9h!4˞ t[.F@Ҫ12a5^n끉~a'wM!%02(9Ij!G2C)o?Foe<-t1 75BXWYί7#Ðiv%.CA@vG ?$7>VT!TrnqO'8u;/H'桪 bf~IYZ2T}`;.^ ϶yMa뻐u2jM@߄%03a~F - +i WRf!4K9Ĩ=RHڏCkc`eOOݧfNK~cܢ^o.,(EA=г/z5kqvM]XW]:3$e w8fޘ]Iv6ŵ2'3NfN3Q0H۳V 1svC;55CO)bVv}ƪM[Vcznm>Bݓc1&DjߘZKoW-5PH;=o1 -KY <%LFnĝ,BĬ Rʍտ>]$x< !.+SFv#N>!atcʌ|NkJHbSYVD_n>%+n^,D'A^ /2+v pϒޘm+;xm>`..G3{λ_ - L.sj͓xS|IX1K>sCEHY |D@>U 7粞gSTiNlYV\ݑWPP2Mht%(8 Ё9@p:d~8~B š?߃\YUMBGiSαFw9E3ЎU^F i'a_sζ^tqHVpX?JZmTVؐȾ>= F.SDr(Q jST7Ԟ aa&zt'X][zJ|mjLYL<>,G9W t|1 ]ZYI ˄[! -T}ÕsMWN}RB{t"&3Yllx.mI>$fs}`'̵V0sPgbٖDwhIpg@_0.)?.O`2#`Q 1i`4,THE|&=ؘ'Ü>\/|tB:VB'Un2hwtjvJ!-.H&fPCys#Fj -)w -"W6QQ+!*Jo}X01@GmG*rUPn`\k~)ei;t?e5ebf -(S9\ g6,R*T(qA>h6Ɠ_.1'b3n1;nZnOHlATuŭrg+ZvGH`bCu2|տLn'q5n\*$Ǎ5eEӬI66 !K}?n)9\nR[tpy Y7kV㜓 :)ODĀ - \ғ I`0n!JB-}U -v $tckw_svɆ|*nx[w{f ;HP9XrM$]L|b&f"j1tmMEĄax[PA:Jmt/itǴa NPis7Yu)]eA[ 0+DuUkmOFo-PIHɍ!5oaG=|'mŤϴ{{ ʴ֐11"[ -ReB};,Ou\pi1>lL."'/s}|gth ]Qu61eӒa"L0ڿ/GV q 9|϶c+:0008-v-WTo:idvI5WH'ϵqtooJhGL2.tɋ[[–(1 qW&iqj|uqS"n$xؔ7E̘I -f A - !#B~UzDC;!;GF8ED&f-h܂a9R1* W݌[(:'ZNWB7ԜW1v޴`7_q=q-.[p>辚1S.Q&@D'AO{jddRA{T5F CR8c:Vow52j2=՞9S8:J *`a $! $ovuSTV IY!!$ K}!~U7OֿhNF昀Wiٶ|wWF48n2R7C p17{rRnqU9(NTk\ Ȼ@*8بƄ'[)OQJÀoQ=,(Շf.#k5Uyۛs^]<%5fwΒz*etxzҦYlwL$CÀ{ܼ*Ä z;zrsd 8K.2 a´ړPJJtg -`uׄLߓJK: uU̖vR0m1-,sΡr" -fƫfm6+a&JK}=h -q^ڷ+5B|vis8;`OE̖ZnP4]- KWpБ;39C%]FBnDGmLPrG /.n<= ǼF@ w0!J})gP]fl^m$= -5fg؅ -\"N∁ޖV!a춄 -ǛUk?łFHߙw}h5ޑ"ٞ -1> & n,"kfɃ3ĻH $no#+%HN[B?IŢ*#' kY!z;)Ud{k]0JERDVlRs# +ᜨ4"x  \[ -)~ޔ$p7oHe1ppΜC  l|~${s=K,Al͚ _lt̗-:>f) m h~K-+L2nnBOvh'{2&g6,f'eGv.qn:|1/#໐ТT -JO;}E>aXBڟ#SG/gSV̴3/9[9ÐUݗ2q##z]2|}W7'w&[*iz)8M(ؙ$|2A3 Iޣj-< r.7k fͻH(|8lUБi28fS L,ÿlKz1p+ﻗЋU,jqQ#~?JXm%VU&cbb>uDz m Sf7t=)(oSzVox̏M?άYr+pA 0czFKPMݚD]vL\Ju3UcoVTrPQ5ܕЃ֓B~fc$f!/zw¯栣.!.%ww$ʨ%jf;oG:޷_IY]4cf#&k9>+9")y̴-ܗcnE"FIT_A8 JE>UWvd5}+?MJм$+,&{CֿN?8g)=\NrUO~1'|7 5W|'+ e]\ya`WB6pM(lS+WV 9֒{ISR~ӓ# 2@]`֑kpmVJlA?s@ 0M N8S4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈM#F4h4iDӈ`QA -endstream endobj 24 0 obj <>stream -HTs3tvNX–6|KWjV۲l Ф&Ѐ\Y1s/@g+|ɇ̔hvy{?gȻkȻkȻ_H4!qV5R#BE,Vatɴ(Щ*Q9!&ve>W\-ajD`6b r\$;"8>Ǟɿ -Xl'0Ns"N))..(asNq+xeyl!-:]Vq9ҒQ!uYWe)cNɏviwTVl<*S!CMPw5hFa]*`r1p-`t/W_y Ee<[asyyMe?$؛7 Na%|m1laywTKr$|Vʣ#(v;:឴FnvɥWٖ"DؤN5; Ҫز707)EvDiڗj{>bgQlV;Iδ&$I/j rΐ *[k9YɅϋ*m %pkʉ\!|I6dՓax=٭ޢ?:BL?x@%a:9Nq)1BpSڣ"t۞p逢 h%3yHZ;QYu1!j&nAPn=Ex @ʇJPkvd:v^CTaTJhir%#Zca* L=4 )SQTuј̭X-dPǜVD22.yCR#C -2rըצ+f߃uZ}F0FE1/ҟYG׈k_fWo͆ǯR!ܚVB`P'dLZI9vo+LFHnl<شND ZL֑HNYޢy/rIZɠfp>{Audr:JjGHLّqKI/r95ws f%K6x$0(&KxmƒDpbb/|@-x.U{܍fB6Sm=ݺtoZXXU16 H#gfQCO?ܛmtR3HH'*hɠI4g1M?!Ebb%Q:ƗO7D0-ݚl+\[cĦ,`acOjww 74i<O,,fAbŠo.z^HtKk(殡0NL,p3ܰF JQ=<۰}A?6m|NvbC Zq?PHskdpܾWx!fA3wNڧ$P{nUљ(^iނ"o^޼Gq/ *oܼWfvM<ظz^9ID> `:6`c26&Wu$MPa(6֐SP -@: pDi6 ,mvvkDE; W.Hxɞ#Ǐ0"&Mvì'WTX/g"2j F^ڕ>=>>£` ОN̔1g^Mv#ЏGE,̣$}V}mN5tntK#Oipla"̬ k^\ɼ7#IIg=S̫{ɽYN-w*~' ZD*7ɀParW^M%`B~C 㐔]0 --ɮ Ԗzql3:D Jܯ~^3KqxF'Љ1;5iȨNnCtaRcLUvSY&jSP~9;%|uojmW a$~UwSAa3AFcM繂C-OϹnaKH?21(IJjr?xmT/!Cbi+ǐR⨠K|T[&2>$x@rPU -n9{gDbϺ,/b ;mm{/LEe˯ߥWWKLPC.Kq+U>Rw.]_S;FnmζI88˭y$9WQ#2k >Up̻Eq= ɼEZ1;e,#,%w zYۯܳ+s+ku[Ea]feT^|__$OzD=9 -ᗶQ=&0Hjbsڂ[؀y$,p hUy7"_JoW.:0|p:߽E}fYFnAݲ;ynk'm"kVѨE.wꇪalq1xyw7Υׇ邞a[j.tbDƠ5TZ>C(v00 -*i-GH.: L;_$ -Z.zT=(ŽF5 ̶0jIOFmdH+A q2[)I4%O59ZKw4W:Y5C*q2sIZ`G|y5(Cxy7hT[ɺ9׸ 2!K0^@@H/j;Lh*;%h TQ#Wֱ($m`"YV qS "V;2*PMZFO-jҠ :& ]Z):4=\*VR&#)))MxX- <ȥKpK|Zty?뒶9xZNW-yI%D\Eo4TҠBpE&2љZiSN;,>$@ƌ:!+ISp -眢Ƣw sq*2/]vK|l-b4[ޑS*~>^v䝽|U`pNxy G6tu]Gq\OMyV;5묃.:xK $BguQ !`@@I;}ue_tQt?|~= 2ږ8 -j%rHz{b}O3⣄O-Y7zq 2gї'~`m=?%b@vZYTܜ޼Bv>I}!O$T\ȯl OްR|&Y+ز pSn~41I]GF4dH g֭A1'l.ߔV>ܗ -z%0? -JluU`?kARgW<nwnT~I/|`tnw*/{~_P_}\^y$:Bq~?cl{]Zژ=v:=OO]_;C64A7:2V`}Ҿ:v bZ?l}xmmL}Lφusg?_jϐ"T(a6n(Al|u(~;L#B|u *r٩w,BLw3C.Ә]K=[w.Bz2{(7 lfLA vƬj<\  kIfy3t 碂A,,VFgVcS -fmRs\F¦@{"%gn>aUÆ?t:-65>Ms^ !t3^8CT'T .:pq3 -ndңStҦg)Oi#׋oLznŨg3f qi׭XT5C\y Ғ_7ȕr-cV_+y6c26l~eN_-؏_AV#Fϲ $ukM{͍J1$T4W0lv!G'̚bI7%}o1 -z׼uT_{G-_)E}/4#E( -?|:f{g RZKSs֬S s9 ^PqQz -5=dr(7=*`ڶWͩ U6~u+ Ǧn& -8f3t&t9\w0 k(wINp!E6"=@V>D@GzIBUF%D7~8m9TQĔ_I/ihȤk}+>4#tخy%dQA`IˊallV烪/!+f&~BWɈIɾXeΦح$dJD\@& uLMMbfe짾_L6}RM_n͎&ޝ9oĤ=pV%f7#3xG '"lPq+JN&-mTm"J 3_Y7!tآdft+?Ox Bs>hkhYeFz_ʍoRIM%;eWdq<onB1$H[-vefl<6o $о/ X nIIrҍZ9lpN4`+vmh7fg2;@\--][;L-e軘~ݷegs燭?&$}@qm{gw~{cj!遬q,8Eɻ[ho=UtuR T o9ۚg%ŪբtD1@GNɋUcPkZg&QK8tLTVPa0T/WC%ZR^:\Տ2QspuO :$(a=4HJFF,v5/E+j!OKeE%)EC:m)șQ@:uD9B`91cgjy%22ņ[z?#ʁZXMhx -#C9UY)vak -P[{KڪPj:UKTk -.woQX 9*^PrH\pbؾgikaV7[Wwu:aR'Kͦe,D;ar]Hq)IWgȌCx=oG֬!o0AB_M4,^)O/A9J=lX),m;aLa^3RpoVࡌOYu/LNo-pϮi?#Zn9@-F 5b#?iSt߿/*yU陁 1juπ^-%kw;L07Yr^O~l SJzyqiaɣ!eS -BPK)GiV)5 3Hh0֒..G4*أ~)?ZmiNF"ʫS{` eRIm߭/ _/\E%+I -:f&TR -ji+j)]}9TϞPv[؈JPjEq&baЙ0ҚRT ,ɢ2RR&fTVM+)kXJj)Fwsot|l1O#q6+pߵ9cRx(K{G [Fٶ #0&%$SDqOAYW)$Cn2#d@=\XF3KzGZh;H(T%.*z%AyHoΜp'jgi?>\XYawcNq4:F bPٗ*^y"Ҕ~wnZp{c;3ƃ8Qe) }Z - |8Գ?R.Ip֯=etcCEpsqIx;3snE=J.KN8WQG˻$-W@n=t\6Id{ߥ.}0| }OxKΩg`.k5~5_jʹ]tka#yȹ\)xw7{5Ѱ30^ލʡr2ڊ9nމ\Fu$ IO)D~EpOn΍ 6 k61:ml4Q6tH2!}#lǵa!BE[Q] aO + \a #,TZmp ~I7 Ks>\*yR aFq']}N?~T6뀹ڦh)16K(dXN\lx坢dZ9 ^]Νq\fMdgk^RyJUJ&!)(& SC x_VkoI% 8 ltka6^ZKi[;uC/^;Ql)hp>o7NGP|k]+]Yڟ.'NY3n tBh߯DtYFaAEՐu.pc_2uD/>%w7*<RYOX4L,/?w6+Dau8n1Lrn-1/]hfKf 0.):[9V}2iu y~X|m癍Đ S|¢.fO1E_-a'B&:I2^Lq]ߑOJv3 ˰'BV3ggEƒ6s%bSU5K^\]X<ů-1B,̰~A5BڊWr1zW+ngKkĵ{C,m居OT^&lfc9L42&q}Rzil.7׌pQz -)ACۍ"ʎ/JT&;C0EvJՄAtPwVgOoڞeh³emu.5'o/Aa!'07OX+'jI6}'JKknH'1dOg&VY6eqi@O}dYY\h7vfr&D-6q 4hNi3d4[$%gCyd ;~|&>?2\Jۺj;F.5Gdͬ~gamҖff t jj%sLn)'s_/uIMeU:2Jik3qDB'o -%*mRҤnȬ\͝c0[&aӳN1>5=rv7밸@\B=Q,/F~ibO-{_=giI"|"X!*Yo)\Bu#@kcRD"Pc;fٸv#MJulB7%W1ꢣvmv7t] 4 yJꤤzgou#L;!cQSI"$VZ''up$]=53m"{ۚr@zgFńT=LXMC /ŵ˿-=;ZFaٰ ӌb.}WV;~w9 gC1HGFU$m=1Qo׎֓6<h: -Du2SB'8B/dTTL?A'0Sۻ`䕀ULV`9;_'QR$LB^.nQY,ay V#4Ӌ[MT79w:IBBw -k9ta\sK?vkhe so[9aV=^#JیB^Ңq6I~@tILv~4+6#9-f*9wOjƹaOߧ<7c! "6wnM_zb>Mߍ'> yFZmc3V} _{LVFЭ&9 Z%QQ` I r$sd:틺g:m8uَ=E ;5vXB,Dz%$$e/X}7 g|ﴒ#}h8W^0^RMeqW>OYJz0H)N[DzKd:6]g-%ْOO=-og5_uS*wr*Yk톐D͜7.>%oYssyM[b"hQB߅~RްZ!3Nm+Kx(2^)m4&2ퟆNLsu5 %~)|A)Ρ*cR rW?A. wBA%煽̥v#hbMÐF$ƥ={i|:s؀sd݈E9~}ae>Xk? 0S vN, j2ƞ6ÇEJ -5)nȑͬ£|}Tz0R'ûk5{ް>ýSAO[tFwѝ!a&S{@''hozB7=~sw)h00[M96b3Tp5]Y<~Hyi31Iv[{on ̶f1OA'ǪIT{[MK(L{Rt ̴6f`Vf!4nd-aװB,͌kA) 7l-`u/H P8yŹό'H:ڿ*qS/y`Z8OfD60)g&:?#rpĜ!2U9ՔPh퉹f~l sklTj><"ܗ:u/{iVZv:3[|X]L8,H=%?Dɇ~B";x3J׀%"oiLwt?DhnzILjwy*ӫl3~L&T`BNxQ @Ąm@-=xz:(wy0}z.sw _[%KKs{KŀQSG՜[/~7J!;䐜a*dFW[4t*'~rJST%2솔Mn4QeBCMb  }yzr5{5x|cgz"%ptwy%ѽw/$\/ۮ-ȣJ - Ps3;o/YȁiX0.D 1bOB4C7!>!傄 a^fi(C ofܸ׶(_};#(O9cDQOzW3no*Y}lß?q^ -w+h1`J~i>4ҩD/=g^!߭h8rnE[Ѻs@@5bj 7Q`렵S5h5Uc^!E%55+MzQU638_| t킶I |L{Ee|\H~%IC!vUUX'vo619%0QlYI믲IKrRTӓ 9ߴY&̆^Os;tO9G@oA xm AqJ) Է|sʎ^m@ow*[n_u7S"3LS6691)eo?` 2PwU4LKfnAW6(h~o(C1}M}-Uy6x.p=U?UJ\1\҆v[\-z [ _<"^^ -"'|n)篝_b#[9D)4{wVl|VXE[ &DS̃]^M#Đj|o}*}G)}r5 ras{%j!Ӧv1oaR2d _ [ӏ)3gBR, ->B%*V׃d)%^.2 )Pw ]2F/+4>@f4a}zlw$Xg2g|pxm߬:nLW9e8L7- x׷'=DOWk.4 -2t5Ȁ䤰9cQ-`m" 6&0M)Z!3bv;]+0H%^Sf瞿A[J~;űAqŬ <*=jJfBډƤ$S)ML[8/I=M5NFzIk/x֭iy<]QaŠͳti?kHKy½²鷟Fw$njrx !knqwn-؄XѲ"缼nm)/V u9\,W縠24\)ù.-+ Gg]])#!a[/^( ↵,'EUg!}"VKV_1kqe]Jġ)Z:Cnk" |T>KyaK:r8{sXى2!wLp.6='SkXכH a0B' {x -XҪr5?eVG_ꚭg:L:vĴ{pGx(&L:SI&wEP-csIOO!s(Ks>.i>]6)tpjggrVbUΌXysYjԖ6ΐgPrK.xtUs. 5.=/WbG*wL2v _&o,| pjS ]\\G͢vq*ya{ Ds TTŭ 5{GEL -{ZPf>jea6~pui!q3T]$ܨ*N8M ׳ (;OtYneǵ"*etLp̪&S7qɡ袴;$%t\#!$x1ʈ!7aQ1K??}:.jf՜nIزO&<7#6EH ^ɮNhU~[ͱSr08фxť?~L 'fW&ƀ߸7+?PEA;(0O6d7l !1- ,^NM1 .@+ 9!31]^n>T5o`&lT.@9G[>5RVjZikZPK]jAyW> ;N5ykQ1qA-*VH꿻/ק 9;8<%2]d -FN眆 j:OWԠQZ5 &5ߧtO|9qص>_gDMzU x ɍd]0fz -->}I廭+c̽Tx*}g#&^cz]LNoHQ۳-4&-BjkgI~y~g[{4,-1xHuPϩ -#վYzg?Jic_w )!@'q)m[73bSwf:Y>9tBhemoWkc{1-j0aťC&5xdkP̀ ]XCIuhPȩ [?9nL_|udx ~|] |dHهe)ɥf\z}-/E@vuH=fa7Qp}0^{;k OoȐ֠0*q+ -#d4yu;2>CَMK;CFzyjLo:G7UP=׫fסzf9f5",뜢c QѸ=;1}sB\H{WfU+b&/zQK@'lrʎnu\3<0xY)$ȃ{J6xUx0d7"Pե5PEqs]zIRVt^3*.;gENky<֣$ ̒@(yV3Nc|d bS]bV`*qAkTϫ - 5 `~Pc_y8+ϸ3L|԰FAw.Iep E~cH-'Y>Wސ0cvAWzuݙ;o:.[1FY@TE!;zg:y{}Ee]Z-2spYxY֐?쏐2_>ۜw/گO 4Wm?_nz<  ĿۿRLЀ5⠖SPtdyt<,]?v>ؽ-[ss1sʩ$f9,R EsDư繯_y~u~>tv8--"!ER,oSwb_E^׎uff%K\[OHi63wKlguUڭ>F>RL20*\ę9QlWG\6JHNS Ѣ<a0 ) uyOM.UxT` o s@' -JavBj\WĐB.(h*`Y"?\s$c&n IUD0 -Iṡ -V9s[%أ j &@EddM,fF&a} gDNee1^ꦲKM -`cg# v ݑ:h`C75*I50)PJ -1|u-m!DAKIJ/WZ¹jW0^=SMKr@OYUʹ>Մ9W0n[]h|wMSs v -lYTi V)!,* -2a램#-?/KuYۃps?PRaa, -J<}dT^w,-ynZ~dž }g\j?X{o#tُx_=5ig -CC](gd8Jӵ0QfMzc#]j+.k-dIbsCf7[c)/T96 %|Sv[Ü |@PSX˓uسY62N  -S2䋊KsGJN][]YSх~f5Sqoک"_b%:r1@岷 T{seI?u,5rW^ٻÕ/CASi#B~ta#PQ.Ȅ_-qj9:doYGߖC̄' -:FebotC] -҄]v-eg(`7[]jB>TUݛ*}WݒJrdz{хX@+Gso/4!=ĕ"R'Kúh,ŤEٚl1Feqm(ձz0'ly;WG3K8-z0F6S#"r?i=@w쓜t4+>E=@lRRy֑ ^?7 WN^R!Bzͷ`YpmiLI`cWQSR/0I"뜫ʿYr&'znV[}&JبgMe=ٜ|"g&OEL a^ "n 5DX䠷$XFɲL:Qb]L#֣b2`~݅(QVFMr"MP&?4iN~3.D/FƅcSG*_mh\ۑ~;ܼ>uR't4@m3%hJP2.hݔ|ciUJېNrJ:*BfVGqA1d po%G0Wa~Ýn/SK,52Vac|F _j(6u >ّ_o~7˹pY5}w7Eͼ2Or;Bʡ\/\MN,~b-m<[(z!j#S71-`hd/4j.|vk-s -l44!A@m2N5yMKuSuup4Ys4N8UmբByK:b=cA"m)#!ZuZ+(xpx0像F -W $B&@o|~u[wˈ N-tmZ{qLbOS=rՅJJbwuNȯ#4~l4vErγTܜ9:l6*][o(JF\jV)rt ;1jsA?W -Pȳ)b9pq*1Ȼڦ%W X+V{/*>Z('&y?\'6j4؝~J쉒Z$ZFslҚqf%/mC:f@nĴ+b]Fu K)ab%1G#xkq`;tC /E6<[k-xljx;d1Ȁˡ6pk^D,JKF+ZYzE УFX /ӡ`Ma35oK"ADmB."(qe -qd踹_k`&?^\kk {r x%zs}2`SA}il^K]])I6HQ菵F>=~Ζ6QߤUwIݨ{A2^~'%sVkߨk -E;Ռ,&- .𱌐lXps̔tyu4]=ZQޛ]C);xZ+"[fXBJ{p+\_/5TX_Zoq@?="4 ׇ2f96L]JNt(*Үio|UơBf2,iߖE*N=n x܂4LRH+!լΔ]ä m"[^7:kI?$ -+(7T'hpɫ&nIܚtS1s6EW.mY,"**e^K jN)9os=*9f&,#B*.Ug?٭3?!wD 4ck}iY/}f+L\Chx%k/̝-) SK*jX\}DG/"@zp9זjg+$y;8 ,׎cqHKߜ@T{eL)/F3O]ʦL)TCmE$91kЧcV72ݔ ~[b-y>GE6B]ۙkʏ1Z*ܳDzQL$x9ݭn[M*䰊5:"&Xj85-qĨqIS2lǧ'*ÙQu%j" q"7G ;ߒG';oo -fs4B^[h+~vk)Y'pKÁ](MX  pgbYjT}Oȿe&8и)bO*v lrK˅uKI5r 'tRvwD"{peL^|5m+5t˚{HHԌ""~kU1*,jbnNԤFxx}t1l簯쟫*HSB*'?ZzK ›aP3q KA(;:QOɬrHinYYH-I_fPɫ%]>- YL,>;lB&~W -=Fק/Խг &<{%!FXZ]x[ّ䃁&E>6j󩖎pMR$Ё -۟kptuR$CҜV&Jɩħ'ڸi^Eqͫ`_GND3Ts8P -ʶPEE/{vo}-3[ߨ(`_O&-T=ޝ,I<+_nu(d]n֞3f6Ⓟv-wnI ˆ^y l W0JftG:/OA.6fB:6&jˬڠVQPrt[ɭ#a5$MPRFapUSg; -6z1i*ԫվf1:eRs pN$l]xý3c7DJ,gU<QЙz)J>i "-,7.5^-6Zj7[gSV=ZhU3 %I)xS3ccVP_"ޙhycW(‡vawQ99n9 ^f681%ÁDEȴ6={t8Ґꞧ\X:1GZ|fņX|i“E ԋS\^_ifm1QI,Dc ` E)v(Ď1FIfO7u^~ӔٝĈ_ L!,sKC&EfaTo8Jh( km^ĿW}8̾k+A^2tݥޓXE!F?<K$ⓑAv&Qlغ4gۛ!w'߇*;mSoFŜ#ef-j -t p{yKa#0O㓎gȹs4ZϨ*_A{*I) ͵gFḠ`VEY&Aãzjx4) i׃AZM꘣>N7)_?F*=f(8bpu^Zv'#pWƉuD/>#M9VD4q_-⸏ UV]*aW&ھ\TVE\hiA}vAT*I{nF{'89;=+*l*x~Ui/;od2y"!OGB+jN0cƅ96JQ xf)޺vv)LiDS6A_7CbM!#bL\7 Uďev՝eV2`?6>ྐj\mu`Z.}Xpyx%f:֛8Nq6Q?'U#s`ݦmf/yRN'( W5ze {3i/qlç`Cթ'S5X]-FK 37{)Ap_4bsM+L>CxhSaFh "^`/ x#I=qg~)mU7FWI6q:ks(AXcL^olw&OhVB877Z6jp<*\7xMpg=+}5ŽQQE"$)'#Ҕjv7 -zLCс-6R!Wk9 -jam{4+djX -Q$ܳ$.y5D:@;GH[%\ ]ˌj>P:ӓ*vU@P=ŰEq\_XWj -` txYEL591+5 -6p} Hxژ5 3R.UBZ6HPE;]|QdԵ c!x^A2`^>y؃ -> -B`'E?/bt&yoU%u `L0ϔIKlw ¹&\8Ź&3β>? )>Ī r:nN"e4L<=C놻}&@-0u[8e /Xd8ՠtHnX.֖g{{ uY<` m2K:]Ep_Q5pp$QYTD@AEeqijhZgikS>=3g9ä( 9^P ³5rR>RR+ -qIPMd&&;H7ɾKST7|SQxy'Ogwk6mR_'w`2A^P0HI`t8&aJ&>ݚyC]v+xwԒ␵(|hL@Yb 'BH2 8L=)cmq0j?fV7IД%nl}24Q!;4ш;B$`XyUIl0Li߷J>mپ9ƿl[3tŮ 3'Hb)gAB RϏ@P4#>wҠ`5⩳)]Y֏=`yb=45]n|ܛY5wƸ k"(C3B'rQRi]A^SzQQxhT2[%U]^RZL`d a0B( -^S@Qtof/S[Xg#W'Kk#vth˱rQ SN(ʏp;bBҤDL_fƝY g9?;hjvljsِJPB@?yӇYa1Й/)>;/?;TU>d3':='em>N_uUˁ(zD q4ȊhbR;PI2XwŶb֥-%^n6O~54UjzaAZDH`0cOss(*($ dBIr:hϫ_6UM{&h^u\k'|ɨI-LՕ4%'C L xBJ }P_A4Wv 7|9jϛ/o&ه>TGִ6.$ڔTd',ӁLq@9d @@qA!J 3 r4U%'S}w-u7~{a;^֕Z rAݕ|^b$2 &KF A"H`1B kT^+==ar86asӾqЪmT9. KpeLi"IvH(x܀Al0")W0 u$[՘׷;OgW:[sX Y*vp^(VV2ilk允Y͉ޯv9y=YT]v=giȵu˙.cVma Li,)hꉚJP]ͼT$O\=boqf8[wY=uMu˫HN[YhzN5OIA=!= 1d@d>AW˜ShxOguG%7̭Ϗƕ]qy5eDRaQ%lXx{!B'/ħF'@8N& [r8XAj6+h0ˤr|׽ #۶Yjbǡya9g^MH[}W.+OAJ>ćٛ)yPiFrkf|"{]<|<~m\޴zoaLjHTߠ6WX>7e<y$O? aCMSN_Ѝo7]:/ߟϬ"Noh$\J쨮!f&@/2s"~AzӺ3H7'.5S-`#GHCN-f<y"ZA'72 RB1X_."[12zɬ(6G;ylC/cOv9cONS]kt/Jal.󋡹X/$p#*՞v,e?eV`j?TWWuL'dҝ6Q;1qkD\.pY dMvAe7LM[ -ڹ(TiT`N}18Toؿ$V5S̨^ؤ",f; B -N;_6E=7fP%3-zW b(_eU#V`| s\vަa1."%=uP|S>^W{w,W :tz5M(zUJBR*"&2ƫc뼙ꗸ\?) ᱕n/wwFfм/ -nz[no& eLUBBQ),r^^>Q^$-zs*A{f#XT1Hi)/TY =H=*߬IJz\anv!ⱋlla (]>4jcD oKƣfoNm `=OX^&$Tkኚ^l(Kzxfsv1)-faĻ'; ](~|&M -ܛ7Gf%#T -.>t%ZRAІK>>DBsC`EeS/5}xK>q|ʅ3.vD eDy~vքszR :A -szAfM~-u;HzԬIv:iU gBXA2(6⥜3uRwqmORƯB2:pO:/dճ*,+0D!<%G.Z(;g[δE=COrOK/-Yʛm㻰Pv/7{eMs+815Y8EoJNT@d32:NĎ2O;&׉F [hP3X1$3r  %jP))ʩG~Ү2vt3B#IFI Bx -)Q覾.8JT|u e.\W!|Y#ܷD? pC6ḻxedy+ ^H#"\iW͘t-bA hQ:D{ac>1OKcn`ꗭo4#I's>33^{iH¡ UMni/Zg?3ε]ϩb4Mz̈́O?%uCw~,d1{YEjV@$XiSQN֋%M9v c~IH/Ð=3qbƴ4CG7Fo\|$3dw x0_ƫRI!ctg˟RuçڪkdeUTA͙]Q ϯ}5-vm^*9z^LM]+[A7.|ι:ueħNaw~qhAf%f䞜iֹy>EMq#wVf ȴ5RWq+Eǵ;3]BsH^a`QX!]2mQ?Wıf@@C݂ yܺW*k*œ:3we0E2L7%9U5 EAܝs#cSj~:.ms3׆G81!::*w̔b]Leo-[%m1 |RYO䝔 VMx$ ? Ǯc׭h&={ic(]B5c} -Z~gS.`BRb1dfOq>QTMԤ{j:L:,SI&VW'&1ʘ% oAvE\b4m4*eD"eu'ӽu?uz󘡞W2'6~—}elOy=㢡r.٫[6Fd=2Ж,ڹx}= 95v 2{Ba@7:bP[ӍW?B`o%SrAKL.*66:5rNw\5|RLnLN{~OlZ<<1PJS5z -QwEŃ M!5iI}"Q\OoʚXIiPy("[<ͺf3;V5#cZ을㵾[a ӡM'xyCL%̄Xuշn5Oh?{Xޖ3&`lkPF~|hx5٭F45Iqy8X05تNTOk:@-EȖx(3#iȈ -Wz獄l-:g3f #nMPWHKI(|M\n;DTyO (h%dan&u):;&3a4B+p2e}j+ia nt<] Tˢ}iH7{jL'lDpf"" 줦ޱ©?^D4YDi(q .dcW@wKa9%*;7¸'ǂ2m1Vu = Qsn"2+w=KÍ#5>|zliUB'}UOt)딑 -KiU+'kE Lٙ] 3q&%Sce}5)E96)a„5a-FVHU5UtN̗ܢ" -~/b)ugbkVAO#'AYW+=[KIx!'}=~9䎃K[uuJ5l8ec9d -H)12x_aw\p9{Ỉ]WZo]54r\3y5C1-[lVc*a4 ]"igOq5&`/zjj&tW:==I;$:QI$QqMP%jbܷMpCTdw52_͹9yŕ' ]t -=]ʍjԈݲOBwMq 3!ׇuZT BL։UTmYTl. ^BINȤůyi5 -)3׷/KM+i04ȚMYDMZynï45 dP9+#,Trj-}ثU{T2&W?/ $@'̞J-I=S`nޯa NŒ dD.z=O=rtWO@A+@s <[P+G?ԦGKkۄ$Sv&|lm}5qyq0mRV@@\8k0:]RL|#VwJ7Oꙩn\0I-2;}rjEHlLjޏ⠔K],ojHīQp/- k?m=^`iy; %?{Y&[꾕q7g0_i^rC  o:j du4HjOG#"ݑZ뛶ɖMLE ->@db$S+ȨU>)ҹ_qݱHIg3<>Gj˰qncԣ?(E4n6Qy 3I <@P @.^ǫ^gu{BO>b\e_7 s7 N:26X7td3o{-?G.ʌDjqo_sKxg 1kcSƆ;V(~xP!TXT+da`{lBgп&*iL !vV` kk!T<}q[ -T\%;L_lmKƀ -]r,R`7Ԙ2:p Q}SCH88IʫĖHP9q:1ED#dP0 xCO+?uv|tc==3E{j:!KBDYeDV[튪BB$"D$ABl'ޟ9IBGDvOV֤٥L%+H6D]R26C/u? / AZX8%;Ъ$9L/Kbj,9J-jrr&v,lvi^y f3*.*&5w7]37Bח{\*6zELJY2(lQ -#o`wscA2^u*Cv)-j̼&8gCG-U<1 lI8pm[F,bb$$lpO"hYtus$&v`R/w'~p< -J\ <М.ziݕogWW]UM o>`Ca_VSlw7\XK&$?TǍ1 ˉ*&,P+ ¬ՙk2,7Aaд6llHhj)CDszw6D}> rh'}8I-J=uFTZTU⾺3p^cdQ„$|YAbGY5|jD2R϶q#gW*[+6NE).!̓_Xo䨬nIݻE 967so.,tCC 䘨ER@. gio&2Xb_'pyJ4k:ghp |m'dytB38&kU {`]SsG؅v)9uvJa|hjYz),1vF1|MUW"&>mpQu,1iyBUV)'j9ey5؅Ivp; -!w$cw(~R\),%elIi%W{K÷U-ŒKK=#C}}]lXSUxk_SoIIone)ؓk zRȶ!!wDe`_`MwZIX\6 , SM7kegi7G8 "\dplP<2jZ:@ƺT/?]l<0ri-iE1bSy8l}mK6^* W%8dBLuRڏ˜/ m/ aW%{TLFP&:ٕSҺrE[r|}*y 0cCI5 -01ENwM|=b[u>`).yyum`M #AH -Q&=ɭrX%}ɿ s"(-?+4ĩYpdpX~RZbd̞Jm|KE~rN -h06\W_\|\7wCٻzJ隬KO.YKW_x_-fYmIy5~ rBm8fbF?k&ӣ"SDȑ1 -SU%3C*h 9kfu?leO:c/۠S"]C^Jߔ8['DȩlDl@6g5uI/K -JFkYQ(Z|!#f:'8mzw;Yh)m˺a':D b+rB[QQNU36 [rCFz iS,Ì ;IVi~q)b|؇AeߑRW:qǀN lܾC4 i!' |ɒ"bc),?6x6z4rQԴ&.ƒ'ة@wCkР򄐳K)_lOKhmcFP+>$ f? -;ɧͶ_ cvյh}pb{5).ӧWO>Lk&ft2;vbƌ% -QTdQVY [Q11 -. ȦA\6535Uݪ[sT7 ΫwHE -Vs t:u~9\0߈/KoI{ZMHBFuZ\ s-dF73KτO~9RGlX-1SE&pU}`,ЏLyHӏ-sq(o;2)d6A?5Cz;"X-ĭ}%|d[XG!p xpgQ+X3#ޯǶm Q 3,jdAih& -¨ S䌦Nߛ#kLC@M,пb/9j|Z 6?8M7~x$glI1A 3ݐtT <)ww-6A2ӗ5?Y&Яg++[[C3̷sѯef Y.Kz'HفؗÓ6}EnEZVnObFo˂j1TzR3Mɇ9jƒ-TtjQw/7f]`F%( Akz7LoȐV镵dZF/ϲ.t~T2dӿRTZPCX!ndEr, } -hg -`RqU9橯D9ϸ6XͽbIjq:1 o^>;0b6/ܲdu.fwy$`B"mpMq9[ w, .X_0YiT:璷#kltS˻;ڟ5?)^,]`}j6foB_gKؗSPh# .ӳ]u -ݫx_RJg1O7ȵ1Ck{f.a);d퉩{,:N7x+0}Y߷E޽e1d5b5 ~L͜&I*\<Њ89ԫa6{W9hGbܥ8<9Vy+#+ikCXmWF.pȯw C5g!y/Y'QיSޢ g VYe#ֺ ~aD'Yĕ׶G럸g[ -SڍOIPOgzA 6ck.9g4xu -.ҿ*jZHE[!Y -j>6y̘Кcke^ayc噺HV3Jvj޷\R{0洍򛞙Qx,5T}9O-D|vcfXb{?X@ʡlD7`A~ צpL6gy_F)u#0|.`ڣh-N6i_-t/v+|wRץ׏U&%*?xHoa b})cz""!e"mEIdc -`{'$glu{J:-E)wȂoF lz)Kљ;ӰDPlSV;i0h >'a'-b鱙C+|&W"Uʳ@/YZR' 𛣘7?r5yy zqw*~ϨPIRSꐺ7G|}̊!}ʆGҘ]e'ҩ5Qc|yQ"֙#3W 92^\PAg=E;LCԢ[㋻SW rlw6Q{gw_~$G15=5i`?$S7 2]n9\ƾiu69LyrH5naU7Tp#O@v Xm~_j@_gp҉ӝ%G`~G &` W{M|X57f -?eImk ? YxuJþ;povnW."꿊rKrbrA=D̐ameG 3ouz-"ׁD; Vm^w r}ӻT yQPr=XdMrT_&P33tn8x:<M.*83 /+9 -rWj6^.2 AU@Ϳgk]UYvȲUb vy* EuBjPC**O}*bgW]B}2$ 4 z'&zl[B^%X _xb= 2lONz&>WwZ $pcʿ^\ZFIEvzܳPǦow({3d2aML̑C;upgNx.[Ž1i|坰+uQ: Q*f4=X}:Pq=5IMZtdMҒ+ʰ\גw#p2שp\1.Π>P -OF6-~ 5-?tyE%tΞXߜfz_G;P5"b(>mYqZMC8sF!3yAfg.J)Ҫ ?L"VZG1x﯆kVp%BvO݅;ɦNmtb҂) j*)XT3"^|pwVy+:unPer1)sv"my%FUX 4G҉g"}_fAZ8+ScշJ+`sc02+l*~\aþj_O>ʸ?G,O) ;cj]d|b6<-S+Hq=sS@䫸 7$ /(:*~, -8s*$OV؝m1shr .JsD/`mJe5:w]n}4$kȽly86k rWϡJO؞*#I5g[c3>fƒ{Q2P!#JI夭}n(nO"YN9^  -r|Q\#GA(1|+1Dm`4=ߒydKi9U?9{.sU`㨗M7W:a16)"eAubၖ~-16s-f:5dh֨*ױOfB'g>/cZR >_pO'iHsM lrRA+*[İMZLlkq@/G:v} ]E:wH^u -=1=ېPˀlD\?&e]C٦qP69mON<td"Z hhk -䐥]_mM@8 p>| s[ά[8%NJgQ| )ﶽpz/,mgQ7ELRda+}F~QZq, mFKF?УMDsixh/97EvTY&4l_96Nlcs*) -X;?ypu'ⱡ[&1*&%n>vWEk[r.kwFkn>|ۣ N7?!bC;Қgi5D >Dϸ_"AG;Vxm9_ؐ h{$lvCs2~ԍJ^ -䎪2e[)GfTņ9W -t׸TФm98qg62>DC_l4TĄ+kp'߃+,[ј#$&X-C<;J+ߛUJɈb<íJwM>:eR1)l2&pjϬ,Bۭ@UN遦Qo?7dz&>-*Meѣ4=K)RRRξYYJLΓ<˴ʅ. E+j$.j0?po![o -GY~BW”K5IPcrB%/*ɍ(K3'ljɥ.swneMhkp*^a\ґ^(< BN߷8c3]%qv–SS4rjmbx lK -lF%gpUs+=rh_{k]ĎraQNы5%7WC1;8 -|k2\^}{Aӏ,͟&l-wlsdBsU5Ko}QHî>iޟ`7O97M܃N07]rLCZtˉ #،)drKFz$iu }8BsHM%=N٘8*>߻X6ttmˉ;Qd@#M g).6A˸E1Z1$n_I*fM3J)7nSv3+qffQ ldV2ӑ>3rBL}q -DI\]F¯֦S\_iY߳g2kqlI4NL21NQbC" HA4vaX@i""*bC@ M&gg}y#qJFjbs^ 2PE/91fEeR[s!k*a_ͨpiAȵN3+&wQs9ugcJ&ԩ"~ohrJ/*z'"gq;]]Ģ$bz>@VVӊ̲ڸ|kQ(vSIyJgYߝg!Z6bwAܷFlG{u Fb~ %$xu9DX] 8i~m6w!M~2AOg\sY[iVv讬t0C~UHNmOM?t5ֻ${hLϼj[n2<8ˮِT޷ccG:6YT|s>usͯNօJu堀~*\`/쬰!:2.ǁd„KŠovE"ԓaڶ0H3Oa=,JkHp4Oj)^%)>}XVCR~|⣭\zkRbv-S#^x:6XwЪ|*J^vj${`QXG'J$hs"_"n6^UcN";P\sA=ck-8OM)WTFdF^^k]Ǜd gjrdBq{oKxCV`pkSO{S%JPMcpAcUK._^lE< ;hcL4đU^ jj,h_DQ+tJ`A@?]Rhظcac};䎡jY`~UHweUP1ֱBM34r_;PL>5GTr 63hfQ||cHcЩ'[E^h: "wG.Q c.W,Љ9[n<* -أQ]AHwLg -F]NLhucgyZlSɱi::@-ŮZR7MF?^j:Yl@4ҝ1B2vBl7ɆNc|7NJ -_|׋Mm-ag7,92|m.ç;?aA[Cmem?ֿ Ϻ8TxNȸeCGz5IᚦGJ R"/f؍ϑ -FQOohgxxնZgkَ$;^.ǼwEoi P+dXۡ`Q,y:ɏ'2]ǧ_ϐ/,7OMUOEntҥ1[T]ڂ>/)xUOLIJUZ$ģ'&qK lX>%=*6u Z1J~)2㒤&JsC7WE\u^%>ͱM\jnw00gPI0?ꇲ 8S}rQ}3pA@*o܊vyCGN g<^0~ohuc+M-ԌE8 -!K@ʙTāNߟ>2D,qhe|(;di3*혐~"hc_bnBhj.4x *ȡ 8]6EV}BCU[WE贬,pNZÀvpq39zQѻZv3K+⦟6aŬsh5P0k‡>Z^WokpN16U$yX:A{:E#CzV-6ϫg8+a3l1pAG -bg5J:h;}dl r 3X9ڐ[^3ʜn&T/K]uDM֏iɥ g_O(?;M/'vΥJJH,`=z.EfaOe̕ӓo{FY1 X%ա㴜[S[ynnZ-qL͡ڸצFfx[a=]lmty $))gGS_$;W l mt=8ڒ'81w-ϼ*_T?nkhzIgz/4l[Yo&ecs#g{g&Zّٛv&ȐvWG/`]C9†-}ńMn3}vz&$#N`߸ -rMYIϕ|m/A1fyȾgTHɻ*bʶ^ h5* 5s\|=c @-tU16a+M:fPmZ?1GNӋK_eܛiYZޟUaV\{K3h'S×sߟL h'm`WSq-$dz@~NEUs|$ -P3 0]JN_^D~0X[#ₐmKgGfʥb^\U$Lhg,s;MI۶Ik)6+\Z b2Os;u4C-XO8[\:|Ζ]9_`?Ӌ)]-/(v:)=Y Ty SqU^9dR܁XZ*O"t=fSEHv'Ŵ"ΖQPc'(OWT(aiUCfia޶w~㌼VI_Ң'TP]mB8:ᇣ ( 9Ւ[vyS5:9j.Fw%mT`=݉ J恑_{0- :Yp(]\>ۆPb~'^WLb99)g]4ŖHkKYw|2k9" 9֎H8 ½ߟb -N5&ޔ~{Ev0ՀfeO p9Z1[aͶ]Ѷrubԋ=ԇʐ.xK+njŸ2"-66Ί91z^Vl[ {ɜ5=KAm(>.P0Qf9`?LμSo$d_Klu C'ށOs:"\jnS\=#d[^-2~;[T]9{C{v+0o?J륀+䜈j$ nH fUQnT3d^7ҵqJR|LFx4--)ӭc{͌_벾?;ls3la3#˜PP$NroFBB*w -D)-r3lvq=| e\ xo > 3[)4a㭓 -"6B=ª}pwu).~فħhXAVŝKYwmUI%{nXR[C9)H}zUxm|B=7ՋoI_{Yg_BƇ\i?MLfᇁUw!:GPqViq|;:Rz[;5ȿ$`̭UAwV΍_jG=Fqa1Č77Y($RVߝ@%'1P@[[$B]*\0>aOEL[&UjӁS|j?PN$(kA B`"*"hg e!_zЀGs2v\ ĭ؇KOߺVD)4  naGI]~i^ iD Pn[{+-}e$dܜf"*~fNI(ink -,!6p_X5r1/_ձUq'|ԣ~Ԭě뒺 mOi?&H!W/C/>s4zÛ'~~֏;CHcw :( 59IS0r]%oN0 ~8K DNs@-nXFAYGp &]}%ش朐̫vGrJOu{ 2ÃjXI75>->&-F)ho Tsjb~Xu&c<0 -B<$LRҚ(>a{v+v(Rsk.g(`B6Hqj5LJjUMEoŽ>ͫ\gjfYXU{l6G`C\̰DMŀ&)pO36Lj'&NÅ {5h] `Mc}[Ҁ+.0NVyj͉Yp*c#윥#7Px&n/30>-VRdoHз.P}t}7!~k*%FoablX<*6,*6K[Ϭɦ⭑x@Ys&Bs̵h2$xpky^ '΁3K9@85]P\dȬ*!Te^*o4?6?5g7N|sҰK$Yj\*fL+,{dl$1˫a{;0 jc]Le9A??eFY瞺V ҬҴ-q SDyFFpeRAP tG, HUs -`P~9p[+Y{ וr@< pCS"W/*m%͝S -|֣e9 -@ K|t0|xٚa5Dq|cs]_YdgEG{z`RS5<Kl65r1a ԯfUF!t;3%.[I{m_ѱeE5lH}s i5xo_] 6)hH Sڸ %gYVs ƧzbR}[ΞYZUhȱ Aڄ<rwC?]%+|'k7ͬ2i{F gKîixQ!0Lƿp+إ!Vppg{b`\=,hkGGx U%6( -S⛵)VVԴ2 .lE*BCxU8pOzW}Əm򈠆nGLYg(QcOoiWek]w/;5G"$d.'zZSaAׁYpn:bޖaC˫s RCz!ftiZgpJ @g݈}β,,?qpY.iu]T1z 79ގژn x{ZjiXRL6 Ģ8x 5*ԏHy@˺ <0 Yzѣ ]⋃M̦mL̆9o00Xglg!cMovLyNc5MۜF*R<xOO.[db78E9>qtj.?F/3LC:?Lȋp~ļ7Wŀ5 1Cyknqlw,B>6t]`!~5̫ gZ&wš1-(mJp \#x~@TR2lr+ϧ!A~5, 6gĠILeQdtvW7ďWGpM|حiV͔{Y/Hޚ#B2{{2D9?7 wfrUmj'nĆͅ+J#7pkWSRBP:%ƍ+ni0 zܚsϿl:j$Gӎݐ"3nUػQ􋐞Tԓ! hC,(ggR]?<4%|K]QARQiBob,;DG%8pt^S~rZBb{S kys~zspQhgbAo|iU1@^Չj8KIl41ߧ}`K+{ю1ԫO0J%@jpoV&yr^'&R'*nMO?eƕyTj2TML%c$8v;cclA !$6jZ7YQkG -LHIĭp"&d\%yaC1yݲbbzڏ%;/eҁ`s'>X(&m2r.[AbR*.]At ͰJ -+_fe?t_6Q%R -f]/e= m  !̧$MW"N޺ HK69B !y3s{YX.: KxUKTP@yb;"Q93bg5TL |}|y8/^Pvֹ.41Ĵ[E8Yr -HۗYxhK܈ YWƯXߓ I'Q>7xOyz*[nQ'\nΛ+TӵZhg׻FW9P*>Qsm#"W0ߣ!0?jUB| -p"hYN,Sɥ8ٌYtktSsӰk1r?ftӄj)͈L VQV="+aF:3+(,(ځ: @,#E҈6kGjX+xVDюKVAߙpaO2Ϋvc>t0 7*Iy BoWySٓ36Vog?`EՃ@!j+85%ZuINphf/1n%܇%th_af@)lhU]NPă- &[+1"/lRx̿ue6ăMKAzkiN|O4\#l#50C@*_!䜔yy,9S5VZYP b2F qq V ?6#iBMbF/3Հ)'Uz@BoH̺1;`ML٪PqzрA8yV c~=&@Z^!s~|6 6cOJq\*yn<+ki*V#߃ѬqMTĜsƩ<=lI? ,'6\8GY5@d^ūaṬ?ŵ09.5`k購 ׃5% -93X[c%#PA9i1"6 !*f?smayͯՃJ,ip4RfVg9ɪwOr9p$fbFoEZA"QmIci8d;aIi$%RwuGK\!HQI,5 Kx->~WDL?gq漮j3 iӖŻ9g{دQ>"/Q;VA,]t ):c~ƾD͈os -S+0E@j9IM9MGApXsT2h_Y-yAv+p1k<.8{4$U\MCvO?%xYoFT"-1`>yǡ_ N -TIw^1TПJ'EÿV< IX;IG^L*JH/Y}tBlxAJ=$[KN9UY' !gT J>aށ7[v{pFӃ5QZ!Z<&&|f_ā~o3~v䎞ysҊfA`><+ '~%7of=L)`2p`-R&L-!⬞7GpQ**93yʁ]tg$ anT>yN0F[1|/޾ \L8x)U?sCcaW!tn@D ޅfJ?fW;RK>Їi -]hTD5RB^/70L@<{ٜ:܃9QS czT*xA;Ha=2o9gGN!NFuRVNv+Hwcrx}s?k|1\ 6IJ#heEQjTzRWT"^kJqXnK`y% -[OvFeogH'#VT+۔v˺:Oltr*:9b7cԣj8_`N_[ۑ㏭l_2(R}_kMlYH/@I)*vu20^IN=ihpKү@w]Dމp+ɚՓ8:% #}dg6sR7I4NxeVZl"d2̥vr7ju( ςzƾ=b&Ϸ95znط>}2*vh]V((ˋU!yt~r0a*nYF0[:`ym7g7D&$o\}BFPJ;/"DzSC!g|G,Kg"F[+J5 M $ h2Or~&B5ܢL|޴ݞھ65~C??/"r2Y[H+ɘx)8]4Z>9D1f3i]ðYHuTޒY_ZW6Q-* |YՖ@TFL%bÈ-`QTQ^* -=آ1lYo{w󞹻!,wZjˮlk%qM)5voV:54tD:Qk ^wR\ j'M_]c`ɅCֻoL%3kx4E+&sEҦDc6G"^8Z@n$4a uO[q#QT` V4ުMv}o\ꥇ=YQx|ew\j|E,&gC0рI 9~%DMs 6愷[RUNDž_б)nlvTNNPNѓ; []Ŧ;SyTB=obc-|l䚳pw\_'Zw^SSҿ/o]pӡuY68.v4ey̪V;s~ ֿ ;Y}:oٙU<^ͱ+X% -sbmw=jn6>&ԍݫ(7`#Au=4/ڊ?5:V'Zٚݾ8]yhk@ib#~O1 ˄sHzu[\dkOsy/McZѐZҧ5ruoYFBR$sHr^G=3z -MOA?/mxVq\h琢# 5Ͳ1eS6ㄏOQnSd6#)#PD&<Qq8Ctc! >wϷ1\lLzLEެ|(.1=#W%220t3h_h -.|χ$QAn _)D'u8Ik=}1=>okqj"W01t)U⤼o{AuEw9AiHԋ"YG\PQk:vU@q0}6cӴԣ}ɵo0[?K_R`&9; f:YG?=TIǏu%z{rxeUTEKzU=Iw# h[3[@ؓLh>HΡ(8T1`q{]".juc\Ϻ!}gA Ƀ -+ Iܱ^$6abARsdSjifkM#ATVKCbƴ?=$t"эx^r$ bkȯCPJ(PEOF uդ^njflŻ>ä\sӻ$l%zUHJZ\S@aAXd.Ml)I9yY|7tcpy20ܮ~Ri}ȮxGcBV\1ƩCPF!^V2YL-$-M<^06 z,U52$3DpS@+1FHV.CdH+eCk0\!OZ2 }?9<%m@ pzOސDȫtHDvL,fdC -\qtsӂ~XZ>vW~e))2=[2sD?:j$AS|HBtx*nJPlޠ1h9ƶl):p9-|]UTQ;;w@?o ?2^S'1j7ك # .B=CX"1`K318y^K3ML4]+Qq {xyE暋I^_~wJaYξ6>m|QQ?Q|7d kg˸x9*+ԛL+F l~'^DA]MKNs#:p$T'ko.#2K/>9sTѪ=_XQ+tf'x- ||38M:$u@dpDbj.Fɬ6ZAғb湪.ɷCq2&_!&|W#Xu@!2Q=4}N OKO -Ipu -*ͧC맏ʯV*'.`Cq-艩w2$=_%BSX{Ŷm#YHZ҈\~fV7$ݜ~;—c she*ݻCCt9'HrsD_$t Kv*zǑ]x-܈0 Wk|5%]~ -V߯a -X͞i/.:-Cz+uشI Wtts{Y уn}0RJEDs%2qJ^MԽN~Fc'ruى1f$J!rQ6iɥ3Rk6-ɇK`=N*\N}oߍkМ mWTAZ*6?qBOi:Zy\]RQɳ3oѪ UXV*bYG%xⓏbtQ9eEݘױܠh4֫]S">.^(φYfџoɸՁzQFxU;q#C!;rd\edل8͛.RQ~? ?*iˁ>_dÃFk|` -w!*קZLw1"@S↺^7XIɚ=vU xHV!A/g`NANn9'/3@-62=S k:ta*0@/Yu'>>%!#!2{ʗNٮE0V 1sP -lď> ڙ_U/Iql9'I8%ۚt<2̜_ؕ@@r{QI)5(d}JͰOVeHN7ucÏmUS@[S`֫yRdcvU{gY -;V|9x>IZ#͎S ?,$oci+GnTz*b0\9\Av1ٚ,T.S{Z7B.3)β{KԿ鼿%1R2}7>ZS09))X84H2C¢q3*y  B\}ǬX9 S5/G'wa} C[3ri~*d+Ά%] s`OiCy38v@EԄDZ5LͶe2ӱ*9fQ~0kV%M7e3BV14}S#Pl_bP?ϺJ໭l"(=Sk#VgUdQc䩔k ZI04OԃUniGz;~K4S=.y,ʶNINGO'*>D^Ivoԑ[4쎋9L7 Ws- P:N&6egf`.{siV wGYs[=O'>!xc;"< ,ZGҽ mQ~|h1QTBjN"=[Ȱ>d TB)[pI?A%Y@AW۞y=oj+BҌKiiBzDu|B8QtLo>~n`=V,r9!.E@Bސy3lj^#Mrtq=gԾ+-$sZbkJBn:y'зsR+{]C7+Ħ0o#{؂Jlz9gٔ\]^5|2ցs_ZL:t iGa{Ap}#s"2>_X05(]4a';ځ꠼WYSOǝ'.(4N79: d{Ƒr݈"u|%gBQlݛq^Bd(2?aa u'~pU]prH6+Y}N* //etwLWrδԐ5P[!.k:jKݫF.m:+?SgXynr_Jh̞~=kk/HCi2 ~~8p4#5*ڑИ 4Ė07I -bũG",98цޟ3QZb}ʄK1saO3XS`fZ`uCM 7g/ -endstream endobj 25 0 obj <>stream -H W -?=smަ{r,-͵R44eM@vdElRsgQew"좨N3==y{~45 [JF˷ͲR -R󐚍pHqOAEK^pd_ͬϰYZwYuh3>%+Ëf6nװ trPń<إ =%%'l[c 舵}-7O'^Y{ئ 3Ґ (Yjq`XL2wdwPqaEDoL=^>ߥڦHaM;.l$( 5~ A[yzekiI(<2ZЅ9dED,pkK~Qo/w4m5.Z>ŵE,oBJ~dK29z!҂ %L-*ֆ8#QݝI}+ap6x-^_Z3P+.[F|ꘂwLBZ+o"r6qNشx4:6Zq>啡ʶ.K3B'""f(8#mM+"4L$'8'R`UP~eέ\V$lMcȼQb}e': oH }D]O啾ҋoFT3 -,PK\"\g -KW9gmc-e}}:X` -yUE5 HH*증v1OIB!diHIj3 D;3o#C ;EÓ$ -FCgNp9XdNWГ5zCD/tJ->N~1.Z -b<)%9Eϰ[iq{ !k lYugn9ơ R<k=F.J_vŔgkj[{[tA+(Su9L?XXP)3l~Pb؎ -M1~/ Ώs9FSB*p5Y6'f9e܇ 싛 -ӴVipcf>ůi:eซjRFn@g^mcgqM<HXgTb儗xxT(Gd .7[&0Y]r&(A jۡI#ӻn1,'iN /Ll}SS@O8%'AmCHSc+,9zDM o ΓܒxLRryx+]a`{rQY:.𲜚sN<=䴬}%YGa'-@VwcխŬLJpMOS*!5}&}&yİݱ$ے?{G"k_֨80!= RO X@kL|n] -X|st#WUj?m6dfuqccN9&9JJ逄Bsj%0!"gIbfv]}?|~x_"A; OB? '-sۖ0x4֪*5 R\WR}4ᇮew%35/LÎ?UQCds3e1mO4 B崰3 #h+0#1+q[D4|UELdq_*}EO -dMT-E ͵ N?Է>?#XnNp4izm -y393Nc;KG˩ uډ p5c?+HnyneMPnPoWC%1П'.w`ƹgJCe31Z`iҢ)UYf!d SV%:U9p*X{2)XFN;E؟uk}Iћ1$[Lq?Vs3 ُ(nu5s59,JPBΚz$h_AO.wc|yR#?Ql|Ī'y\Wd((1ƾ̧ -Jȑy(g@?R&?-[-ۦK fb]hVԣ/萿w?<*zw}[׏/8@a45jlMhH~ān&~%ojTK 6DG{6]ݓ Ƹ/ϵkJcjK-J<؋a,x=QM -)+A+ӝ4u}97iGƆn~f `2a+_ߖi,O;1S8@P5 k8Su[~d@Hr{|]֚3@pQPYJga8YydbV%8uR"oO֢\h?M#eBׄy޳n!=k*G*b[qxd$Xg2m{*EY5d4`9ض!f1LuYBz;DNzeUPu_3H|mÃeôkv5mdTJ|v>&˒TxxAغ b`}%Tƫ2ZomNUő{y;㽿-j>ݢmb, -)ަ_{B0WsVz%adUM[}>n@M[}c<ҕ$"^5rtm9T77v(gKQV[~ph*<71%ꎆaCLa;Z?onc~T]ن׹L|C*諎#9*NgIGj`N P*/62Y|9ǧD S?>h?/p x~zOLϿ\9(۪'ܬv nV:;~\ t衚F\.qPE]> jⱜo]i CW|sZWmIm{(F7=7 ]KsSĴz87c&ER|Cn<^(3;[&7f2hQT5֨X"4 -hlQcJJDb Xbl>g;;93Io P!!rG ܜ3$/c_a}>yic;^Muwi+zԖB7P3;1?W5S(z4Љ><4yr?6:`vX;).mfWfq)؃11&@0T"HFH2~zNk |⩚Y{{D-bdeА/gJpə0`#4]:Q#ϗ[R`2:CKs*Zp"v־:ikWx0C;^+JIC%~1 {Ј'E;r;qc1zT?O;=0D0{ +햄 =E5njjȸsJvJr}FCAh 6=|D?[G[V`o l)]]xW2}Fn >vU%0+2}\2kgm}Oq(zgeTt^6{VN)9>]MV <5!g(ߣPyH?ך_j$uCVz_ =q3&̵D0~&Nݏm&ѭ&y_RZD$Eg~r^K˨8f\_[o BS-z0Uk&8kr<\쑄3rD@`#= -j慊]r -fjR<E@OTgF&ʂӼWR}$5 5"/Uxu4GN )ɺ|=&r_lVChMwu[>qlm잫-<RNc9E@ߟJ 5=۟QW򾨾`s>i)jyړ*bz]nE30B]GȺ0ôܛhkҐi3dI48DEĉ2e{ 7Lڊ 5˃~{h<+UUgߖ:[.En9x 7둪 @!D Sp!Wj[ <`u=.}v(hy gc=UGakPR>!8S?@Ołt)%HI23Ue2CLϹPWey׫ F}EHt!GRF֞:0֦& wcY̳;edR&|_י}Oِ|WOU3K.:6/WhZ[ŀ;eĴER6i9jS܂֏2h+mLEF_i6aUdYMPrB6t}̧%VAQ\oigis]f2']I4L5"( EzFͨ)+bJ "Jd&a/89}}3􄖏*=HÂ(urr*k#*7a#j. ]PAL"<2)^޷tq'f&1 չ[2.!_(L HlFo0I[0lwȅ?I۟g|]@mq l1}jy-4簆\3U@;4Ԩ;r@B1SGĢ]9~1=EΑK1s;&s?.9}wT ^cw-ܷK9r39ҟVUBf |(dʐ׼3 -hD_aX{DRӰ.*gEmo./׺)7-@rߑY1l~D_Z3V@tIP9A5ZjMRˆ p)l^XFxp/'q(k^7WggH9c>)CGAżaퟧWJHR<Ļ+ܗL] 42*o.9jܤ$W)0WW@) u ';הU`Ogrǀo(nb1hYۿ)9kRPN(> -9Gf.9aZԫ`Tl/9ab@;a|Go_EVxȨR` _wɊX-!ayx5B< }[e#FROya= y MC+U)lSFXקy|kUmG9/.zoD_\_GS+ @ bb'ToƎBF.ŵ5Oy-罓5N6IXՇ.{[blKB2BqBK,;3gj_JzwGQ &!Wcs⸪rjD~%1le=5\GQ/傦\ -`iJh,nL/9P!sx ]ӈq`/7\BPIH脞ےжyf[׆.RQj bn@ -FuDMM9-RBgQ_F?[ v\? |myI5D/iv}s0f>:Fꋸ\WG@7xW$!UXtOܜjLފn[PC5J3Ł^!ES]S]qSzDE>'oL -zMdO RxSU}E߶D}~5:ƉI92rq#VK}x~DVS¦bO pAD |쑾&=)tc3J6Qˤ ĴF*>Ȉ98)sAH{^@EHظ,|BHC.K[\Bӽ2V zY; ܆yA/!Jb7bwUz p@.vAd⠞Rtvwvso`7w[93'_X_Dy5՜=HX谹3O.W*&fsrCޝg2J:Ow{f)έ)3m\3A@dE,q2dQA ɖ9oy|}JTQe ȣ`ٮ4z;FDWH%Q 3CfS^޴OW]r͡2*.6*{;3C ʣ&]rC4:`~S?(;.'-*-P7Ks3'/`wź+jv = :;溤${ejЍ=T|'/PP6ܰ!?n{"_ɑY>.7eTOOM<;̫!^uKU UdxTdHB@aV8+kKϵ,u[nwZLާKVȡ%wLUkN>'j2;=8nsֆWp$#bJС'#|h?04sOո'/b~C*On'%lCs".׫;tppuD@޵~0;;SV萴=mZNm# J^kph&9=DNdұ=JB㍐ -JM cҿD,zhU`r޲U8؟󸑍 k^:8c`{z!Ɣxd-!S%%wCU疘X)1F$ 3E̫KfYO "v|~ -}oYܣVc^,Х뢊 شo':~ C:wxs='V)۲C{K↴ u !.@GrSJ/{f+<6alװa_,@s-W^ꎘ]1O/nWv1W >1~P*H+}#^LyaU rIoe~9>_,QKt떗M-b(=5I;|O? 1ڸ&nh0ENE[w=}ۚ -O@«$77>7Ze -~=S{=;Vnw|D-KΫԒd8bxH@mx{cs„^IZE"QHO -k(1 :."TF?ݧ$Ϣnk_S¼3Id >[( 1d{zGufcY /hvR\`,2/GrBcն8L+D.9ɹ@;3fFn;'Q]C11ǥ"vTdCfRA?@nl1KZc#f:n,cor+=8d#|"qhD72Z#0a! p -ugo - GauU7tc*|WH ]ó#< jN-}%xY:~9#ҳAOn8 WGAXVQ 8u^D{NY32pff?ӣZ%ɣ&M\JTKiͷtXyW:Xy 𬒠Id}˨҈)EdFfXXM{HQbMrH2J@ΜN;dɘfb6cjL\" -"(Ӭm(; "\d1{=]~z {@?DtD`]|d8 u*wIVdO!|])BxU*;Vq+Ғ|ұGszKXp~"&:ҧTF $,a5 - p|+T ӀM3+0=cKfo5%BoVٴ -6T׈k„GHĘޔt2Oz?zzi'0Q)\Ab>^d$5q&$Ҙ%JNռB7#a?ؚl+J-`}-4 {;, -!喯K<#I%*WE2zx9z=wb$ȯ)M[㜘0|qb~XCDMb]mS{"Hh5)+3Rj½]]O(}Lco&L\ЍD=1붧W=*a" EQdTMi:23`+64]) 1 v`yƐ9xem9dځy` -NWĒCwoF,$h`ꆼ9J=ܞܜ>9 0);w{9IԐJV -0qPq=N.1by 8ZR2ڒnrK8^f-}#e٥UgF+BV\j臝/({.&yπ_ߚjp -)bJ9'K\*'ncȼqYUo451s/%b=WA@) -iMԒaXw -j܋ %j I}Z{wsv% A_Jh)cmɶK5J(kbZя}4g?'݉.Vb6.Su -.:ch{?.8IL%m3ep,M>¨;Y'2t'oE)03%@Z{׽1isyև&:d;3zPa"[dǜ^mhg&(/^%Rg^5U14Fh?Cy(Y3'S-Cw+͂Rݙ]SM&jU@# -/ -p^,b=ƫ5?m!pDz!5d];EۙvW^;%MrZdƃ72FV%]{z9qߠ`d -~ҊL2"68˨:βnMi%(FEwrMdW:Z'pʜבkwޑ+ho # ,[δY3ϔB|iO3*v?pnm2 QJV)%純,2nM,똆V]pCԄ_jbB%;=GE5Zm75ТqS{GOL,܇eͲ+ )Uv3L~h;PJ˵-`825þTWOx}LY+ѕW!X!ҕ_nz j(b]*yTcE1 61~>*y -sr.DpW93n Wvg+[\(I )=sެۚ6"[=d]9~ø*{g\&^=Z(d=p~В.*E0[|c|$B%yR;IN;O*u3"Lݡ51hl)a/lQ84nwglΌב0 wOSSMHSzV}#a^1srrQ MC-؞nˤNJ6uBrN6!iu"%ix!8cny2)3 -M`r,!M2Ҿ~zIpcHûw97yr5fR6(Iҟ2%^*΂]w^JؙM:;]S*6ʢxG^% )cT09.ӧ,OS55LMմev7=j*@  rsBQ{fl[U%B=!$֐; 5ȼo%XmJ5)6c֜Y9voywsz"989ϣ - Krq(;bw5wNݳ6!|䓱N`։lM.XǚD_*xac įgoG"ru~c*펚<+/+uS}j% I>%6Rt5$<ݰ⪘1&לJ$qS?A& -GJ(§Y1sZZVC!'{╂Z\RS99?,gwf/xأpVX Dp@X csV9m1)iF&'W=ro -6&k㋴GL*HiyϣFA&gVRcbw?a/& 2&q;u0nπt._v=#g}QM &npA6$alXf9A0BRxIĔS,h"F1Ϻ I׾*e'kej~L2n$^ڞ^709u5)oh6:v'kUrPr2V+@^d}j[(E3@opɎENiţ`ևTEoy %<3"Zf]DBr.a*gZ.$V=ƞT=0` -vڽt߷%<2TY{3OH5@ָCX%~5^Z2Be1C/;j6'5Ӽ+|B:{/͗?Fr(іz6ԚJP$ KuQH'0wE ͜K"Eiߕ] -FuUV w*YH<̧qj:#ڔE DnH),̋Xy m?>b\; ,G]wvᖥpk'NonNK&mtVZ[EAfe1 .Q2NnIYtH=2zn^N8e )*PR"gg"BQc .Y֧)x2o'\C&}Z´ EzMFN:xՁgӁ_C<=:.4Hq^4Pv3"|Bt_ۀe3 mYUtsLkfwBV}ҝ v:7f=DM!nbF8e)N6';cO'O}_vR RCi1hKNٚ_ >]Lg]z%#TנWB L뵤KTĽj^!-qA@z{1 ܟSJٜJY-s}~$w6>(I،۔Ir?qg' 4Y]-Ѩ }OqA$.Yne`"6Qh!+M>hxүK%J&IWǬ88SBЗdPϳL@N|P? :qmYZ/N[&Qsʥ?XE:l5’ЈVGA>j./UEڸ=%,ū[rN Om7c .=j?Sj4ӻҙh匝Eu3T0uQ3"6q-acW== B@P+#cvꄛv sv+v,=bܡixld6:'kHdC: wdpʤ]G2^FÝJv.Fgяjքriu;IWB`,Pr@>5'­I7&O!]x8jFNӊx tkڴX+[^zLSN>^~l+j39uS}죦褨< 8 m5.- jnƫds"(/okۙ];ᔯLj9U$Ȳ<ЕW؂Pʒs,JҺ/fIMoEroXgM~AޒFpwf5]ϋzḣG6Ź=R{ =L* =ҪrA'{p>,elL*P#uC=/PLAʶBd7ݏ(efƝPSQޭYd"W- PkԤ}]ݐVj&4[t@ۂ\1n!PclNyu;4By2o_;|N8cnrAKݶ(Iׂ!ڹqNqpi>>]@Tl /kΥʂ[cޏ &'܄QVulOw3+a=Jޯd|0sn2qJ3?N;;vfwn&ijFM<7j4(7/ k61A<&vg{|_p#:REM"܇YSOOoP6'F9`~]QBzrCj 90T~Sƫ7 R֧% -z a#k1r>W>wY PD"jHh\԰lOf}j~G8]J`V.%%(F}3sq3F>]%U֨ ;5+As+DO,Vy7ŭ#ݥmYSx`v <3ZŘ*ncqw;o8sCymyou!= -^!6n %82!*lLĜ2z˻8x = -ad%!6<9&G3ў}22aൢ6vK#&|rN-6#9|I;=lϸ !D~ʡ`}Gy2|U0](3.9%Czz }U:QwH/æ,ӣ!5u9Ok_>j. -o%(&E(#5VPy4' iOFAI=BP*רwvK^.LٍQ5ڀc@}:x eg7:Yӗ{ޣ"Ǭ#'zvpjw׎IEy;J+ U.2fԐnu)xQ|J3ױ q6z[F11VRIUZy&TS-7%M=Rk31Y!!5*6iu|⯇rbO9sI6~Wʣ{{K{Ks3oH79M"Ꞛ^>ٜt3.aEVׁ3udrާ/r>PM8]c'2a+aMڰ|7L:3J;Ɠ%Ҏb)g޴ӓpKaqEwe_KCB&Vl}?j[KyK!.f󈒓sq)#h~%Tt˨y " anG/lq{["J<ٔY<P5+iɵ}>S?:MZyy+g|%T_?*]紕<Upp̢A=q&36 z&% Iq;|#b0;#߸^=UH!-:l8t}F#- @ZR*۰-.\y{z;QV`eǣ7gƳ - -^)s> -W5Zrn9#i a M(i7;:v+QBMO -z!N8FI|i!11e紃?2[d~l( lji1b_:lp2b"d\R|9"ןɵYW̽9<1;J - vnab/;mݟZ8yˈZ~c_gbB@k? TjQ7)mҀ{BߢVOˆ&9?zeOl*D9Ӥ"m 8QR[|"<8R1I._Q=o)pxa:%ヅwʴ~rNˊv%BP#>duS a ΨEu+a/(^>DCsLk ƤL -JT͛=X'<|y%3bv%i?eH(Q;acr+ޠzM+*^[8(D௘渤Um~)f2Ls3uM(cY9dV\PzNS$}}+࿭3t;mjUL!u卼M$$ܐ;kk]]WAy%yHyAu= u:vwg|2swI{=|o[ -yW&՛ZhTԺy]hٳkt><4-mHncS +Z#%罴4fѳZ ]ݒzoOoVY/x6o*ExZod}J6=ZA,0KRyȦ'TgܔǷ^jg*?_8&)8zo79u$h2\fK; 3gU~c49GBU̶~z?ܶI^mVoK[榲-+7֝ L =ܤn%K]FTݴԉEvC&x.uʑX"* H//4/_`' -ڥ}/}n{'|Y$ܚVK7,򺥁ƬgS?ܭ)G:G޵5[6>M]-us3ėV04ǯCY½bl}s.hJ3V?`>]ݚ\r!J%N\Äu@7YdoQi6`Vz+c{$S? Y·\P|q`q9ES9=9 $r5pNNh -Lct2-.6>J)T:[b)D-N%`h`"D&(؛n NǓ[  eDN$\ ǰrkKz$WĔ?FS9 KoǏ8Ƣa 1 -%DRWS8D$a9ĔcU ˋ:x,zO'KK?]4"rtDYr~S CYp*8iX9iUNF'| *u^c B"Ʉ: M7g:@GGE:Pc< IJCMpIfQ3ߩ KVGDb&,8.}}xc0ꦍQq7emX>$*)6jԌ{)1Y(p4AE/WS{yQ~<nӵ]n8ma%c!;.=>szUMЬ?/Yn*򚤌5=+ye6a[_9),|c2愦gxFyaEwm--A2|&5i^|!}2y>y_T0j=#ZP3ypE>qXfMiHd*T%B L*2X 6x"d-},c$! C2YcZ{SK16޴޲~|[}}ӟL ~c̪yT\mHϾOl>9Qׁo^}9bꬬZKţ'Ɗ%yE}F(8(`- ;dSYյUB^[r!.u -NM^=S7A-Toр#յh`Gk'4]#KYIUy<硯Sv\ڦ&4ȨIi[$tuGM,nWИYBbI)k$ ],ac9!!1bsSàNAT۲Q5)M[G2n˿C_r I7t -kt=>sgQŜZ!kSјL"ʅƂw*n:20*z VhӘ -[KƽEMGn9J)6`l|XY0obPlOgl f!c&;k1fޞSo/hT0rsVudbo`n,ETWQxcN!1 Q%jT6ldWȄ5vcv 3$PаcAZ6O  &!r -0U"njV4#2bR> uSQMO1p -Y1C4YɹLTmo@fInƧO$gзطܚ? Ԟ3>7Z%RP YMO/$dAc. 242[z_Md_qMt|Ȥbj_U^8)/Q)h}]s@Mܺ_\P:*)dZqU< -x^s-k}*d 6 E*WPDV_ݦp|wQšӥ975T71zꝧ Kk?vY <~dҶwjvCe=Ld??׃|kgN+#F[/6Bw7{&1:Փb>6t&.5e94b WdOOT"&Q /Ne)nU -HC.642 7&~n?c -1imj9Zjُi[W +nxo~(4@n:U R5 -tQى4Γ1HT[Jwu~V7 -ĽYsl~^}Q@CpE,"8|,Ġ{՘m9%I#еջL88tӗmLIqҴ4v<1F t[s$㶞8s - VBBZ{hWdNj}#<3|gu|Zw>E#U1>սnpa1u|U07'sHc֭k%=ڧnˬ(^Hmƭ7ȳw'e׷&'7ҋ9YѴdV^hZHL{/6)({t鰶Se3T A sAacv4Մ4" 4h{z'ѼxV{}<+,;x(TokՓڏSJ⭾~{'ebVی[)YX\s>C?mO6|Ea[q;C'DU}ٰR g0DŽbҏ30DeyeW4#[4S~<9xG3#69',/_mXg0+YOE,J PYG0QȬu>62+nZMOqc;rQ -!3!Q[l%z -*S{㨊 h&dqmw&i#F:8lm.dVdf  !Y ,&O .*$\Ϲ uOzUR=np/C$ILۚ="#?ƭbܬ"At T4S+3/`&E!|DrQ#:g66/jt(bʖšEm:O++wX-efr>G:ܲ=3jOZWۚo/.NKt-)*ٙ>o#>L`&fijYKԦW _}pATx),ɺM}a34>y};PC(*f7bSM~=Cv&"VIfAUZU쁾oDǗyu#蚖{Y`h )KߤۿI- K-_ecFr8qՉoNDRY-~+{WOھʭuU འ$1ċ x'Eòl>WK4卐 ZiᲴī3 -"mRrkz1Bs?Ӹ0A}l=4 q[Wu7k"-tlSAa wgIr vER5\ ">60Ӕ_HU䚮T:sqEFlhqf?۲9"e6ccO|lx9h6LZj -ພnVA@f}Rd@Kze5TfJ=I>gIa*&܆^ATll2Qd}l̘9rXlq!64JPo]@+;iLO{$7 @u$hOnd&c/6&o=:ϫ$~`nWR-ޙnrü/4!rڭnJz Ģ: I7m<P$&Rޮ+ۯ?oec/B5{ݔ4{0w5Rӻ=^ՒbvқS!f\yjQVC,ʪgIze)&hʅ -£;/@/5eYAѢc'["H1) вhqXU{ _8-sXV*1xFRFxo?t VHbA -gZ耾4\WYȲB!䲼."A 0;Ϯ IDŽL5dtB2 C$gUchVPlX/dutXb$v98dB܊<[I! r*7N -|N7Cj2slm>;2˲~ՐNbb?1a ӄF4&N$9o>lCO 2,ԝbjҞT\ճ1lܦV}\`7?{D>||h, *l5 pB97r!+B5mO }~cvn## &YE8!]U4z{̟ڸ8Wmd&mfvZ;H$KZHe>2v&LAFHhu)`uvW+/~y -'E\R'>]꿐[w:]"NVyv߮Χ-bwN~(e1onurcdn +_ԕ YT{YDqW -!mk3h(dl]Q4I#R0pP`)<@U[ Pvv[51<&F&p֝tg]9IB96'dTDAE]tf/FQVMq|E TT/1YMjj@r ^bokqpT`CP IBՒFWux{^[rKNLyuL*lIG:hRYV %5Zm3`cTÕM믤K aSc?PĐ2)7f3`6LʊUV[xbPtHq̭e/2Ï(|Ɵ֒?2C&]Kw դsB؆s  UݪDTǐ\ʤ]o>h̃M~UY69:UMdtB+dSZ1' *a R$l\ ]w1n- "ps}E0{ߍd=,3+IxfKzIpi5=S \ 2|OY|Ǖ Q40Z1­5Y ,JĢ."j̃y6=2Ukzw7LmeIL.V*!2/Šqb*:i3)[Q! -8+a;OvegEV\fCUnINenm -{0K/u{ z&h{\bpڨLCnP[ ͊FِeB#%pے~DAP ^ܘi޼$$|h\¿1!4K>ٖOnht<Sy_ښZVY{S,T;* "$$N;tNYOPDq :79 @@@CtNd̋J:<4LH-/NCONM¬*48Z)iO9S$ -HY'+>)p '|<YKs{sܳ*H{cGw'Ah8= L-񖣤у_Kˋ/( ʃbdt:V깒uo/p~h23Xƅ {Q>`Gx -M;4$kQD-fuH aaGܓvawf^g56{SXJQӽzRvdڭSBxF?Wㄅ3[)29b.1)#A6?IzdmҴW$IU%n(i E]'S6USx -f 1ɹ\*_%͏.ޅnL/j UI\RħVnE/Cʺ6Q'g3My%9UkU ϧl_Vاf:C-5I@#Ej%gڃ@&p~8I -Zvf?RvB]. ؑw9^YDzsK*V֎=ʮYj^1S#OIÙV ZXf,z\Ρ,ֈ2B`*bx]|99j}ʧ(":짃rN9HJa\i9~{Mf^B,gFjg_ޓqH(G{ՂbXS8}D,O(}lrQQ OUCUsnm[)72v\ޫCe6]o̅0q֯`܆hW}g2h>`cՏ#Zm%aO7[-*Y&Kz9.&;Lgi: i!N(!!c,߇vV pRp `%KֵS/ɒ^]]~y>:0֓èv]Hv#8E,{* ",q:v3Yߏ\6o!Xf9Ogon0@OuYYYO7\hnmhg3WY`s@5Tv:qR5miNT̃g(h6fW } <Ǜ3o͞yxtUoi;hv9ZeqF&[հ<CA.0tY6s2*?Bڸ3;"͔}n*l UfGzE(6?[>'sḩ]BuQ[4XKlփ*vO?Оŗ7PKڍ@c{؝<82s9K("aUҸqܖ1;MS{? \rN1(Cgd'x6 =ʯ{D__h@lq ʙY\v172E{6 ]@Ty~* -FV9 'sj瘬^MK<ϐ6M549hnkQ% }h|&(h+]2&Cf]2)% so*mvWO;,tR3J9F#^%n=sww?f5Q]Mݞ¤0Hv$k&i8ுC\c9@ հ»ա/F`Nc">&JE#u'8~9v1eO٨nYPDŽ}ZLs+Sp74fڢlv=2n0h~ YI.Iht"' f&=^Hu5bV7J%tʃu G] ;dtPSږ <ɓKb -&LQ5>itY,apu Z 뇡Gj*PO{<:21eAF>!t]tY\$4׍wrqǔ=3nRJ$Fo>ig:@ - CaY&t35ҦN` vE/ aO[nt඘w1Eʦcȡbn&'U&pXn Y)Cײ#bhtUːj dƁK#'𖹞0 i |̀GS+70 tw`t}ˡl1&>u)9g6prV; ^,w%bTUB3_c^1 wy)@quld3 p}+3nG? 1my4^\blM%Ӈ#VadfށyK/p1DvܔeGۚtÜnV`gd`~_0H;yD):!'dw}\¦nbS tmK773Z`L^x+:}_e!)7V~TYSIY~5)̧6&nW"gԏqqS!_B%nJ'v mXH9P Ecӭ"4we!rLJ7hߗ,j*&=w|r? _*{BH-435EƏa4tu -wُtK:uBaUnObz*a`qT,:vX4vPeyERZ4Q/#/6/(spH7i[ƉcՉˊ%k ZY+v?z~Ass9O9!-Cn٤ޯ1m;ûKE%U>eѵtvXcD-e7v^=^3gUù&Oj3}.|!H5YcirW`>%`e ,%煽mܸz xq Au^ڛULѝ\jRpIg(ߵF|M9"yHH?NdHӉf>O{H'sK Fqu節'k,ׄh -`c$9ίӔȹ`D&e&솼n*Zn.o_qKY̺k~) a}0{;\q9Fv U!mR"Z2yW#}įΖ'~gqc bn-(i\.IbjUܞhυ6#ږjdl 9* _*r8"1|s?BDxm#uor&|l;7+7n(x} 5`Ldra18^~"Ps3]L=R8yU,j35*@U8;d7c_(pyN0MBRIt5[OZz޾}*f;Zo`!64Or:[H'&AdV)B_Kzw2EiZ݄a" /fУ֖"x9dӊ$("?V͵ -ѧ|pZ5:,fBX7]OdFIuo& c f.svW^@,:#%MW,Š['j%Q,|pUjL B[_--?DKѴʺc )ZxD)9/ބs6> #3y|ѝFO'f:!&og=ihdqa1pqg[{++mmxTaz!Zptrao:rCet:`3O`D-FJ)َ)7;KFNkJ*`Nsh.ۻ'"q!v!FB`Z-#͙EɐB:p6xG}Ssndiw%<^Hg<33/pVlhF(x=BGkdzؤ\<ݰIlEjm+8UnL <wOH̢cfu>^/NEF -?Z}e}8Ph@ǔr`J.+<>̧plbMd2: 6mqYbp"vPvi[]ES3D`v֋]EDrݐZl }i8[ȟ?ߒ^ǍWjk@m5j&qKd`?jGv -tSKfOyvJK+9?lF|uJܖi04W$tu[נG87"!Uͅg|xUOK8RK.{&s~F>6V~8sΧ[,Pٻ\FUq~Q=9gOf&tIh\1j٪((R5dtұOI}A(( dN/TQ[ϽwSa+zmr7e;3Uz[eŸe ̈́~s Os,~Қ{>~Noki| 2,~y5c>F4.!NLX'ɐ<}6+lXoczyfkI('AF Wߦʚg|&IjExv^aMhz,Żr> fFza U y*6hRnM0H\eȦKݖ`O6uCnUnQVoG'^qkqzi%u7e=E9<9%94iFh?& 5Vڭic|XÏ -~A9-.!0MgI[$") W7mdI,!^uWy#/n0z< Y}!ńVWx6=(„u-V躐ɘ - 8H ~XIY#aH-o>S9+ͣ%Ս_& #&4s#>S7}0r[:K\҄lY&[3/^zQOkf.KKYjǾ4dNf)mkG-&>K0.l|Fѧ.6>}~gՈ|bʅFǰ-!9`~&jF(x0pUdY!89QJ<b3OhDH0d -ph=.+؋sS-'64t`IMOa>*yZO#xW̨ޔM^zowQUm$uhZc5Eۡi{+>jƯsr`I֡\c2:0cBV6 dq&sub-6eS^?Zwd׷.dsnE-Vea^uMq#Cί^۪ -^]H9/3A#ڎWMEE} -]OfzEόMv `'q#S5}FIڎ5=ڎ2=v.ln o|86!D~˧qU(ew4aTӏo\V.*E_6R\'šG՘j9rrLˀ3I΄՝)j΃4s~]oE;(z`۲> m'Q\~X؛jZb2VZ6hy"302ɸlCy^YvlR܀`yxN!6} A΢@GkJYɱq6H^? #-B3ȒGB¨*& M -=i`Onc -@ݧ7fx/# ၟ+r!-GI ܦe}[8Ç#W4~UČ4,$Es.D( BNyK:MVF -8q9_LP-:թUeUK*ZfBfUq#cӋ)~'ؔ9ఔׂn^l:Ư\g%`Zƛ~V]ɬ:VКGݚy\?yEzU#-{Cv ]8[Y-ǫhv3L%u5 2^5^.WgפWY/\pkD+y鹜({#뮯svy v5KY[Gub $X/!M=YIiQ }gjʧlhvKZ;ȨNذ -Q9=0#zi%+EЦS Ԕg(Rcix]'}ơO9Tu6wmy'z ri2v{>F ,zͧ +.ŬS%|Ȍ.]/nɯW߂ٰƬC{55nc2ZiQv%T Ҋ ->Yyʓ8Le: s\Oif{uw식ݛl$KAzPTLrc&""b wP ry<9[>?R&=1aop`mA El.QJ3Ĭ#M;O~lsڎ6tA8Ze4٘&[|(0J0 - V#-\7,PhYF2昍@D4l O}3 vF˽ٓCSaU[Q.Cɬ%BKI."=NkT5|"'x9 bQHsLb2zG%,^dd/l!,KZ|^h $$-)zK֖TW0/SM{ӹb½sbtT`QJ*3#zf[Lbryz`2NM_W8x흡T -<4>!AD[ǛA8d{f9X}|@-4vʐq`M!ju"L/9\|kR_^{vK(Q](: :$h'(e6lA }R{.ߧj[ \Iv)bBu~dÂ.NLpgrbv1t]<ZȑޮAY&]?"Tw;rZi@M@ǞjR`vڇ"2b[I)8]Nu=퇫\GŪ}xU*^/$E ed%(t -b%7f#A7ks2rSJL2#^#LŽB(#58H).i[ /ʎj!m1wſz*5ҽ 8%Uχ߰jo >y [:9Vy}΄*HA+y0),8E[MqI1=d`{oqů.Z\K1xe&f 7GI/Jmy^ySՋ31GY{ۃp{-2kR(DÅݡdjӀ`+9]g5LmKP"`a;o~@e\S"\GW,O@ oo ¬bjZoO:@?[2L(.q\fsMcCܚs02c7ÖyU/j/n#L"(}ߚA^iCqhLTkCILI?ds`aAَ!gQwV^]ov $&~CD 9K]YMɖ]>:j-.v-煮RUe>A[aX}}]o#%o_(zIv9F/DoW~A*D2dqsWyIitoE8:$BDXzRn)&%gy:J>VV܊]KlvL۲9%igjNV{!t^d{)^8Qnkn~s3/ܗq,{ ( t2۪s`εk"#8rs;F ]CA8+B@9GӢΕ%<_uuG/vMfr RXeD؁P[Ē~Ɉ)?6Ļ G3d4)џ >%}[)j69N85j'ٍ[i]ʔ]q7*f}n?xin-hkgX0 ص<;Ü.t]JpwkQ7Zб\ s<#щ(χb"]5'"Ar.,?J~{v>"ʱ >Xܳ<{x8IGeюaRz'&}o< $NRFYi4Vafݜ}:de򳶻a+XטZmN4DM[6dF l1 oo;nRA Y>ui 1!N $$M!\cTTğ#k#'kҴ;sLs? `v#@Ί?t0a e)qGS5ցbv_ܩeh( ץ|jEX{Vs荎 `y+f KPsgYK%luPI[2ft1cW[=Ns3 X? ?sEY:P`iGmE5~;/LI0PiB$) -UԼ\b}HSKE}DͱPҎvŮ1bb /81p!rQ0F],V75U].rQ*ړ# q(S/@,a"^"\pK K{E$㓮NWh1N,łs.skxd?_2"{h!>C '~FNo'zquu(S+Ⱥ.tD{V>]8&_߄}XjDy%uuc-gpm՟iI{c.r>gZ?-"@RYN3R|CZUV#=UNO>X%'gQGS<-@6bl@8gs(FAyh9/d 38G*&O\ʪ>Z|Cxvcp6T#NVrS/V׸Xh| {DB;i$8g 0"b -endstream endobj 26 0 obj <>stream -H S۽DИ`XMlػ"" -"( U@D![PAD@bÈؒ'|^}&D&i IMb[)S2Ã^Z6; -v@7c%|MHth_e+ J?N;@ Sp|j -1^qC7~[K-`ϳ)r⩌h>c1X09NR݄ICh0:HwU>Ј9`!` 1{ģP 1.\\ aǸu*koyo.¡&$=7V[s~ '͸\>\)")3Ɋa+%/02ǚ))1hoYǰ!6=a{~|;]#F[Wv*JwFNNrgC#~ٟ$f+Yy{~ S)@STq:ͱ h]+*gM6Qˢ_w OGN;.ZDNd m!MJBF ]MɺM1K\ D8$v"MZ -">%f85f7v'*` yׅ2Z'J8n/ 9hiǙ䈥RocwUTɍ+,cu`!BUL9;ev6G5=?߸^eV5v_xYB'<[&i,AZ^]^Wb;QѪ\zw&>,nas"fN55n]OMwa}MͅRJics t.I>ÅE0/T\lE8T빚ma*rizkѢd Xh?;Bn5& Fj(xuEyCHUmi2o赊 6˃MKu32P`b$OOM#nTy9^ O / 0PgW -Tw$ 85%oY Z^+p x0cWV"@?Ϸb*lSVGn xwԲ g}:P28?0/"H+zzDuSօ<˳J -NfgV&` 8nD?VDt:9_T Í(%QFT#Af''m^r䙱m9!Zq|gH;eNUZ繥r8H^i-hC߮muaQF]ɶ˙iY>}e /~fWagg,B5[;[Tח)OWך&Z&TIyږ~"C[xj WV3z2RMLo'?6=W+7'ܼm1h<Jj%<7za/+.bMĽUK wi96UR[s4`o}.JǃەȣqRe2[3Fnlԡr2ζ+JB,#S߉Pob FK*\NS0q>:c!EhS2!ߗ]E5dQ2/ I*V45nl -4m3lCO% js{=p>b[NK%t9tY[kӶ~[j9I=c\ig‹iJ)., -Ku=Tҙ4I%fhc1& ADQ@EfQsAATphJ^]뮳<.M!jf *Z45<1 (% |esK۰9 z/˫SdUP`T%y<%mnuFi -w[o &b쪋$ &P&Vxk>p 3^Yg[r}w&ѿL-N0rqkG3LLݴ|ݺ6P@yW\ڏHQs'/L]- S!쫩9Q;\o]U3#ZVEJbs ցV #uIp^Pqx㐆9)+Tʛ_ w7f랺͙.YsƩ)KIr+b>O˨ޛhze9ULJL_kZ"vg}h<3ߜ[c45"2G{0Sewk}@mSwa馜i0/Abm!&=*J.*c^Z;Kc6ӫfFD~#%iJK1Ȫc3bC5%&X.@l_w59Jf4t+h ~%nk9YpK.9Xx4,~;2JID؇䴜ZNRs+zve~aboҐ_4DHG,۟-{W]gX(}_Ѱlh4!'%+$&uuKEk*52OԨt0b錘 2l9:Bj E4my\[+'CBZڧXa[cT^UR &.6faMlKEm LǖF6x&aގ!%|򣹯,A+x #ԂEWuP+l)1&-%9+(a1,2. Xavpa2Z6 2d!x@m0~ yd3 5!ǒצۊ_5L  V>0s6QO$ V[N54kbA&Fd6GwO@C.sN^9-ny8h$O mpTrjg|D5qa[>]zLlSAٗ`Aa+0+%"ͿBlc|8y?]xXxw -`?+]nQL) DgF>Li'yj8vjM KGh^q l,aw%,4t`*2ef!¬ e99׻DqR?ƤIJV@B/.1k} -^\gF:9Cz¶=)$BOTkSr|whc{R{a -8<jU<"ѥ]אOw&/Vۓ5YNw?ƿ1T)Z-ZZj4EcĬ+Cb%!L)æF^%FBJ{'=>O?יUڎs;ݤ%i@IxzA@YaԼBY.'٪xp{֚s*IkO&dUwo Q'4ur_t2\v8nGsֺ -csm5v)H]Yp~nK6"X%BTlfCyJbTx{>K Lڪnש|<^z=Tsj` -:A *ΰZ[+N2I޽(nv勞;~ R)l-è釪S~÷; -n\G$ɏ6$<]aǽ5^~F +\]Mt!])JI|kF'-reo>yv^R*Kfƹ9;5+2l*tyYi+=oHkT2}"NGaR8;jV4i䣦z,k/gw CmT}OmN'Sit S7Y1o ENbfŨxo;3i?9Uw[]sI+fc~}!4/, `zӡ˱J}6,A7rC Ҍo̫o>nV -`ݦHxk8\0vgtNP|XC_ӯf%JԾ=~^H<ueX7qc[a( wc?NƎ۵Z酚[tfܮH$Z>~O=.2lw {CԤf]ٙ{_BNǣ)Q?_ >V39e#E=$F@~BߺoP  BOhFr>VV/46'U#kZL|3ʣ*(OeXU'  &! 8ݜy;~gVJL{9'%N&yx}/=NC0 7{D32&IrSʞ7`q_,cc ->CX qF l <^P|ढ़(LjX)o]l&#c(Ŏc z+i 2yb޽6xq[j!EwuP֫Hsl욢o˨dh;IA>͢䫕-ۋp3-*-ᾞf1!P(-6"zC\M5}-8UkTCAn?`7:XFvJR0/DJ -qaf!΀Ŭ*%AAM{IyךRӢ|P:P2MAqa麋` J9`2 A`KZJ#vZ"$!? jb^jČEǨ nt1~UNgŬvQUNb jN@X%<_<~8 -? -`&ą?uR"=Hߪf_j,g+8y@EFXFB(5/3.?191̶ b:qVZ?H[/jߙtx{R놧*#@9< LBxanv vK\;- uRFj8~Jη㲠ܼ |B$$ Ԫ+p׀ [ ` b߃ZԣZj)^.V' -ӕ[ӿ@r#"@ p>;9n!`CKpnk |u2g;7N$NA-!Dh,Q*"Dl Y,^J;VuGiՖm_;<r3g9s6KƔsKKΩjL3/%B}Z:4q@ 0* <(8xEXDݜs@%9q_O}qv>k5VuI: F!G)9,ʅb&42PCx Đ!9? n%~~*z-yH=a]$9-jq9FP,* `C?|1^pO CXzep|q==] f{Sod[S,OBD !  -x!WC} `7}e{ݺvgV:NRu5 %LbBr(2_0 dF$)>DB~0<Nj}L<ޮ6\o;kx;~S*DK٨DdFDE$"`'A* z$/t?Njbo;IVfVSׄVs٨HHi \4gUm[_2۾E}1zmcwuk,ǁr/D*x9f YI0@ij -Jb+ =ēA%]ܟ[2#tPFGQ758,c&CVlp g-F)y[+H[+!ш8T -FAa8"D zT; d'pCvk;bzkhyVR.6O~52e84ZjfBftYlH~ d:JQZW)'3" 9^ -x^{w/hAs@{'j٨e2\Uu%$ii bs N bP080Q FַÍ?_f| ہIa_=5Ku(;(LmzTR,Sd䐆pR@"ߝ J0TLg޵}]xePjY/8\ϟ`)B72__ <"Y*!+<h-r*+b՞L80|9r>WLK}b*M lҀ7aNhD+:j]s {kAֺ]wZVF Ax{!R]|Xj' IULk׫š9GdW~5u=YTm,={yȱw+N60:zGYAV* z+%8Cu5Z?m;sOW:9Ilfmn{ZaSV]M\Eجv$dEج$ć/B1 *xڼtZw4,in4ԯݖ4&+ߴh7ƌHcRc2! -N:h3y^E5^f_#Y>Z(7xd}6~X?_L8[?;-]97޿E% "QNwj+̍q#p5;}a.).u__ݛ-NCi3j rSI%Wd#g %/"XP{۩co;L;-}dz?OM<U wʖGc~Eɮ =W:k9>Y fAYN' -2QZ?xfgbqnj⠏!:q ~]E5G ّelAܖe=-n!|UF]XsퟻeS'6g!!KmG[iϦbҷ̓zY:4 G* ovezON(Crq jWK o@f+`э=x"D' 7Fi]ALJeR:`SȞW["#l% bŚZi f«2s %M؇6|oݿʺ<65հluPӭctj7z|jX ,WPl5"++ PvҋT\<0ءa}ɟ; ;5/9GŁu/iVnd[HH#n-,\f鐉´#ǏSy +d̬ |qwl# i55ALjdD9–9>JѼCDFzJ[a.Ċdxңb M*N=a[xDo -Z%<.[* -B4T<򉋃(^`cW9Ԭжl`-7.wE ?H;؄nv΄SfZ;!|ABuJMռrJ|qn{$g=.A.p4"Ʌ=_u#lziZ1J^̹ȃ{S'i -W!Xy5x]Tpy O\0c]UVxU$a.bf+dWmrwƪ!'/d,+K&p*M}Vq>."^Ԏાa.V}٢1qn&`>!.U}I%sgfB׉ߕ_,ľ l pG+rEfu''ݱu/*{#Bu)"ӧ}k^[yI]rWllbM4ted_y@pcJ벟Ƅ+Χ]GY1ҥW5*SRW2f*NRL)gn^8 C̍ipz)UHٹʏmd5)Ϳ2pϽ䱜,g?pF` ;X ɷnh#Mӷw-ޒ3&[=`+(} -m#W` }v9wY+y)C@Eb\wd;)X&;b'zDyڅE7kp>0j0}̸ؓ# 8~͘ 'k褽)CaTr97҆~b2;' /. {c|r\#ŕѪZє2M/ TE,Ys؎2q2͟cZ!uZ/A e00}|HLRMNз -W6&oԢ:^MfJjy?VF+̬vfD)$4$Rbzwl{/1%20a`cq HOͧ{u?s~f$+ޚ0F -Q@w6+Gtā]8iδ4FTiH1-aF=cwq3 NJ7[@&e|[nw61Sq883!)~/"emsO!Y/8-ykxV;u#㿈iczܳ7VrCB#f +kp*!a 6< -,KˬUL?\7 SR ~@]"l juqrv@n{DuM  -~٫xze)$d̞?ڠ>.:y ѹb$(:Ȍ,f]r(ƍ{c_:Ǿx2jaljugq }7}98z~w!nDI*Y%I/gkiubQ+ -PsR]k|mEcy7ӓzJCFilԎ^%hU -/:S̆OaG{) xWE/nϷ+MٝJZWFCi5"l_I%6}M*kczv7]Ӫck"+2Z":ZcHCE7@ˣr%I䣐sLQq^TȌk1wj:nF'TL|l:cvꉡseBo٣x&3FOʌ>e@al{|3̺&;\TTp<shF |s@>5(r I]UK[uur-d c لČO{[΄TN҆Yt1ma w~h'&q*fމ&7s?{2=mM8uqGO61c"{X0p$c u)I+pɻ=dWȃ <l޴\Hkډk8*d.r2ߙYE;XT'LbceՇ* |d^hɪ)OCs)ŴٲwiMNߡ -wg_wmO{ͻ8ʲW,}_ATij7x_{|lyIr -ueg{㟮і-x ޣ65Q|*Rs1MA6̢lKOfG/rIP}JjMZ@&/~K[FVH}m|]j2\IYf@lG":txxmʣu~5)a \x]aR&Sp4n^5ƣBW0yWn`W&8:)glT -lGH98s ~ w&D8}}4< "#ztW[}④ r -ReX9"Ww[bقv _锠_F<6<^&5]&CỈАj<:Go@N9z!ջ78e -c^MwKۤڤIMXI>FDžH Y.9.du0&x:<ԯ$T4ָeoGlɌ!}*^~-Kk,$,-sthXޛQ =abƠ -_f_j2U~s̱M<`O xtKzJxCV}U.!-ÀOR,ޛ4y~tPc&^{n@mm۽ dYx$*R~+8_}hj+VƁޜ~m_uze"{L2 $C -3TEd$<3a>쒖_FnhnڦZ3[}Z7p%)}WQ}RZs}|c瑒J>\='׹5Q/B7O}gW&&A2\}pC5 -+ /]Ň L֟| -j&ھoᄍg]-vfnnJ?gP9[z\3 7h5f➙w3bq X wR|C )WلNyׇ LT<Е2{:Zc:2>Y9x7K -(x( -Z'ɕ^9ʹF.q`ͿϽ;̗>yԔuMi8j /T#~ۀN+Yal t53DT4r}"M(%TXp6n 1 ḷ14qpP]bW-?r&7u4k&ǂ`6j,5 m|9jWۥ6M09-ti8S33-eZiYM(;. [ff""(*. -(nuuz_=BBc[ţmצ% mŜ+dHٹvd]\ 2@/? =_.ηN3 Irد?vENr?2q-(i`пHҰVTY-BxEk.kɻfh5A雡Wz\ -VŪ4PdP,ߚ G,p &o<3}uZ.0 %SRԕyMD~q__Њz%f#|qr#&gc~Bc)CE!r~<+'η"-Kc쮘k.@OZʼ:}욪NJ7e}LKP/ islebcŦra=SLb]R&n[F_T0`>4Qf\aq7~dZ}}cP^1墱צu{u\ ;I'Y>=)FE/5\Q1MK}zpR0W{~댰$G[W$\+L4<@yuէc=NɉӅsyzxy(MRxH6F8Y_촋lܯ'L0\ʲ[=wL y<7s+漹 j9D1FԬjrh}3~jĭf~sosRYF}%Y61UܝmlX1z!e>f{UhRP!!SBGnu @c 8ܛOKf3N,%aoIHiK]J·eկuTK˽#}C} SlX[]xk_]oM鹧Ɩo[to7r7&ॐqjULJ׷Z%&ƼZD?S -wPI|X\ & ,#4_ՌRCG3?C1 6l]gN_an`t qea\ -RTO.5v8]  BSy(l=/a#E/ )!`ܣt `3g -C;J=-(H*]$w}Íc# {PvT.sI}zsT bfJ8f4X:JɸG2BHz}&m|.yln4VFM^|]j=eQApHQu7Գ>>ƀ8.*7A T&&gC~<5Ll63SB:GC(X[թso)eS|2,!gn -\7\7@5U^d]Zܥ#k܆f.ĺ4bW{(uZGvOQ>6X\bVńDk6sWAڑbak|8wԤ<֖|t<x6 "^h{~~gWNZ2X?HJe!c1wvGc'⑈2@F?mJ$g=?NO`(?6kUORBSFo+x5)Jw2>Z;מu6K:16w Jb2|[^ ّNU26G0 k}[bRCJ|"&#gGJ>⺒3 (tzIB]1V^ꯈBLgߑW;qǀN,쾎M0=t@Oye~02:ٔ/?6q^= TswՌrAؼ5I\C+YhA.t#Bj+@ !g?Vۥ%њ6ԓ UP˴0:uWNkYRבL~ݝOY -k :ζhP U3`4r)!70|z&ɧI 0N[`҃zDZg"jwT41w{-1ўe#/ N_2f/.-*omv/k]Ce'bOOZ ~%*l'WlL< T4>:yA=τԬE{! :Q~دnee6aO^m.'L߆OD*Tx+k-?^iE-i:5 T3ݳ4p5HAܩ]43VycsXLu9a W`r]U{'otݽK-!S$f[S[AϐdI ʳD ,~aD'nW6F8&2B}tSbgn c%c}69{: -6ҿ"jug\t"`z*bwʢqO#x*з?s1sg<.ӽl-ЖQFm,"[CN+ IБA虥lҌ=e=3Qx4 5dS#D Nc.3bzZGE(N>Xn_Dȡʘ4[߂w̠RJ +S7'YQ|ů#Ȕh> pQJlMi<lpn%?<5N'yӨѽ(O4X@jؘ◳ўOEK¢ꫫG*䐐J<׀ˊs91^w5^ie[c=}5?=R.Npδ:>k8UwPU[ . -#*fc@jݔ ﯏]԰37a73[;̳ $cc{ߗY^ea< [_u ږ5h>_k^pL2@O Lcڙ#幥/weqTm}e<9M;/⒟lOE96SPV*:lck~ M?{jXa3#:xtTn#ꞔߙ+q?Sr.7hiZ0+ϸl2P)&l @/HYZ=wtggJ{RS]ڊg6~W${d]aSx.d(Gg윝䘓iL2M;1 QX("EH .V4 -"( EpЋ(d&|}}}~¯n/B*zUD%u9y*_CzyWlk -A1/3IkȻ Ȭ*2ۯj{h—ƍV -t&LtL Qc{Mx\c-wOC*؛#x02 BzڋjRoݞC4[B;W޹׮qR;lg>^{ggQ@C~F0ϵ5i` >7T[j9X¾kiuvplCʛ aoea=ӇO@vm~L@nA_w`K=S%`~nj\m [{zh 5f ?uEG veFy ~BxڅڬOaO_q7 _e9ԭgg0Bb&>-n(Fyj1r(vSB>NS`mČ򁘱K7mvc>=ԁD?\'UD4m>%o r}TyIXr3PdNsԢ_Q3 |n-pVGdxq03<ҹM>~jgV'[6Zs >l%\`fjOcaW;xre-c+ ~2$JQ*E\SaդIz] f-%?+:`WkΣ56*V&)tkgSUҚȺ5TFɕ2!34vıKZ}JiP:y|0;Bzz_M.t>8`d4R$[=zC9*R<~sY =n_MWeV]@m5b , -?2k9 #023/P*.^a{86fO)C_5W<+tm/ tMwNܩ]>yjgVLYH[yOɾw⃋ߵ():^s{/l쓛E[os5-2"*9O=O] 'VE5PT^YӜć vgd_2`k7rTƅYR{jaHQ7P-@#N@M.M8``yІZEh7_k!mx~A^ctAA\Ufy~c db .H)𨞃 k y!M[o E,@Gj+DAig`ܢc>lGBpY󙟣!pC& -)6P'yCe{O;;GpǰY7W: /Skxg($eU֔ -ia}Im:fEa#6qp xK(MOX &]p ="2|ߥ U߫NOA6StܜK($V=;>HO w/c&gpґ3\~5"7UgNQwBZqPor`$,\0( =ձZRg1T:'qP -1iZ45 G.Xky$9LeE Y}fE\ĩg{2p#Fݯ޵<.#kIJ}TSu~c\\f/駶.ip6J(>Тrw-ڌXqh8uݣI3?32alOE݉.y%Hb\'&@%Ҩ8 - /).4ϙݳL9iL4hƱƊQQAQ0 -" -HA+QDƂ JALvgO_.ދ{=yc4tWՔc-6I`샌tO͠K*.$teVQ - =ۇHYbCk-k<`K=:x6uOEi;K*2.;\i#E殚zń%@/:տ@.- |">}s Nz{a|WIk;n9o~J ߛC<Я::Bt_YZj̍0LPhE`]jU|%n>vUEZs/i!w[jn=rۣ$NR6>"c;g),,1H˼_ G74Y-g.OrtIeG4g5jWHńF\#0uI]ޡuUK@Qr Ԕ(#s64ZJɹdB}.ڱA=6oOQ3[x֝ԣ%jyDϨ0:ĿoYǟ*m+IƌlQjs Ƚ6C/J[*c[`)Q~xay@M,4"p"9c=ƥ%o+ suiN91: 4n_E,bMK)/nRwT3~faR,tldZP> |DL~4 -ꄢ6>5L0]_J;=Y̤L&ND$1Q *6)R4"N06DDTĆJ5LΞ?dv=@zV崸4|QJsi2lU~o}ȡi.E<$mh{E|􈁋;\ޡb -,}@/`vb~T-ҧ /ԍ4#- 16eĽhh-j%e?mp]3:fyw Vm,|#B\*9}_KJ0ql t z\ilk9XYc0vFn+p Qrb9}{C^ m~(@@i`V?owo/+IgIr¶POOĘؐ.lDm.7©E{EOeJ4yfIs(U>=.2[*Ac::RSחDw] ^P=϶2]^w020ڦJAc$h ģa&U^U/y,{#rbl B`VǏ~;am 11r$"EnKa9%RIJ;>~_ ;Fٹk߉Qƶ`,܎ \'Em Mh_HɅ,~E9o[+Sfb g,o[tHJ椸>\XU,pll;f|ZR~r(g dzzjvK#0"(?+@/$-ye4/H/Lqh!>"ֲ *%'&+Q{U%AZzPW Ø5YY, u$ᦌ`do`z΄g$L`m1V6˺G4p83 RWM]M l]^Ꜩ9!; B|c*{M6AkS,zbGV.x^Q S .եb`vA/2`z0ۋ=H͟wdl7quq,gݜf޴I,w^wn%<ޛC1o3͈@CvH,mttuHvDi1gGUW%&;d+W |Db|Apw @%O (8wr_i>m!kب`pћ^X4z@L S^\l/}%$ސ\ye}lA{m]Wfְ9DNUUMPIV$e]Uqn%N?62腮S%~JY1TOJQR@&IAj<.OҵV9'(8Zg19d;Ɍ~b:v0'#r槖iZ<z wC'Pb"4gnboymȠKGZCDPt2͢ -HY0*htwxamkyzKb8TY(m=W+}eoL')㗒x7벊d˷ӭ~=5iP'KMѐ -C=طZpg>I+SWe5=r6c~-hU !kKlU>6@)+iI"5/h-vSEUrjYTv "c\kscm58*؂?540Ofp -bm9 8[SJC!N.{*S"枊_9DNkF>y1Z-k'%=^s,*|6/.xb, )Ov @5Q&r8Zv˙S55Xbf=:Ubij A|x^b@EO>ޥݝIڙLf$8 :~6}1`礨ś:)^-a斒h/Sw&ku>" 9|08b?][r`J6r;1rKIL)`1ʜ?],wg-"TmMإnj˺ܰk=NLBWdK-n -(-66cowz:p2q٥ ڡ7P>.(SJp)`;LIS:{(p_SsM|tET֐賹:ʉGڝP*nJ~[ޢ8]d) ^j }z:Xܙ$Õq^ZaIzaRnF4 N慼m#C-y/5Jw KRӝVP[{]ҷ=+yo{ZDmg^88͙nf9s氙CanrBE"I7T*fFrRKV$"Ͱgq=|}j:_q::bw>\nY' -vyms]q4WGښ۰! Dm.}hX!'Ӷ VgAN1J"F޸Uw!gYT7d)mM#5/Zn]3ɮ[VḰqTvd!OÆG]ˌoLE-s`aKmG)7ĈWׇg̿ۥ lꅾ97X/J#|k_yUUZTxtի!V4vXFZešt( ug_I[+P3 -$4я$AEN -#aѾ0ҷYҊ$T؛@%U1HD[]$A*|!~_EpO[&WjҀS|j(&*k@ Bh$(Gvh{a#_ЀG#/= TFo v|F_ b]]IW{JBMA5O:+ -raiir,n:4EexU|l.]3[b@,ݘt`{2Ji@#5@L=ETiJI.*,ޏ@P5pb_Z?b|+c}φ'Q}h ץ0ϦO yo!^_y~Ji16Zg>>֏;v#Ʈ pn%>}s`f;Kޞ~a֞,r*h, ZzP\*varl -#&xKRgܥ !y ְnCΖZBM Z.6S2j  Lj/Lt쥅tIyal+G\{=AOf<˪#!*إ6\Jޥ0r(+WA]9HNr2̭!*TZ ~j*zwn*:S3ۻ9x/X9LJv  G/gW$4 l*& 0IkY9FJ:5q/mܫEr9EHwk#hr ,mqF:0s%V -5O>55 .V3#vbl8f@,^%Z܋e&֫e]Jڇ}`}VL 1Hw㶆Һ[r\661J1aO!QYւ~fM6o W}5 #faHv¥UytW/`;-]疎:֡m=9ÅR"Fwe5-flcQJ}7#ĿwlS5^ _T - HLSτi)ƏG|RGYW;JztR&hHDO1PcUF+>__(i 7[etQ |k|]^5#hQ w*H&~*UaDžQTrc7 GEkc, ;[fWi.֋Cjb(77P.::IFn䁪eFY瞺VΪ 2Ҵ'p@MPATIAA@@$hok=kyl9Lcڞ:}&,t#pO3asPчF|ɮy5ڐ>D{<{si exoIJMes*`Ps>`GçZz 62@\UfI^dxK5;h e+le Z䱷gMz!omh2tO)  ^ec:CމV*rK/sHO}JL殖T6p'vWؿmڝ%-s'6 -Wo  GVKЀçok巷&YU%!&U7ץaRgdEl(:  :)!ݛV9H U8jSM c}cYZE#}3moլ: _@􃌴n Ubf͈0'( w('1&CtIvTp_]2ߑ3os (?u[%]fV5¦U'var.깕f*ܣGjFOJ]-zMjoCEԤZDp=sGLi)"팢yGO]m m(L9|Iؙlzg(cC;;ՍL?!"z9oZ~hǎEWf[Q<(Erccl91ͩty~ -rJOBoV1 -f6f_Ǯ`D9W NMRR/{`q4ȓߵ¤nqUq߉E;05CtMً=U㔜sh'u !5"?R)Eo }1ťwO3- EgLi_,֯6QۉU M{FdG>qT:*٭;vwjɓ[`c;"hRUb@כYsDJh [g"u3Ԣu2&)f_κqjj6)=cc+ wb0 ܡ7tbwcn ۸JIHRDޛk 9!pr2jYr/Y=5J'82l0:>-ǧ =#" _ݜ {9Xo$ìgAo6vcnݾݚF(Rܓ8WG*_b`l ؋]W𫷵|*7Nw2LC:?ύp __{̣C]rjq^SuħDE*0:C_˫Xa VQ -ִ - [XMA=F@˪@o9)4ؒ4%<ܚ [$UTYNɽ%ISk- - whbOVZ^51uȥ 5'"20&E<!OHGwmHC |usȃ0k%WȜ:qy<jz]a21@35%&TxC 24*]O^mLJ,r~ɴ7Fx!ܒ.MSk#/ﲹk憎Mnu;WxCȸ dTg`P/:bI@Gx&cBv,5L7K%wpSI?q@fػo/wG% ~{sew+:Ɂx3窎r\]SEiM7f襀/~i&ڜ-3)ЫZa͓  ¤n*jWN :?l.85RieWH<~$xIT diMx4JƆDg]XDƭp>x{0ic9 HY'td `ndK]F.:$y+HAJ;(^%䲓Rw(ȵnQ[X Kg:&Za5Nթb[6`gmA<Ւ#5ȼ.i.Y$8sfn59E ƽ4L 2Q3Fp&bJhl,Ke fjBJ|nd% Vo_fjX94.q&d¯g=8Xc~?BHQ`9p0_WAKm/3:bg\v[ 36lNr/T-YRq*BY Ə}dzxy3CxL4B$VYO:9bzG N$kET < -v+W>-Y/K]5] ίw{}k5?@)U"r [%G`g,MF˺twI(Hnmg3nr -^%mRO# -"YoǕfXљNj42Xi/D9HX RP쯠87TbJ#۬.&[QF;/5|Z"[_z~瑅 ̍h?Yt]J׎p3;ʑA9 :|u4o*٘=9a|peNvIVTt֮BkYSUNf߁%N 2̽ * ;vȩ}xEX+{etu7n]ELZjhS} ,@] ` RfpЬ^ ;m=|.cJP~9'e{qdt_P|ჸDqdBM#bF= 3ՠ9t -i[Uաub cJg4ёٛݪTqzрA8|-ytDZ | ] ~;OHj9KMSD̋.|^VAW ޕa<P(QHh8K~Vu&*abyT~O9C -{ 7Αk5yY-W)Ea'Y0t5rP }PzH°qV@+gqay+6uĽj_2RkAs8 h+Bm#I6o>EoF -^=2KfJaM#mfuz4)Was GF/n.%5F[s\&>m$VV[UԪFjJUlm#lr@B `0m5>5_l U*mz<='6f4#o*#w<̛ؔV5[ _g_pBy aJ5sRFThDj?w=7oreJc̈xg$9ZkJY*AϧS ꥣ:#!NƕQԍ:kń.ePzưx20s -}3)’jT ȡzV7*†C2v3/ Ζ~zv/:١J]plx7R"^/cyyj;9}rI[ z.LZQ7Ȑa<l%gFNNFv \VCNv+ps+~Cp5.bp!%H~ֶcnWJˑ6*} -Ao8jI7N A,vNFȭ鴓W&)"uu$М&̳}P)*nZ䘋,Qja5οؒftlxF#B[\NyȠI*qw2VI.޳=ihK ү]Xщ@h'Z4S49'/k)ir]tdlOfγ>fހ't;1.+X%vV,꩐23QY,i㬗(N *͔?-=(p -ZҪamVeOI1 =*Hft(wQv c2>"6b*u-(XHb/F= WneGY6X3djd[Eݷgt8ȣ7J^X̡[4 -_ Ys#fObzHhՈvG==&ys$%j1z 2nvۋ1оIwƛEcAT6'?AʙlORSW%$ϳ/f4p8ôY?t<rQEbjme$]FǚHiln۞hyABLG~DԞ>],91M/Iu㔀A!!ѹRzQ P"L 0h \uYdo%`s WqA̰4xM.IXC<(7Mo̓_>=k619,nb}dLf|Sm4{Z et͸``̢].-HQt*V߱0Uoo,<]\ f\T9r~yFP.`y7` Ei{:QQbjlN4 Q -Y"S2L" k*yy~y~deszRYq{ɪ]y\ZU6[&ydjt?7]-D?tSnay)]v+f3aڙUժ]ib铤y0[I-FRCvX @G(Uf_lj?zebOWUû>g]ck$Ө| ?G5$&QhrJ9HM*up )F"hyLuuvJ'X\z TNpN~fW#;@#;c8xr)#筼]Sϻ.|7JiW: G՞NuyKli֔Nۺ²t.|e: ,J|-ؿ)^ܓjm7R7 -srm#fn5=&4UU#3QN{DZ0xZO oǎݟIh|^ z]`TY291Q7;OZ9ءyNj7="S$TUCC vӟ'=]\ޖKWƤׅlz u.nn5Ðxx#@3!9x AQY E l?f-4gL08k)cTn.I^c0kiO0U}VsCžٙ]]4UpĖ]NSݎh4vZW 6$@ZR u5 x‘иI{KځE~ OaD4cYu\H~Aq*HDRwɭ 3oؐ|FhYԤ^kٲۆ 8`$>;ƐgqmO ht>,"oR ,9 -kT -(SQw-ܗMdoR9bJhfts04^YרKA׊CqJ,ɝ p ̋VCӪzE`6 ?H8L~ <&aUӰZ& ne @ ) % CP!knӶfGQA1cT5tVQq̭%ٟ!J8tZQ䕌<7Crw^@F9RC^zv?tqǡS uxm%j{LFL&B^e@2$g@v)',RV7O&Od[ {̉[A)vn])ʖc,~H3N -sZrA -өw_%HBqSCRXJ#CۋoJfkF?VuMжPSJg!qfm\x<Mauo>kBB4y*Z| fE;./*"lŴm?}7h_^AS+L:'V@%t3, w˚.V`Ƴ״h7X*zj1Y휄rDŽ3Oܜ-ٲ ~MJ׃: ϩ ,d+^{{}֡d3%;)MF I9Ը1y?N}yxN` ZC[4 d2ӛG vI*}!G<|c 8'qJB]GG´_ڇSFfIĊ͎1N7f64L!nN]Y gW&`fs{NC{?Em)JY4{8Ѐm1TMĔ9fJX3[k\F/.ᄙ 8`sE0!>Ѿ )YC (zDdzJeQ#uh:ѹNե%Sg+;dgޠ+xU#ټ2GB%p⑎4Q1yEوk.P`0U΍iaK-Px#X^iZ2nguQ2^N^FI)~2Z{6!vX|d " -eYmC][pPzGmR-Oݧr|1cvA]A,ndMDC?*m\$+絙1naցs#`CpOؼF#6Lp.nGEf'SJRO~~ }\ ~YKj~ү,*Pg:x*j(j" W2"=Fj9|P":b2JsFkM'Jɶ!qdăv=MZ. w+DvHOާUi<#seezBƲ{aM. r^feVdګL8u}IȀ9nChiEM~¼3-=[.WM{\3۲R9'-$)[s[7-2.4!:c>=)5à0%̏YɊLtI&НnyaѺ 1Kp*ꢇ߱V7BZ,z4LfaȦlzͣK~ANJ^?Ɏ/Q|X0μ`8HF@1nA{خv.@W":ݙ ,W⋍RYkmu#T/8BcrRǚǚH|%6/G1CRvڨN 2;*k̖C+C=+L*'Sb '@I0ęN 'w)Mw ܓ3TfOiZoSܙd:LҤiLɍ1$.q#(Ȏ)IԪ,dU4i\ϙ}J>XH>C[#,ҲhShMvM {Yq=]<ϯv3qC #K^62d>?D]7ܢޛa黇 -Hs4&5Vfx{ r@O;>73^N>d.\&N䤎㵙=BWL/Z=';ڮ @ebVs8WNPBFǵt?lɚONJ؛5Q\g\HT40ϣrB˹>VRY#]RjH5Ľ ;D7r\rG@wتh֘60QuS959EtؖބWf~ߥTWnƘ8UO4ጎؙV9 kUp: -a?FٔC^&\𐼕_N#f4M9.W >N`-YrpIaELjΙ QYʨaϳZd]Dq5%Ӱ^Qe*#%Ƽ !#Y -caw -+k"k$Uc  F2&\ǙW^Dae4`#VStcFOr03-wk哭93^-&d(@FjJe I ?c 7_4䕝.&ۂ6C+}w#}w2W>:=ƴ0}e 5|Jq=E'zS\o}ֳlȚe7}"퍼LAP/f675,*si733~3RG2:&A2م~XGb Ab 0G|J^U OS>(#=ZwNK 59*B=>!UޏyoetU6k5q=3ٞ1 %YL:|/&o^l('ێu* d?e3i,k-oWJEK@}AAEMLKd+&#l悘N3x{~?W@:7HNE|$=꺜C 흥e8dgp]AЕ`aZG@Cl4kڙwg .g@}aÂ; ~wLT𐑚cu3ѣ/vSa{H];"ؙSj(jwvNR`F]Z#[e0`Mu2bTӀ7G/u쇆N̼ ̧fSɵ+qϱ2Ou !vkm5<*A<==IHV>"#1@^PKd{mIؿ(z)i)<.+It~ um,ەRmp^@j8=\9P نiU^ھ`_8&+OtlSv#j?3ۢ(TMNl_AKxЁ=545ݑr#ro曦e @y"€\S'% MGEҫ;T$=>Z깘Ruv7-^jWl|FEHPߢMmS@, SG'ͭߌb+sj`G+  .H-9:,TOpcO%@!(˙XϗFm wscl~R3LKϷҬsPWkh:"TKM(ЭaR̩F@8@zuIR~}ڇV1w阦ç:QD4DH@V'^P܅"n9z ~u=22R_1TsY7D5WL -SmTQ£TLtCɏ 1~i9.uC32Dkt)ގ "{17dza8pц7`!+?5ʞs2Zt8'}x c#eOҸs#՗oJHMvyblj1Qҋ2lK^s8[ 9[呴K!M 562|d9^0P^,|Hî%~lN"rr#K"āo/v05q#8dDb$ZAX̆(|O]W|̺#.=`нF2HwϷ2|eaLM #)BDQk]?v;;>)'^陸C-| E>ԃ&ي}bp9V2§aif`#nOqSZ%~ꋕ biazQf#髄P}{[gqZʁ6,.}ҝqk}] wlUQ"rKri -@Q7ZFsm!B=&+AVieKE~ib:t tvIn@6Km ,! jēL7Eg1neejwN=LcM}G&̙m$'ǥbx'n9/38Ԡq.bg^(wUA:C')@_8d o3¶ -cl2\`Y'\|@Tt3$gˀw"P[ڑ^m|ʿg@Q -*%'y{5C℟,յ푒8x\׽516Er9̩:H*)t"s>e·ȹ(LY!gs:RU 6_r355in6%㾔?2ΧnB@w_4Q,c9PC{Rmy!ⷕdg>V]m=9YfO߂z>ςk"R&:z?@"y]iy̖'[RkFv텚\=RK3bX"eV3mjk̂ۉ7ڐ(;@mcc |9WV|r)Kpr=p{C𝁜#-vFăQfu4KtBҝr{aa-Qnh_oH3 xV`N'2v?s;.` d~9s3ݒs oy)h7ˍm}c童{r* -`\=h] }-iS#:o*3`s|A{mӍv_Az;iMBxӚj?ƉqHF1ҏyu2NovpOu5`FaypY.t>SУN'hQz0V$HՈ~>T:`07B̙𓱋:Yp/+"hY~:८N{ֆxtܐˋ"ncuqV\ٕ1cjam"<A\i -ӁYnFVUŽ4tML -G?:I -/6.z^$gbZ}{>B es"/MtfF8>V)9:H<φQ`ո#5́0窐Vl>"0ǘ{HۓwVkjk_R^˱3̣T"mWNOLWsB2xaD[YJr𗹪LSD۟ȆsDéC%%M1{\ymy47H䅺lUğW -u`6xey{0FTPd񼈵Nߊ;j*FLGڱ>2w]CTY$2_:_pmrn͜(og<]fϋݕ%>&x ~ j7 ~paD()QCDAʑoc[3DF^bo}n¿tboaFJda32 Ʉu6=6= ^:1r8) \zAwx6B=፩3b%E@V=TiQ۽.h"Աݯ|W1Pq_Zz!##vΔGo#|ca[Jq+0Yj϶цot2v"=e7Fjc,3,o%11 -*|sD ^be(ߕj?zu ͸4߶9 ԑqxؙ9&ginicVu-2>%:np^6f4ٛj,C&#t]ׁsU qmKR7| -ZkQ2=ems݉Hr/7V;+k`(86x[ODԒ>DzoyةYv`ȯӌgA\$ͶcwZiK$6a,:Vs.ܚyG3SE;2*ꭋXi- s2*\,vd#X!,,0//=ײ%BJXnE 1ι#o-_]i>d- -asQ~ _B+>E~|{N4C7;ƎCON²GqCbnl칖Ot!vFM=;ʰ |,U=+[Bɹ%JK؛@$;E5~5!ӯi%KM}ޅXrl"z{d]|%堏d= ##|*nchgg"Wz_I]я@wQi775ǹ@z@Q.1U' -.H̆ -9rA&]ۣ`]~8ۘo&^tÒzlrC>- L j}fuMx^eǢs8 ->!# ZsU#gxTܝq\ݏI:Us'| ׿a-0\֬ևfnnni6H%r6.c̳y -wbEܒ{R.$ԅzvn >%](W?[Ne(y S p&B֌27v,TOקaa`U s  -wÒ|mf0tJ8n~(ztcU}3LLi ,%c=$}i/OBSmr{a@E -a#i!,~_C4oI^ \-4f_A_.-ԭ]oqigzzGXZieZ&)!ҋC9ycfj^X!A~C#t](:FmOί]YerB0!6K/ vclDmzoA$:B\5;dRP?IHp.R(|w.$ɓɤ8DI `E"(łhfb E(A@hL&?Ξ_ZzCW bW[dO k9Q520N ϵVM怊Y1m]͕Vs^`yM!£SQa挤MHאlf(KyV܏zxԄyI+`=twʚGؕ!s쑑ݝe!R䥕?.g>Xgy+aavg;rzn&Dm5~9wgEtl;*c$0z6&Ύ"/k|BUכ*nDR.V^ kŸUS_!>ޛŽ /$65ܐ{l&$WhꫛqSx_+&ZkN|HONBM(^ i;{6!5fp9'U. j2?Z61ȯjplK0?)98=#@{dc?y!"jqCD%F'>3hݘYL¦;9gMH?Yf"cJs1c9ci8V.a`VdP3I_8ozӡ^vRfBviDB$|LPE(@GفyV]* ;*IO}2?ՒskNzXG[?mMŖ&֏ kd"<:dw׸ܴ ڏmIo@Ϫ׿9L&"_J_^(2ߒV3 -lOu$$bbA(_B,tڰɕs)qѯh:T<=22JF^_Ez%{j(8X mgaʂWD󰎃?]RP,0Kygv'Pw| -# ΑY}*`D樱_Ӝޒs֞ sԤT0w6972 }9R}rBvHÂub SH, '@.3Tsk qeo mC"؝kWrnYmVHM,8]mpKZԯ~ 1qA_m -/y /o@.}9KV}~9\)3i Al =Jq]gu|ȷ,"z -S쥍_-qƯ:Fe7B1.lI*wew}r,n46O"2BaJˬ,P5<8KLoG>Q|DvG)k/j m7+J/u ]9<'UФd,\RPc@O>o!3ܔhd)mea)P=*?**bYWߘGZN>(6,! ͗ËJBgXbm"?XXg'_KQ* -PBa[M.{c.ܡZSkRD΁WSKX΋KEBYfKN3WJ ~uguU{duV8Zo ҄Ք*~fb> cĈӼi:~<`lN -RZƁR1 Ix`vYNZ8&߯Ɨ4$hAd69wNaovK :IHChHi*,!MYQH?6,OcA# Qu@N;?VwW68bxҖ|њsg\ b U::6>p}%%5aH>JLKJj qMGQxْu=LfX!5%\=4?M/yn,M# gf7l}G V$LZW ˻i]`y3c]s@Q| _y!氹z3*0@_$WD<(:G7В5 -be}KKdŨQ@ؘ!ʀM#+ Yk7=7ڊ;mP:Ps{ 4bC+ʸƷ!aF {<X^벛v.ݣ~&D՛#ȼΤ<4Yhh[,,J£lL^H1#:iⴁ;gTmZbEThx]9oM\}r\3ȏk&6lYRAXEDs8QXUH\aT8Z}D,>q[/%b9\@KCjX}:'FLw4j?FVW캄ix$mGz·N«tG^s(;i1L +߻?+3?̙鴝̤i$l&I]1*n; (kع"ihhP - WDјLg̛/>\t%nobXzqKτsK?W@%̺ސ2Ѡ7!WG0T[2Q^BweY6p"3՞k{VRKb ب ,&|z~%_ Y1oڃBBwև' nΊos dĖ&tr0iWlmPkvBmݛMwؼ<0`;z vu{ c%&ΨO[5[o]";6Ey4Z@GN=); jǜzb&󆐳:酰Z7k@9!&lMx2Pbgq; lJe~ n?xB=_UEjzª9ZHI5ռ*Kfy?]íwb W~5bOͭ\uaAE/N+o@'Ä4Rl&O$bRRyaeɗ%߸0?$ rnPӉݛfEP]T5晕 #qg=O%B3miG$nzU0x7Za qm**emnt2жqCI~3q#&?4ź4sY2x{79}x%dݣ 0IBxݍ(ipcě5Qc06'8l :;Q3NS1 -ʖ_*C# w&2cU$|ð\oW 2E66Î^C޽olOx{;< I}GUfUHXVN٪hL)NηfohȔ=Ȅ)ۋADGOͭy>:%G<^5^ؤC3ˀ6*&E5'cUk# 0^XC7YqrIgb၍K(Ausξ:^ /2a 6%>5a#FA,6nP,!&L\[QX׆H-p xUQSbeݲ}3ܯs |6|m"{uHWKR"A6[DED !Rx闡5Ϳy}|ignz!{%Ow|5dun!zcv@+ bw yM1B {;y:`BY[.c԰IU -wr`w(y>dg*ziG8bbV\W1ڠF\" Dps˳+_w?쇝dgvu&cƸM@**WJBIeqb:W @HBPdf~3s[wۜB-KH ?:.FC3m%-Ҟ3W)V'I?n|dݞ^ N-RJϮ˛9M%SK9Jy zvUdUYL b? k5f7^jF󂆼7眬O["Ɇ7i洄zV89!zRO[":)o ~*gWI5D2QAߞ컽?C;A|r3lJQ"g_Uv>v_5RюҸ׻(j=L4K١D~"z9H:C? iS& ;L\KeG=v pT'CAE)#!a`V;˷'`vj6HZx-=0MOJ$ܢG* +W}bJ̡VcAYx$f7C\q b hy˼Tk^՚K!H{UQbY -;No GPB8mxif4 䈞 Q=Rn3Ҁkr!!gSBc5^u~IS69|ⓩ~ہe>|zm!Gps|UXxM Oh ZG7pg*%FP1>FDY1wO9q$Qp[s 0CUCӜ3ՖYAx97rv̭OYYq= "Vh0&# d0~׺?4 M5t!eTtB\ :Q+fѣ@ۓ߆f[ keebYt~?ks _ǿUe]2Nh<=i?sy+3[ Y5#;rfڣte?c% sPC'i=ؙ|X7CsPUˆ!<$3 q5"V5/#f%`!'-dYF]<[26m)|9* -7TZ?/e工®!BOAG&oJo}9Z0[1-hv)3Z3+N5ŜbF~Q5oOw!=!n &b6Fotݷ[wT@WҌ '#^4ca6c([1+ n!د4@]wc-L^CgsͿj?GNv3|]O.+ H&*ןί`g&vˁݔEt3죌Cb>?1:""bJ&xzyU:^Wr.v#{ʏQ3I󳁸@h!ۖs;FfuȩMn9tSɦJsIIh3V5"A$cW% PTSXJQt@+"z9@ hYzwb~8{]"_8IͲ'7 1-Rb+=-.~C%q3f}\5I v p"FA3fbF=nm5z7z~c%`֛}l^$Iʮ=[[cYp/fWM8?%4ZЭs@YI'ԙu@m'[r(eWk%!dshpCE=e{mKQ]omtMʦlR!VJmXep[o#KqTrej/' ž1e;/ vwQ=Av!yCvQHH=^(9+:v)8Ek[iGs#ifBDP)zMjuxr-fP@& -In]!=(SL,!c?ά ̞J8|55}3USS:NfRY+t4K+Q8,nQf&n5@@îhTdVMd8~ϓ z"f*ߖUb`ۑeՈ݄JI^}*yxאɘGsBĽvG˫7 sk(H<0n`N^9#'朒V3]*(ĽQq`8Q)o9.i97/~vK K00qzZe -bFzޭ]6r2IZzM->QD׿L"#e} 5©Lܛ>1cxqwǬlhսaZu38{vsn?YԴS=k6!Bb. D9*Q0.p9#)q(JTYϨ"iz"Kk[8AYQ=kzq{*"Mljgv)ȺF̚oR˼Բײ?G^=JAؔ3陬o~;.s|_v! -^T=a/HF =ad5L|Ȩ >%2 ~tewI`n?8"yJUeN{'l}TwfШrX:4#dmŽ[(fޖw| M J fSNUsXYߐϦ!{嬝ޔqQQå|`WtM&zyT,=U}ᜨc5BB" RԧF -[ ^=8~6mvU7Uh|p]>|]#_> _BMԫ'AVQiDG|.ڼC:wCZTϼjFqoH.8Z2^!1oNEZv8芔XtBMܓwJ(%KSJNZ`FPgQ2R69y-r3 QIѭk\,B>vЧ9?ag7V^0qo2NV ;m(YpWLɹY-q1/D6sY~L=Gޚ2ѫgOALBObvI't긞 -/ʻ`ݸ$%IӚ Yg*A\R6b6.aM|l $L¨N52J6 5:;kKyجMHCC$whҊњ?d,IDȪYXF:. -Z!@?a&:cLiLz5.F9ә~ZD]^]'n|pфA|U圙t 2&6~o<~D?2_-ѫu"98j%IY_@ΡQu܆sLd*e$&A־Ϊ:eGi8F)=?݂v --1nP1J\wL7_|jfFTYLxek| - C5qCJy[=7z_1v5E H9d0faglJ)lVyeK&~i@&]ZnڐXhA|ҩW:r{2fFZDC< (&FvJ`U9ߘJ[Taxw-+_.'OS1)&GܭCDyePG?ۇ b%loAR ޠJ;dܐNDz7O+ šފ:.jż#ӬS8x=92KH1={e5մRw ~իB3[a;Eru%jC89իZ;g;co615eK}(vh0sr"a_) kI8#!M-LUaՎ@քtQNCG_F_BbZpU< \tb1Y}A7z`8WйQgD_ v5Ħ8؄0roI*#v9q(q+\ -F\Yu8!ǎ'yb0kDqzzO,M2ja# 3/e*6ゃfR6K-Ǯ\wv19cR.!Հ.Vs!6>2^dԺ.?@y3С'^D>6XؔEƁ׃! vOecOBr.eWăe).Ӯ&49ϩsNTdl* K듐y< :c a wA aBs̟o_~^BxˤҰQ jFMXh̒:Ng@QAG;vѮB|fQpnQT{~UլՖÄvigQ o 7ҍwW?K+|LN<>Ki[oewʔy7m`GEZe\+"_KZuL/8afTGV3[.f'/T#lwt}w{#iau\|jg&<|d<< 6 _T;ѓMhČpu언M{8?lN7d 9Z:f Ӵ[+"LMiu{2㐈seFc$-"58]5ƞ\]X7,B|Ҋ0 -뒑SN LG%hSCf;囯?k~?bP{Q}1K ìE -eL -`'.)HYqZBaƁr} -TvrnfO;ma\-k8-I қVKɱ{7mcOW1-9@YX"$lc]o~^쾖6B 6KA4HA'I+/1 B(fD5uƭON,cfHe;EV,V˙y* .3o8EmHcw$kkDŽY!{ff y?e-S>ZPk[0 qqzofC"k{rT'We|#YUoO7}<𡪰מJ<5n jvsDiOY )froU4V1=+J%ghxfylkYs/mROz?}|FJjX6A.C{O"sრS&+;IGRVD;QD$t3hbJjWA5i}EwOwLMP^ZrN#;WEYH{sɻu0nks;Yﴯ?Qt~^~.ĤY+|,YƌZ2RҫGb{V<]2d2&&da5MrOaƿէ͚͹Yy؜y.ى *"E̜j4PCK %DT0EW@\E=m׳~UFɛ$~ -]ه]}GMݞewW>weEwί)>l})[)hG(D`Q(3_,M> a3ZlKZWԦ0dzt }HGHɥlJuMVSZFF.9hc0nߙwI >8Rr&H'@yPa^! %$q4Z!lwYz̕Uy} 1p袸/ --Mir_ǧ!ѯW(NycJY53_Z6ۀ> ZծUAE܈YwvG|gP65rtU37;U':eWf69%M, {K>E}.`&l{75){OLC;;)9S?qR|k^RVm;_z۔=%[nTYFjn'*.[ -@ /e髯rrW15*<]8r-UkDu13U8%!o\$.H_ /,4/͋ ikP:Χ4|?ZhjzTZLji9f!"X]vdKѐ~dR~#C]f~ -0dO -endstream endobj 27 0 obj <>stream -HS[kL53i;j$cv$Ŏڟ!un76j_[!MvgZOf:L>ܟFқ}=~}(P4h@Ѐ"E@ (P4h@Ѐ"E@ (P4h@Ѐ"E@ (P4h@Ѐ"E@ (P4h@Ѐ"E@ (P4h@Ѐ"E@ (o[B3-L~~zV -8O8!v]h@Bh[3A@%0\={14Q+\%*ҸMK4]iҔ ;OD[+xg083 A  /*jf%W5TJ{%l2H'͉ygJT;%xÖ_ Q]JU|fWm֣*֡ې[ӚoA$qs"]`^OG&&|FZÍr+cFQƧHyM)Э뺨o3gLXv/c6S.jGIvMv)JFƸWx%11#ݰ2lϭl"0SATrCZV{̪ cRf Jddz*$o$Wkb EE$z*jcQSw1I[q)ȮADM2*bB}>+?6^=^w8qg"hQPano:&^d-$.HGYq旅t.qc9\sQ -Wud|*t\ejͪO4s noJ>J,(?#1FI-{Og69Ab^v- ×蠸 -fC=C353 ^zvzhIXE<fƯ<ED4r=M eC'4lF/oMwwO슌۩+Me;t#]#s72*v}|qrkz['|Z=khܮ%#fi3Ȱ]ǭ/7w1Xl/hrws[TmWʭm'\ڎlXߛhFA:*l'2~g{3+*&:fdb ̮K)(fPf bQ1l"\+x:=Xp~<s6pAvna"ACZ&4dIlܚHf9頺"Ueg.N ÕQ|GԊQ(~?>Yrٽke֍㍛ջSdGYͩ)s_0F$sNQiQ[CtԬ`_!Fawg"6}mvy{ 7-|xקzn?ڒ]9XWo|g_'\̪nLԊaY^,ǫV-Ye޼>0} ~φdXIw?(bd,۝eTyYfʩnKmBD !@f $Nwe4",! 7BȖn7 2yus}G -3ԜJH&6Dm+˿?ZVޒ=dM%UQK}6ѬG}pq"οMT\ܧ;(<=y0UO| !wL,,f>0^h潉{-B«. ^Yh̳y(lɯJF|Mvu>3_~egRo8K7^#.< d @Kx5nԒf#R21 ꫟96_7T YMަcm%taS#έ+1$i36IMj,hbAMb,`c&-J?ƝT¢#5q1 -xdN?augY%mΧ=Tܼzs&a~Y^`'ʻ-Oq3Q @8/i$@7Han)%.ԎQ~{VX97#i~-k:]/fKE %iKɭn2 9n!ꁼ߄dHyx=@G!]~^?RЀ5/zE-O sX껙%R~+4rp|fn0?,]T}|hi;w_3mL_lߌ?[5Tئ[ƑZ1]} _ݝaiݽsՑKvCؐ|tCˡ-qlEɈ^6ԏTB&IfY ˺*qA;j,3Bvt orGj/NSV)h-|,hc~ E^0!)e֓#9_<":~)ZH4^KIWRĘ -niG5bPԒԫ[z%GxyNU)yTФM -2b~~3VV or^31:jV#F4 -qȿRDúڎ2)EtJ9T𰬷D9w{^|uwABdjm5tj听q=Xp(lYuLL<,'D*ef2*lj̬':W(x-5^=wC쒴hYYXn*<>-Y^ܛhz ->ò5?D44 -ޢ2`CVD@yh߸H2 DrH7RI;sͼW~`Ax |˻h}ʶߣK6> -K+*q;[5篹eepMw)e` Rp..s/F-w,@(%4!6z4qyƘ=\bUϙz|8'lؙOH~~*m20K&ɹE7EYgT?ĖjB+·V\iD2f]<}CN i4mVQzS9t n.qm4-yW]n :W\X刎O#"6: JB@ޕ(fJԦ ;ID**n3Rm2iNgcp!}|PIN<-f:eP0S_ F92JO*DO \-,y$7Piryg9fRқ^TϪ6gm:A2<`S2(Dtb]sdSO;,9A4c+ Ut o֒_z +9eD~S[[>ֵ֭ūAp%!\QVW gI`rL[&LHPH?y L3~p?32d㡑g+NG -{.*~>gf Ad6_6ނޘquEνyZkYZ8njQeV/VUB+bǩx*;N;M3:o9~w}]מH:4:y{Y{gvgsg9ܜ꾯Ԣ:P^#:6fZ޴ By-W\I+2s=oT7v$r: .Tez/Wss/e'G_E>73ǵǍ9\~O;*? CV)ȁܬ -6]!W9pYy9@Fl_#"zPy`Oinpq_(|2qZVk^?K r6f),kȰZTi 9Бo8aiC'elYy쩂5*޿qzhWaޯ:)[f+]pQ|sILj!sft_sqA\%B=U,o)1 x40e!k,Fl;c+F̥N.rNWfD=b.ꀿJo ./~["6nڛ>K/YliTHg[Vs7OoaKAubdx oA.<ơ_Nw,ěNg#u7w_鮭PU&IN/$zwF'_nLu؛i9k>U* -~ 9=,;韘@WXBVx'# <~*9y:ugM(E]y聝[-Gk~"G!cK=- =LfBX^C3@LmS/wk~ygޤt87w+%0S͆]ҲE\P{~ kQ6{$ַ碈A,HNC="nݠdRw1n3 U&,r~mpfS.O)fҷUÆ)v8OPb"{kڏ K)'Ttm&rQ+ -ndcLabcfVrd7oV=QQfX1@41 RW]$ȍ;+FLE<bkWie/gWZ;Y)Hlmgg$J~FZ Q>bgrϥ5=&f!q߯O):8B.:+u6y/xe~"A6kzG_OS"/cj?<df96?clmϬSnv)ɗx#Ă~LTy.;bCNb*-w_^y7GSuuL$~.9M~GT1W܌LS_bZ.8׭̒77m -HN!UnIq-Q}15WNk8*~o &vdQgco~c3g -33/Z1A+„:hSC6%fZ?8]`c.\SY.9d^ݢ,Ƥ.8Z6^j` 2,$1#Jн>A91CJr|k= Psi!?}9ȏ}?zA!KI Jc"!Y-[;Go_p|ҬbpKd7+iܽ Bb.;OL3lbP al_~8 墣wQ -x\cb6-gv3,.t> .tǧ -VU[8 -#40j).gVV>#Y5KVR9H3ťoq}iIKCfn>8?<־nY5V:[^98 -&r'ݝN:I8uTFQ/ 9;w 7!gw; _OR=D2ye>8GG+tT'eѤ I"`=‹[n+Ls1Tu -(9?>-?oNuλlP/!k@n~6?Αʒ֡2myb|)A;y7_giڳ>, 6P/mN|oʛ\H'5nρS݂y/KsJ[@0aV,eb&۵IHٵ G{~6:t?wmUDZ[ɲ{i,_/u/>CN{\,TPK9 .:H!OM.zi#e`0Wuृe7WWHB̈́x;E8 = Czy9dĨmbmmʮau99Qo.!r 8$j4#y`0)k)Tq`,6gxYRƆ'ɔ)o|`TT;PO[աk)s&f6WJÛp֟Ve\_{4< 4)];%T5e78EZhQS+7 DZ=`ˆޥAMٗߪwz&SU SW˧!ǤʺN.B\T^3Q\ g262_.)nfƴ0s wÆ w3aH5?3z$P):?om!\ WsQ!d^ Nm;^zuuRp8+瑶]«հ>x(TV_<Ϳ>//:.V!DH9קo<&璷]1m WN\+M(&<3֒\\}q,?h-ud7McdP*Q~ugq_?<@z3S*b4<ftIO+wEوATDFal31 -0KMhlR#OAt\Ҟ&=(#,H 2S4%4")`[&`TQ^l AoYgQ+ٹ׬&puuQ+ê9]ԈiIt$ParTI+5'/*.*2;2a,&jSF -+JJ!>Q"mEP l}TM;^JeZOC?'qW1rlhR]O$t>N<[67D1%'%Vu#*^)" -餕m$`R}XOZiV_A5r%ڻh+QO}t{F~` 'ng1$.-z]~qh%jZ2x]#fs)+4]cg^ђywjREwګDI1Jlۚ<8Ӗ?^io=:`'m--ZyrdEǷOg-N\9 -0qG+xm9Oi* j" -~魣#N0Ix]:ozwD`sl ->#^5с9Uk`p`ίW3skNs(r69WRng%ia 30AK*J8J#¼[y^.X2SE珄DQIHnHYpO~އމswï4tRFp Ew1}"ʹ_'<  = ; l_lER} kZz!;S ؆ LAK!D\o -OAeti> 듵#v~26tDEίs.؛cϐvr(FHEewؤپEvCn<.4a1x-j{%؊q8.&3 _67S\*UI%ja }kk66C v [kKn;^d-x\K>SW3GM~5"ӹQ[_mU^N/2V{1{%S|k4ZUszZAx혪6"𭓅uDm2;ECzuϗP:4?]'`cPglc> >UK) C~*lVQՅ_[ZKY 4%UAH|iwcþ//U̢cfY񙶵_s' UX7ʢPy[\_lv-20p:ٸBaD'8:*m"c1p)NLL"Zʄ@4YBK_J7 =uc~0Hi|vH^Xq[b!lfISQM,s9vUeL'8s7LdSc6'vMs)6k!'o֍]o_h۫)3kEAWSV;=~ku?^g0{XXg;ٔ(:9B/.4WA7+ه\{ID~R6iu 5xL/A'rЕajl8zcks0iIKx/ȭ ?|.9Ot0G" -܉D -|fPSwo(.ԭ4tLJg3O~Uamoa^=o/7ΟO;fcy`@(]6pwѯo#7~M ,Zi{*U}dw3٥?S*>`mܪe5nWw~`ᨬ If p>Pm4jUPQ)8FΠbv<؄UG],~?21ĭ\fl5crIŇ -X)".5sE6iirw{c2j5S:-&.$qIpU8d4z*f7I3`y5%"@J>5$M#O_y+}&wd3")4fqD8"hGU{u6}{΢_rZN5CG)*;萲 ο׮|x%qS0!ei߬%T~=QZB'532(&)փj9˧T 6 ^fcjܦ^fAMº|e(oY\ŧdT;LaNS3`d%UH8TO/|Z>ފݵDř#EB߁3(7[Uz5{[+>!˳*[u1EV3եKbҲ G[nrmSt:Eo`:K##+!|>%Cp)YOrvJs|>x_R>%G702ƺRϡݥ[G`*K(d&&=[{ΒogqMP6tff9|'nz#MF]\5YnObFzM!z v8)mM+\t4wW/fxCgyeP CSlrTR]سȉB3EcJ1R,F^21'[NFR#}"P#}iOcƮ%xR$NwGD-=; 0[IwfƮ9uB?_M8?S}h #bFb̈́ l -q:NTμlsuam_tٮ5ޙ)'4'd-4cnM3J|"0)axO9[p67ݜ{tݧkLzp>Z)f|b0x=W# -.Hk/֢S̈́M 1PMq -flxŸ7<>sW%j*q¼xehVqCRlN׌燏}|3p0" H)Q*.T5O)X2.[fſ:)Jbp -gOIOT1E[F } -}~)^! ~>>onEw5SWuݭkPnֿt[W06Q1hmW -C\:z$-CZ訐 -|< =~5?_z:d}|1tʈM),eFClw9'}oPM l_Y3]o>5d[;ot~Q`]l-d}г&=9fqv7map]̙7szf83SJ!! ,mI-/Z`( -4,M/ $eZ-'c}#սysFO^ϻ,6|ц? }dd}4zTp6~٤m`sP 3բ|:l"h -]Ϲm|=$(S0Ǣdf|}#pQ lL\w/L4 @Ƹ1.E蹨Cî!?M|~A"RN.TABxG`G{4F0olp.LUR<=|k:x?_ O֒h[_R`8pB7%C!Ӫ1{P -JƦPA^o -rU~^+\bְ.wh:j8bnb\{e`x8a 4MF6?zlpIIJn>wKiD#g SC L9ࣙFJ}ggu+QppOç\%RHg܏59_OsYèHyY觻/'>زL*rjje^yIG||"'mrrknIw3-ڄ΅/X< 缺fVY5^,//w,X..hϰaE&lTs]Z璏NO?y%\nk䣶>&=W^9#$]778i9.Zb'XoexpQʲK1a:;:j7h[?]:v)Y|'}|ɬOF>tO? W{?/7vl⨚K%bND Kw9Jq[S;~kLWŌ:v>$&^` `o g"Qi*&16v >;nPaNV&ZT82SɅfe@=S -G5; :pFC׀us"+ȠDoko % LH8~_ݚI&Ltx&16&>|JFNmjs: \O˽M9!b=-p&71=R{h{IWi7bmHp[:Z[M+X]ۑ"rϧAkFIWC ]FMّ T3Qzja椕sI(?$%y\l畸U :f_fHs@˭JEAIP$-H~-SN)lS e:Q275;$r=+O! wHiI/ĩ wVɭJ,zIt1ˆ;R5E-®?y&ˢ^kI.;V3%QN -}I[!LB®=ݔ0eA[!LyJl  "+rfbuPHsngpmUAwČ2&x^!$]aBƗ'?oxudGE{s p8)$<'](πߟ.[UI)h7Σ*,fty d]op|<Yܙe}ϞDOq]<d^rꃋҠR -%zE~@'=xT o2`f|Lùs<N*;+ hdAwۿ.rqu_՜U4pkΫ=sc%ҠA5*Gj77^q:sofq~p:D+Ǘ嘉]0 13rkfEħ itEF iE1b7= /ЊʞLn~uMSx7TOSʌ9 e׵FWrrʡ`Ox7?fMaAҾ3F~s85gCߵc-TzS>JY@Ĩmʬ)u3f~{5'EXwjJS\&lIo -k S$pEEAPA Q#qEp EQDQYԲg9<{ \7ynTEip4Y-@{qk<'C*)Vī.Mhu>Kuep9[ !:E=ZE&[v;'uh.I\~idcPcBb^/sWPy&E$蕜]tP5M -27QL߇)`1{ 96LFȈ~k|U}c'rZO<2Rh5^6HߍtM1 ȸK5]C[w\om 2A'(6JĺZ`"lñ<ύ.hso,nTR:.:'>YH ~jNSie)&﫢L^0iG]eC'>5WslYR?Ziz -|̓HSQQDY⫢r{\mqA;91u̢q$!&!V 1HߏѴ;0'RJE\c˸V/0Yy8+ ;DZ0M0/m3GSLou ږlNvdO=ϕϔFlkC퀹 s)ijb['> ~VqI#eWO&I䌌 @SԘcIk􉆗&C/󍅗mMSnxrdَtմd8m p>T7Iium )Z^*tIRKsVw29vC;Vg\ʫ3--u w\n}2B6SU94N -2 )@yG <&gmOlhE‘Mfnv_(V=,~P Ԕ`w JهE2=uY/M%:vfkUך&p[PiqB/@zLj& /{7NNR?;ALY (v:RZ mh'z0畖OHwAi㮂 Jh{4Wa/gg?\nF>3SP=Tf<${ڌ\Elv廯Ui!󱂙z.gX\MD Y-^QHOiVڕȷc1)p}OlCjy`@yo+ǷqJ v8Vi&H57A߇xq9vBlӬTۗ$ %:Y HazGx|w:0\c|w8Jކ~Z1BI?LwNR0Rq+|!qwD"NdѲ4 -RsȀs -䁗>aSB d 8}:[U_Yg@mBj9ؗM; ؖ1wt8F?Jq}DI%D^wY+'(}?)@ 0Ջ hz ~ aq"WA LTg ouJ"*|`v{Py}}p@5b;OH{k!d|Xu"+edH+@ ueUL,3YjO-&E[hH_&;FA.x($Cڱu+>z⧦fz:Azs4FNJv1$ƨ~Hiab_nwz ?Y^텲cAgtmY7KIRa$TÈ[{H>}ůviӭXt?uתbFōSsȫSW8QsdņT{-/뱒 }4W$"n&>{i(ֺ39r vS-2JuT\W QAQ@ \pI%QA\@=A״zf=gΙ/2suaܕ XPI?Eז F^YonbGK)e(? pLņZ'mߏыrN0KaEPЗ* -d ncLCqz̉K3>v6|m*V˂Za^c!1ڏFd=$c$^ XFiDz0'7ny=[mph}݅YNMnga:bUAb|r"P; zefkZvdxL%/aDx߅&8/iK.2VC7$(MIh}Gzu"JXxNJ~4DNXO(jyVOv۱f))Ԭ|*tϦ(1Აa:gņWC{?9 -yeTajc?)O5;Nx,np:Eo(Ktcac -j^?SV/ <-6Jc-|C'- `җk(ʘ I.FYqx] m.&y?tTȅM\'͵9rvʑ,LA?SV&"]_*/BӉ"TY>LUfo JMIDSە#!4[z~ ) .3z u]~ï0u ≾:| 9,g}p7j6 )*4ս;cEP&t"#ԴZd07lU}x؂!N|$`öe2X}?* 7 Kx߃>h'|$cۣ}i.)}wtfkb-rB;gڊ Amg^ΰ[hЮoApc[Y-W3 k5;b -Y~8 3\ULVr腊ձЗsc91tՃ_kEe0%j?W'#гqB4)pJmZĹYj1 -%7 (#Vk75ͦdle'I+87u-~reUqD_PuYV5B9lznv܏og?t"jw>/:'ZqBł}̬O;RĦg%j#j04,Nv2ɄUDgzA#quaB*xl*k;}!FMɰ))gLd Ǣ}PZ(.WAk|[6_cej:nc'>V?Tr;('kM.ie`{{Z-n{!'ĉCVE\׻'- "YOeU1 Hセb9'^ky(4=y84c(=-1txmK|F!PL:ggYL4][IZ}FWW|9_Yl(?X>LY ba6X~4N7o^)Φ,OXp65%~.$-O$(T2%1)4473sdf)jILYc21&XcA@K(EP>@؍b M&],A* &ٳ~s޾9’’Hv!}ԋJU_*|.c?WE?+hA*39NA|ⷝ[hDz.t ԰OPƄg?[^ T_g1NgUuGRRkA,,^G[eQ}0EK:(!r8.gKgm4WL[|z -:?`֢Ñqb0>.d\R=Peg[!nծ-@LUM| V.-i\꒸PqE.tPlRJYt\DʫhEQc> s&&>~{>%%\M ve7Yb2i<+]Iyr*~Wp:Ԏےnu_Mњ݁¨%Av5gԳ6e~Qk}Րev}:V - #xl)yאr70jA]"kvq0UĮjHOKmY6w ZVh/ :9&GӔk34T=鵝*ާ!s,u0YC7U>mt͒~]'իӼ:snidk*j:ʧ0Q[F.ٿ(h8?6耡>ˣƦgw-;00oo]+#q#mK6r[K -)(5Ա.ͭ5E]±ZVzb Ws4Nq^m69-w]}253%#ȋd2M6,QR>W;I&@u伃i}Ѷ :l 8V-+3sp\X)'5'.a;)U߃\!#d16͢Irq>N5lq -"ig)Y[ -k:<܇%xE8<9׿;XV\ ՞;l_Z䇥h=?f@Ϭγ*?SKY%'v%~ji-n*%:d$ȳVБsLV:2$,4!#`F~K ;D'|ּHuV yN91LCF D豴IKՖ 惾KDiԥifkȫ /83RN EnP'yeIT5z9,ݛj0HZ2Aܚ|pi5672J\S\^_ifm1&Cb=X$1`M,XA(J"K,Q,(*cԈ3|Ź:kuo?{2x`UM^x#Q;aue܏Y g4hcjT<| 6˯GLUdyqrK -Պ\f';0ZGn96'xwe,}\\ .b,@Uh -u>+_,0m:~1;bd'-%-7eh˒ZdVTX:0uoJ.€׊\~:.(9喝`6Gq+l{ -5(\jFe?Ffe}},ܐ~JĦaj$򱦡(wy>';*5񊤃 rQ,8]r0BM;9ֱmP0*zLn!- 1=兮]B*c^ڷvByPO2,i:{7 řZ -i~sQJԠzWY]5绔 -;(;iLSej-tq{匔$ҁ r6j24f#'|ViWuWe Z2}M8lZE{'#쬳) Ӧo|NqƖISS$L1um6s#-ݵVɧf^n(m +Ɨ<^0Od*|ؖ - ) \il$km88m3V<[#{WDSQ<р䅛[eg_g_?{yy3kbх^7'elM -,\uZXz>WCϦeArNg髳QzՌp8&.iȸKvYS0..2EUb*zg%VUerD+9ֱ .sv,X'J,7:fv?%< W/'e'ʭVZA+\SG1w J$%l[t1+9HG?D9z.1 6ks3uxC~zaFZJs| Y־.V27TSs\t2nߨѶ5x}c󕎼GSUIkWFEU+.i,5Zsx,q-,?lUȧ}O:VH/A;07rbR{Hp 7.uB<@nN - vEտ6D<}9xF$OH^C|G'u -|  !JT QN=kq>2Csy{! QWqzaeu&LX/NxTWɱ26ơ8Dud@a9o ~  @B:GC 4YۙVz kusu5Q$^RK?uA '=Uŏ`QؘPgӡS!.{ օK9[E˝ieE*0($C1aIkNJ 2H+CgE-)xюU ?ʘ7p(-6솱*|K>lK5--2^k?&,Zme! JvVy;*襢VR9D+/i#R@8K?B9I`~מ x w'ƨ8Q_@f#69pCkJz5|E^{0z3HGM9bg] <*Rc%īۤN0OF\ٞ&5)oG1!~l}99b‘ׁ xvunAoQ>@>i-tz %.neX%C̠&R 6 p%DBAza@~[ <$z}2}KweW@Q5Ss%QATDAYܦIKF=FM|*n=ӝ/3=s><H(>hNw0anzfUGmOS},Scwgi&#JI$B L*}= $<:/t@:b嬓MҹkPzjSϏM5JZ}V TE) N!7 wߟ\lrTSWszv>i_<0t3VJ#7!NevpgP!$Oo z }QLBB}1iCjӺ6BG]ΰE}šhUKpEṖ)A&BP>sL=.cO~0f 럇c}_f_ٲN779|x!h4!Ix8N6܀@ м8 V=?߱}sL|}X3lnaNvTuDɍBL 1A/4?j y }@hG?N-'WK=mGGp{k3rU%1Qh y"$ HK2w1?$b\wiߛ{Ͷ1ŲUvFS6z:H@Al)4`!7:8lO,(F[ެL.:o>r*rS1JQ* C++: -9H7Ąq\[I^*cNҗtins iv(I)M]W% aPE@& ~^%BZpg|/ EJ^ݷ>$/*ֶIE浪O[u/!!g: B!Ƅ XH/&.ow]n/w޺Ԣㅦث-}p_LE:Mv$+f)1 bFeŏLȏOv+ yb;[|M9{0Ri'˿v%}E^oƝ֠ N :CR|P.Hg8!Z(p,hpˋKR宂ۿ>ߴ}w:ΞXԿC[j=|l!/S+@lb~hnD^c@Ҳ$:hnl5lšڌU%{̝w{`uYsN_ - 7^ZX5}ش|A//gɳmfgytmޘCFStth@ZJ*%2`YZ?eY燇}!殀ԝL"d$nnzaE+Ѱ<ߗejV}?M:;F9[]ӳRKO7;S(fGǃ$! CLFA7iKfcژQtz9x53u6"{]>z:nuBvoa\bPͷuy%2QH2Fޥ"Y<(K]e-hGvՎjP;O]왕˽Mf}OΨW]CqVVNJLH\a2fDxIR -F^Q5|:F;x0inX5V_- m8G-/iгZ"PސWHҽR UE2S[N;ttLqi&Bl8f3Ƙ KWEM !$\hC Z0Lw9=o@4MscZ=hƾ("~:^0i} {|_/ܩɣn_{ڛg?ko}t7u?ž~Ku@yGLl^ҡP$9ށRBdּ$@5*Աn},j1Om<| " Xpb٩&m챴G9C(ط<^V- '֠fP5W´33k 6֩xS X8 -̎2/ -C5^Y5B->ԤnJ?Tb0/2bK *9$` LW1KnPG[ydMl QQC-57EE RZ>PL_w*]f9 -|U$|lYŔ fᤉP"c$> -+Ll;Y rJk\PB3?-5U︔̦t$ 9m9y7䫕МV}eEDh)'>-RdV\_1?֊dOU\NNZKQ{Q CHZʌ}F)(A.3D%}l?!CaVIF]Z]Ŏ޸x֨1; v}`>0GXP0'[DZMПHO)7%4m̶E7-VB]AiѪ͋6תݾQNKe¬$o`Z¶ߞl|/`)@:(y/H>s&f}EjgfQuXU4Zb{a##i"kد -^5mZݔs?9Sٿh M#mRM:4r+/9䔉<Ւ?vJoT"ə=Qt"̵tԵh.bcSvr&87M 0tn1Xr#W}䑔홃1Rr+;s`.ťbf-dL23'kGքI9ANlKIlMx5곜;QuY+@zPY/+E[0ƲvIe^9'wH2v)/S6hHl>26#cC@gT#W,9]G!ڕs6lKbcf=Ey7Ar) QBJ3pV-9G 7GYK¬jؗAc @pfs.򛼗M(sΧ]H߅^e-Ci3s&A CC}"Ծ>rP?؛߬Ya'X8.kRI{8Bk魛9Yp6`y. 3&K>AP5ɡuϯjؔ '=B*u#'bLڪD^7O覼[s=~d,B0ebͭuvcO( t34r/3}/ \0h~h~zޜ"h<&?/_xKEqɯ4V! -R]yG*!ga+PM. ϓ/7F|+^Z^Z/:V -fli#-uf#v4#ww7S覼-f鎊>^vQ^#swRG. MHوRf3lC{޾ -Հl1fRG0Rwj?<@s뒛_.{䚢K9\Tg I&{U@Eb\١s/=V|Ttk~ͣj Y' [8DZu %}DmK'xʮ1cl.G^~tIB|ST2MG$GF^ۢ}*H_K{QXM:YDl).&48NXNM3:0"dP}@tC',"*B@ وIvFp=ɴW_WMƆᓿM,r;i}'{ٛ4ЛrVv_+CF..k`KgE>>+cb uo5|<+߄@o'S e_⬓ e]tL(5D[Bg#k!e珰CEW -2! *泔ٖ}!5 -t;'g!Ҳێq?Քe$v;ƒʮT5 G;cD}1 2U B'7cX !'MIH=d$=jp5uryv$܂gZ;N@HhY_㍮됅j-6ozfRfOTY+w齂xK{3PXt{.F .aot|҉wZ4 a@i bFL-ф a9id36z{Ɩo3Pr_n~+9hPe6+a"'Ev2|aBN -oއ]LL7(w9ZB1rV"Qa`J~[rvF*f:?iOc:ⓘ7QjC/-GMv&BfgU?e?졌Eޤ?*إc90br ؁(^9AHA[r~ߗ/yIrݹwDSLQAsVX`T`G6(*YeI/g2@W8ήDt9܊^6e DC騍Q!e5Vz{@w&ņyhCVF'ש]h yWSFo/#߁t@ǡљ odԾJl -{3l+9gU'H)슐]2C[Tt@<N -Nlg?/ [긞皧MdC1 ii# /iOI;\!܊װK>!$:b]ɖ,2V6X{<3ʌS>@dbڨJȞFV5-Z(#^Jf!9CC[i=?f6 g&kHO.dݳP]܄|;: *6n7'*APl`!߬*nܮuld` [tܐ啣'L|Fx ճ:2,/KpeQ-dwԙӢ7~-p ٥5%4̎Rj*rk5M"~9Y'4̠+틝bNWFx斠%J9_Z ]6sz[v)5JzehMΣx\KDצfDj RrJ%66DC l&*;]* ֜FqI=;ӠĵbJTаhbES*T^9'' G&,j4VP`nj恌5_>X"7^'}ۨԐ*F-#$෷@ -(׹62ȵo{Smk|#Ck;M*2'|;^u룳}J#>3czX62lf8$ tT-=tܳζdLyX -i+2Pgv~MMn)|]^wJ7O ꙩn|$i2SZ6"tӘ}I!)&ⵟvf{V:ssᲿi%{ zi;>qXH``>FG -jJ\2 ah5l_hEu7?޳@,3Kk+ھoDM Ú~I)@K6կ?FTqz 'T$L{WاvlrdWśr䞀zG͂7%>ioP;\Oxo5%nKXۜB["k}wk#?c>+ p{]Ok=У6ȇ֬qqj6:2?gد}W!YBG w? nx7p{JkXBږڻN)S0tXEis\~IEi0F^RjRg_y!mSV-i%#fBMLOCȭhݟL|d\yk>BHI #Zˈ (a  -넢G>;6dˉ}-%r * iX"&2˄E[݉nӰ[~r\h%ŖlpwNj\T}2{5-t5޺?+oYZ!5ӏ|(AO$kS `Ԑ2H,#eTgPwߴܴMfy[8ZWBtKwɖ;#u3¯vlz[GF5 ~9@@R6ШC|{w䒶}ĖAgt,[F,6O"o$ /W+?K<\|2Cʷ*뫮Az庂YgJx@=K;8D@*% z78Ptdڟ':`Y颟 _ -Z K20ȏ_M'[/C6%^~#dƽ5G)ʎFOUQMfa_$$1ucKC8W%5"JZML/dac3{;8ΤfK411UcWlA*Ej1ƈEDQT,Hiؒ>y߻kpEi -m/F.mKX<=eu 쾎(qَψ=`y@[j3- @X!۩@8DGclW/v{TF!.¼1|(c.-de^"y b6d:VZOxY;g鸃ŖN4)PN9=ΪPᔒ˂K&9_\,51 plp8-pL}WuEW oWవAQ. pwHeIᒛ쫳SwjrhŁ&ġM9d !zsHϚkbWk:awvLH@3kC8!+u6FLUzPI_LT wG;&_{Kn -- `sTRoQC.}[Rh_f=5z_1)в9'x8Xsza|J:ZK*wy8@}5O t^A+ pHH}ΦZR׏1*n_ھ9VahM=ol&-1~5{܃zvသw -aV .=ۘF\8TQ~U+\ncU߾倎z+BZUF.?^6.RkwW;'VQe, #- Q *zp#=ffa9zem/lNcUpA<r} 5ofYo[7{,c %%eՌ|h `}563)rϓAs@'k4.׫%y&ʡlz[[6L ,bջ&ev1}uvO/Q ljo)8Ẁ1v˪hF (2i.RNVؓU%+blbl_ĭI }$+*~WFpuvnEJf%nrGDX魼1TԽu^S/[R;cYص[b!*;}&q`>UK\rJᶐݴ|3 U~1z:ÃK$/?S@".!)ٹA5Нփ9Әq)Qc4-%@z -,ڑQF4B [A, hӕՐK}jVÞRX>"C˯щ"zy EyCL+syh$1#eYF09s-e5+}Wp9^ g}ݭQxⱱsgwU l/䙨93⻹*rjdJ}3Rih?IξE4Om%m}zh= :0aR1_V&d,fsnڞ KKi`UV+) -c6鬔@qTp=5q[LL&֏J~ֳ&O/ ;u ŀoSON%4H9cd`Ow5[a0S ב6ňgn*`~/<}d|1:ƭV{7‰Jodr;F{ !r9m[9957HuⱁG>s3LK+E~em}"~+/86{f}B&8%!W8U϶M_m F1!mcMUB5OPfFJz9NQBQc%(pM?ts  twp Dx;?d;Yv8=ޞDc3pNЫ!~)$5lf&昉LbǬGRc.xۀ7 fFĄ(xʡ܈A@KT6٭G燷dQf0@ ,‘h#ȇo&AOyCIiSkD$>}\f9[ 1,!/^l@R /XbW^w mM3fhezC@jvqYۀNցUyNT1 *6&[EnB~DZXmLL7&+ڞxgH9ګ nɣln 5o:*2`1y~zGzuAՔ -96R{vE̺Էgk˺'/uO6#C$󠨕C_"ς 9o*ޯh-G0]iE+#V=b5'ƶr <<\p2R[< )vWĢ9F[rbg&.1e *Lg*iWޝE~G1 -p%s#;N\q^v+%~-ɿF{wdA-mM6%{7<~tWM 5t`T-![Ax5{k{9ACD4 e'>٩Mw -( sKj)U@yf :>tkUTqicWV`$[G=nK*F?]-ڤVg8kyR24P;K|P|ҡ @빸:qb̵e[$ 7?Iq.=G[Gn oQO%J7f&-9_7ÀǚRM=e|ӍDKi=,1ji/ Hp3K*86 -#D:X.-`5;m5ZV}%^ȏW% _piT'q{$6k};2xO #-pJM.x$8jc3N/ xgyS}ѻ qOs4XpGi9"PrоĢEQeJԢ˾+Î>q'AC`5B<>g Idʖ-Ťʹ?N*97\Yn5TasB ˬƠݺ)C毿-OsHʮE,j)pЙSPmycg` A2~URx1j5:^'̿!28;-bSZﻨWyf"Ҷ SӜ2|APj X -YBWNQ6QCז/Z]9am5W-.a)5 -FEGlRhM(E∹{fŃgNavվU*|u;j]9=M+V*);?eFν3iLib6 41kw\X7$Q%ˢXYEpI6}=3s9s ejeZ8D*]s$'#6i!@/>HӴ<`OE_XN& HI'R6!GʀW< )h5]Zex1ԫ&y_k'_.G2*=wi)JMXMڙ#-k𜀢ց,nWY)d*ja cfJC܈;j띤}=`qvՄy\gWT-$D;xG1C"6;7N)wg&?#*JL~5X٤Sv@[]ёqgā̈s vM; ةS[b 871]s8.ޏJISԇ߄WuGQLE iH~q@I(.rjr<h=0 #%?6CB@/̣ K?n䒏l]#x|`Ej;Mxn&~|ѕmhk,VX^HNz<`B<+C:ݤ>{O=S<13QVE)t6aŁf/J!RzQ硞H(U 7o \V^L!rJf:}6#XwG kUw\Ԓ_o`]7Bjrپ:vq@C:6ҧ$&Ll[қBpk;,/#swE>4# :zOIG 踘]>㴙9 i{\ 3 -̢!>`hՠ6e-?a 5 @Ӱ -[\joԝctA'l {_yga޻ B"NO[L_n",hZfTGVH:r=v.D[BK:H#PUg6ԒߏΊGUlᶬoC{gveo)E!5$nGZBQ!'6Cqpl籃JRY&yCʎ׺܅q -ƫ=O,tT]"}X{:,-HsU_ݞm9'PY98Ɔ%!ձa .?W`VT|7ߔ}`|_}|Yk$Md: whaW9j2%КlF^鄍p?I;: yjkywk={ua簊RT EҴ5IsξFH XȤ|h%48`/;F"lbc䆰'r6a{3wm#LpP9_TJ5~9.U䞳)G(4!28,!e1IXzNRMΥT_UzF& -zS=aFHIchTl E9W‚/3퐨Y(:t'"&nG% XN.Z$U5I-8%Fk ݜc.Smz} -Գ,OEVOqy}Qx֜NL&mJ3L4DLk+ -F)tTH4^Q)"RbAę3y^|7ŻqV %틑'(IU%̩h=\΀(ա^ef{tq~X kḡXG0A@Wublg, -+ -Z^*ۖZn?ٶ XEVo/!%R(ȯiw)X5 KE<6J?\")r24-lveRC_O#YZgU\;(ԒG0}kwu|wP6gU? -2(!OrƖm蹙a w+>\<_ja >iCrHњճ3r\arHBV;ȗ|hHZ9~}y(s7n}>BwKH$JG !82y^Au/XErM9SSWOH!tmhn}jzKlo!dYt>2˯—n cr?bJnEz@Ksq溎wh񊡺yшzNJl1;ݡ¬2|N?*M41#O6U{^%`#ܚd{iwMRƥb-ӄC .~w*^g@г !SOo 6M'֣D{uC+\ڞVruFÆ84urW]vwk"%o6-jb1#f:=!4r\5 ->cuFQ'Sv(Y72F" 1n#eH4~Ew}e'9 "B".KIYtD}`}A]usx#o.)``ߟj|?.~-!#쫾uԧT qo@l†N,!6KPyNqk{K*3~^vGoY'ko{q-$TDs2UQJ*8ÔԨCI3rcJXMwO䚨?#0Ztsc:zuɺ{RB15F rm#ɿH1<|OEw7eD(-:N2 rlS7.Y[9ʏZK/~ n{L|PMit@{x_ pMoӐN,_-dxԀ~S €6Ni[mi/n#_qsw䀞YY{4XbCOjkeCگvXAu舠IiPJ?xE(}&^aֹ$䢣iODrkKLg =>Z)y):K/ZGye𴘆 -hzitaKͿ[a[?|tNw0xdN}e@KYHIo,P ,C}kkXsVcG${9$TTSm)w`I -gS?j1L~%Ynn1T:ȹH o@uB L6m7)W.)dD˾&aܳ3CZ^yQp W0LbX$<:7Y-wGIGǫGKz S-k7.`rc*SK;ꋅnbk-ٿ9܈;rv/aV@4P=%ih?oRxM&S+šlOuO+sP"™nFѪ|h˥tUk mS=pj̯tbťoTȫzf˯|nѳta˪jQ̪rZ˪rSb7Z$jAʲŮqnŮp`̶~|ƮwcŬxaư|зȪeLU)ͭtjrlڻ~ÀзwnֻulͭiNža5ŸgF]Jǥ}aƢyTȣ|\ÜrFc(sNdD e>kDʞw\pLwJƘg>ΤoIͦmGΩnMɥiIápMq@sU%ɝxsҺͷ~жs`Ըwjг~u̮uƩsm>]Ho^ӧx`\#h3c(xTUȚkArJ|_?dBάydͭtZ˩iCY*xHsF\)Ği>~]=R:& uD'ұЫ{kӯoX^0M[/ϭyej@ 2? -endstream endobj 28 0 obj <>stream -H [51Mo3Lhbjq&*8@D2DpF[8PqDh!o9 m,1Х"ۇI+Q'D`0ѸftcWHm,mCů-k#xB34X5rkn 30`&J]{c>1B#rc׾ \8^ͤB2/@ NgM#ߩ+ TEu@BLܳ2MGy"+ÉG\!$~@Gntl pu*Kِp*yyd"Yؔt}'7G/0Ȼ V'!^8}2 iƾ"B\~E:DuKO-q@3}=m[ũQOEubQKOƎ!&j4rkiB@{E kgYfkJ jA᝽)v!PI9DS(;p>+Q+*Zys٦Ze]B(湖 }$NwbRNo# 7 UF̓:1PUvN6ƸE[yQqRf xӇɰ|Z;Y啯,%֮'8,d]Xe.#, Y4xMRɑ_U -kO5bf/%}5e_.*]jFEMP IϊRCk/pS(;Et=8L]Am Q$񖩭ou@̈́m1; -[=5ʼRs^bnGDI(?[`ZF1OOi!ƶKoOJ %t}׍M2F ! 5%|GJ jX,f$\5uf1ڔWz\BS\;6us{wy1p+c.kcU0:=/6`g fc%#GU -j1,{@ɂ/6U;4 r"dKͬu2QͭDTRC=B}Ⱥ<k0'8H ʭcWyMuO4.LZ ):eeUH$x$!$ϥSIw1*t[Oߖ9=Iٟae:mg6ӭ9ƱFIރALfXD31/+=aBN 2\MځJ jN =URn3ҷ'M ]M.\F6Qr -[m#5[i|jU_l{ϭ,ҙF$C,bC{0Fʴ"vթ;剶җ%:f -!3G -|ڕiJ%I~}_#F -hI?Bz瘬};/ - -`KSsHD_Ve^332}U̢éVƚ(݅ʼYGIIR .|CQs6٥ǰy;,隓ԝitVwG)Ⱥ#N ڡ{O g^Ds)هJx] -B?aSύB bMJ=s"gNgm֪h꩹ 1@HAd -Lq@OW>VD,4A$%t|7.YjG9&hKwod$\wA& 5)42|a#-gK>3.gؐ~J~,j;&nuW[JpGP~4B厈ar]a%PcnoUΰZ. fF"䟛u=@'/~ri)O<~jAku(.>zkPв --|L8Rs*),҇f m7[\ l#U#s!@IrjS)@Zt]<|]N˙`m]Wqb﯈^.]I-ҊOJ _v[L?xϱ-G>>J7x]XuPk\;\8Ń.V˵qWGz0p -NDzȞb?:1+UUiwx1׍wz.ꅚ [k rfBkTҮhܕ>:&!TPq}(!+=Nt,wPRN!RG)=`gp)R<@]W`%|؛V6sv'^>u;w_񅒩hZdRRp皤쩥3GGajZ޸i#& ɎNXg萙6$؋MpYS# }wr*bF\|kiġAǪ7IAR_r&|[(y)aBSq>S}|A,20ȿƘ~y.jHYRdZ7䑎gm+hEp9'S'VPDm0ĭ LOEXPyQy st?|2;6Tx0ScMаgzُd969*'Z/ ޔѲۄW ݥNOZ!kpnrf]JW2v}#rE2teԸ2Xk`*vI*^&1 &A^;M% ~LQ 6FAh{J;1Z*/<ڹ#'Y&!>xr*0pCZ rjR3쁁[+oͽk"r}Լe䑥 -Gj־&X8lD8e 7Z0Ir\(PI,B5@hUkT&x*.?Iޟ"ghO ®^`dC_em )1 >CAOA~Kxg]Td4 .s"ʕk;LcG͑a樥']>SS &h;kmӳ t< }@_EMs+v]R#<(~\BWEJ/vT|)l+\6.֯u14xVRlz{ -~ZGΗxLQfb"r&b[FͶ}er!'\8)N?2%w8HMutcF%~7jBqTO:7 Z:*!^`m_2jtm6:ygߺx4@!d${F1"Xј.JOȵ>D䱞9_5:8ߪ$\ %z>NS3lj:ԩ:!6MQC5AFZ#HKPAT\(`ec52`1!*gu#WR{eyA#.}4X~WZ񓥶gkݨ(CNY cC)*:ئ (jslތtI$thyMC`")^-7="F`k5ᾖ.tz^z1ߔ0FxK$R+͞ҘK-a Сn -104T00p'J|ág~} ġۅ'>7C5e~p{Y0[c@c"Y {z3GlPqMɲvblxm#%z⽩:]foag[cTz6z 18X# ~` -kA yΚto -;YԮ(P\Z,-gzoWnM$qfKOBEpId] d/tOi~ShYocKBE"94u[S0I6lKsځ -_( [gaӂ_ {+#7Ps/%n]\^@6GO6{1GF~ujB↊=7//Jy{ʶ (d,5HL݁Eiw[;iPk;OC2T|#ޮCdܐ>>!vb ~"IJ-2jrd:BXrQo,f/ $E,xo#npsTlE6n+=QW-%R_bd*kx}{*'w~(} HN B "6eOMmGjK\~yݧCG0Zh:P|bR?&޲vjE}(>odmCmIN%&uJ}s[jfxI|hpk~ - Yv94hyb`VD3ۓ2@ ʪ:33V>'@qgM<4I%dGРIazS9r9pEW[ޮ@{'!#3rn~^GH >-hmM< s)<^c5Wa-:,-'&TGsOcX|"m*-@xՌ`?ٺ)qRYfl#Eo se1}!t$CVF%Ey=ņl6pюp>j{ N2VP/LׇfW BR(,-=6婵E3DPSb<#4*#_o̸WPR7'7B{r7ށH<֧31ۣLS@=dC3KU¾N@]h)dļXR2p HPU]%{YLW J -R8.ѡQhk H{)8$XRdeTӓn&]$cqoVxQdr'3XQ3rCX[;WcZ2G @,+촵G%{ZҏCpnYZHYFk?<qqݱ5'a[y57w(fAM%3't \yVx4KO&iUtH>yOIfMrtVOӑ[*2d (}]~KALN2lS vv0'LM>8_w]* DDPDIRH/PDN !=BĂ^ggO?ߝgi&y&1x.6L~$l)j@s]<ۚ;Wq7I)w%UI;lxD3 }:ro/ț -e@4[@7r$>1Mۙ! +-#Z} D@OCΈqV~5Ivٲ]]$,[ 1{]%"Y$Y+<ȃ?msһĮz:T,Xw䬖א~h< y5NbMۋ|û7^L]RHIyFnO_WK' -N;޶o̰am/Ӥ|gF5QJB ڱaS::}<'>Jm"W$176ĽGk"A31 >65fƖ]h#{HWOwP ։ؙMѣ3۾Y\юqpp_ =4+QBjriDo x-ԧQcW fezw1PS_kTr[NiK~YGv.^YfF_;}n7nh5q%⣙ִ*kɐ>I]aלC=tZ3 n91?n`6vuXeWuTWq̽F - Fz6ed8Q0 kB^xKFLw E> vV8(!{u6E#\Q<贌Xxk2a.9ثdBzƽ,h*V!ˆ?t203Ԃ֨hMظD"b[ͮk8 j!~!5 &Y9ewP_ֆTTX'$b*~͆}?4>Z;ǚÍW< -F)|Y{`\ >ÔHO"+d؆uc-k#{kZxG) kYl,($7C 7tUlM7r߫>Ybš% .cQQCf~켥nBfaneam2أ>n{tׄ4g_t:&n,r\Y{当 tt_>Пn:uBsهS-!5Y7y*';ZF=\ԐsO~qHQZ4JInˏZfւw#O-aBDjp^>dž`=Ȼ #_兗eEzO_M#*,S5Kl?OquaSUQUMJmLa2i )BHĄ6 ^{0f5رz -&_3wHGs>̺jg&Lx(gAd -l.XSHǿ(Tun֓VG8ϊա!﷢OB#0t앃?jm2juҺllĸ2L}bf,q oȯю<{9Syߠ[څ9Et\_;׏̾}I.yW%qꆗDԼ#tj =mKGѤNFvI{Ѣ3icf[q5\˗N*^ֽ.6P˾;\iTtlb F%:ʧ W'ɯB꾿vG%?)5z\$BҕZXTHu?QASj15n Ko}rmY#r)}RrNV0 a;&^:Gsݨx2.au"♛8SSNh_FՓ_6H]pVŹD3.ݢ&V#LLLm޸NnI>x+MTu7hcɬEmND}p6BAV^ܩNGlS81e=o9cqy5{Vew{A<2P șTN nX/nJ@V11q@ Iwj!>[}Y -ɵ h{[a \tkP!;ctJO몜C> -~κv=m+5Vqq_4 T\g[ݴBNHoZEVGE6& q(gQĤ";/޳& 5?o᎗=bJ 4k:Җ\nw3z.3q{xtuC^}~Ds޿Ĕ. -A8L(ƳD/ƻcZ8,ޙ)i*? y= f'6r9U/}]5U<j٣~SZzP#(wѿGalkԦH_CSQ=З'gUʘc%̌={|)Ԍ>:z+^(qD#T"<$x7?XS=[>slwQ1!R=?ძc;qZ'|31C-f{湛9­^%;fXcLOpG9-Mo"/ t|.^XIyq)+9T˙E/Dslf0 0@@KV3?Z^n $\E5ĆVq/Κ}vsR_֎1LI#7jJB:#堌^&dnKx8^#1%pW2n 3,BڼgK¾L;UXNp' -,PHֈ9͸Bt_srԭ]: ËLGES`11H҉HXG<{Cuo>H#Hƀ^suNcg% GQ$y5Hf$'?fF/PWΉ?O(D%tجLꉰQOStxJN Me̦_Hۄ5;2 -~4S_O,قàH9>3WYME:;Zyt!ݾLrrE{ -Z*D/Z!ET0Fx?@N[Co;( E 8A֩cMkk;N,eeIHMyGq7%qQԠ(QhR>/wr7E ~;wQB#!a5™ZD0Qtq-7?&'dxQv_=.k_YQK:ZsͩڎA E!L@P\㤘 ȟ_^yWN - -aZۋja!V[ i#Mf~W~zv1+A-T !h\<,V7+*YQ: O0|˳'9huBF|+jrzo)`ăMg4mQ=6-`5Isijk|aN:)Ӓ&E?(5 @q#f{k⅝VUTz!jk X!n -*q`/G¨'wˍ0Cĕ817FiК#fDcW4.{EXXwƝIG4DV{[r&A't7;Gnt *K2$Ij$ F1XB -^;ԈzJ Rȹ zT{LV -&)gԢu -?=y[a?h7Vۄ}>q>&n$ފh2MνMrv9g>ț]QԢ M}}U -\Ij"hu2A 8 !-$>_b\(]EҤWCj1vawb獴`{I!}'rJy}+LyH#wzSdz5 }2rorl MVAh]~vZMu EQcʋ̺I2 1B?7Y;J଴\^= --tÜ)'ޔknݗ~6!s$4rRA5^V JnTkN~FE(s[|E]R|%@74;K=0\2sϓ˟dW%# -liS79a#p?<;%WF$581T$np͸p径1Ha~0̠V@q7:SB"@"~O>:a1L$;d\U r /aʣeeQ߶%/I:]]⶙=r;NsKe wpVd6ܘ+ig*A&pAUa gm^X^>ܹk\T([.i@lOƝ.02E9^ЋIo ^3 U\dUt&PRΝ}>>뙺1~Lו X&myNlxô cpu,tϙaݝw_|vo(xӂ:͊Q 5¯GePA F_wj,-7l]e/>y0ufCfHt&W37rhr}%͂\C2.Kdx ZFӫ6cu,_/QrlT$`#mY ;ƿ(ˌ1c02e/Y -,V"QB%K} 3:\God_ ?7jp,k'2AP?2x4V5lm 簌A*OIۿ$Ii%dXf÷4b+k5-=vj=8l|PQ7S/S>,p^jXg)֤ 7_S X:SJbo6Ǎ/\u^8Ϳ[>̮mN.ݵwf^:Om2ZluT#Ej6 -2_Aӷq6hޮ1Tyunn4iG1!FI˕3wR3%-H*EBfR@U-C :ƱW_ů)NNm]ז7]h{Kſ*t;oVm>-5閾Ouv^0#v{D>h /5Ь/L9P\dRPPG݉&[ՖvbMJ&O8]vc8_Z6(Fw↹qA2H ,/|K,,s'jhjp{".Rڑ:n D+DctOqa(2(BHǬhv~=Z!mk3X*T|J_Ƿ{es@QVY)X&xzKn1Ҧ 6jv,{~m_fmѼ(_#7 @ -3 - =|d<J^Y -W%H]5) wɓݥ)D"neH'ы8R//Z @;O:- p <Wo8jKʵ \;q0#{_W^S:k/^WA4OྤQxNFG%4M~\w,RݗHa -!W:_ Ldzoq(g1׬>2=bMD:ZX[,_e|Gُ&>Q7,Mzf/ oA436bJi3$cUO Ə١y׎YVn6i:gJwEOn gcM-8 O/ -M=8`LsuZy:PpU}^+D G>ݮ8A͎mѣ[ةgvY*g-S+ߘ\?ѭ`z3a746@Ʒ(2 BdAbFymwJP% gi9JI;qS$ b潳hKYhzDK8*ݘ0>K\¤qOyr dP5ߍ3%1[B/ht9v$Uh7ٴ1@FexfoG)/%5 -$fw<8cDd^u3g<lN"` 64ZG]uXe@ -v8GK5~S^'5e'eҽxt/L@Ѐ9aO ;ddy"NeGk&=&jRl4턊+dq2YYOF49V1Bz~ܺ3rG7z>dWE=O0z^|&RODOk#w44ХYh#iODmu jgJT090X8XmPjk\:#ToMKAXvvcJٖC|Qk>-ݟ%ҽ(9.وe< 03[ƕA#]#l:JaB &~g~Ȉ 6So9L֣epI r~1wc`Y -ɇnȾЌܬb~Z"ϧ}&}(N -b <$qy5mg r5biT)%eZ6 lF9O6"εjYגj"%wWW{q _|bBn֤ٞQ+3 SR~_K!-yHw=i+ 즈a!a^:srbd}ܭ`+*̧fT &ad"酯WN*9XVxVWi --&g㢓&fa]1-b* ½nܗ' -vO3EG90g!EuĪ&&2.ʹQ%Gkʔz `n1oO#:WÉf◭Qhw}?m`B61%yP}薽|Q.NC_ҩ.ߗ.|ӽ5L{sȿq~C</$"gWd|3<ג^g^yKQ|r IڥMAW"Of[Y1, V»3 Yy4Jul&9S33l)c@kBj(x4|Sm;bJz]Rj .3_jPHkFI-b5_l0Z&X=2adv̜%) ,u=fP*F鮠/gS&XX• =/Õ nU:">NGW}X *գUlm("ʘb*:$/zj}ZG/G[WNcT6V3[8m ꡖ_rl޺:g"LP5эg:*ceóA|nkTǤqٿFG]eI3>R_M~Rf62W_rL6U)2gfB 1\DMu̦u\6ews3urm@s ҂ư7c⁔h@1-$3DOh+s =DdĶېۢ%Zh7:q~_/?VQ iTPPS4ĺX9kpΘ_<¥)  -‹}s;EK@$Cˤ쐡.R癃[Z{E_u>@)3)\8X}'ԭ >Z$SW"O:¤MD,O\|[rCi^Ʒ-QKGQq@M$spPqeS() ~п0B -b*1gqF3ZOybgc>pT[k뜦R%6GrKwPQ@DP*M3͍E@W6 TD bf:wY>pKB359+ݯK%IB} -:V'k3Ecy|Djܙ$&[ ,Es#B%]v]sLE`6b㢜Ԍ冢5-??_Wk\vj&YU !xʵLqncOܷM=ߴ] l,jN )2NegK ̩Qt -dII<᣿|@jJfԞ5Zq:E٦ -VqMGB5Ns-w5s̓} %RAλTJREBI'+g BS6D Ӟؖu!PՄVp1|Vk}*){nYx.ɾ.'0 jV0KAvQcq;Bxy_Ԋ`~z4KF\2ǭjQRtt}J3{Z0aח{J:LH{Ț3n,]~(ZŵI"j!%, )+G 籲.1T9Ve#~8,j ũYo[k ĥl~, -&a.͙D%o3}-*B3v[@Tm#L%OKmp5 ,=Y]slDw WA/VeNc1 -RSY+;9fsiUvun)3( \QR#U m0񫢕9~zep}o.[7ySIkVĭg+:nζ$T?F_h22zVSDXE6!1n5k:;xWR Uٟ&yi=tl [Eؘђhj`.S˯!a }P;\*%'{W'~_l?FV "w&G3/fhi̼ǿ\F]V f¼i!RC!"%\Q(|0K]c?Y,r^z@W5o~ۓ6dK -nz#ܪ6RS6L3@޲VETp8WC>rie.-bB6:)&2%,x`KT!ΞdTwD)msMݙ Yι͉qLCZ uUPeWp./3 |=ZyWx_ݝsbL=i9׵6{{{>&h;]L\[017U?>\=j[oxc,SѓĵDnGGx0.U \*==?U (f*X1Rw.)(MgyGa|K@ϖ -v ڑkv)^l;/)eZ=,)M} p7Lߗ/Fm1 !=J6캾#xU'׾36MWPAbfכX>[2| cW{6T(yI:~WLH^tD}j7'K%T4܈I2\6r?u̻^0٧:jk SNLqpLQ&dP@DrDEsH Pr@CDܵk'9>}_'kWLITUJq!{1+8I&{3 bCB-mޒk8諥*QB^i>Ox>FTqU r ߁q.:9EK˓,I0oG$YC0]Gc\g.Tf<r<񰣱őV- blP_I܃ zeMEo[j.X^gST 8MWc;KT_{{;"+I4Aڶp_ɔ/'9ÛˢuBhE[K͹)cY-0k~˕ev{4B'Zh~v1n=XqZ3?`GZ+- -mu~$XeeBs{ƦAiZ.kS59ۉqJ70#a1yr𑙦 HQ7cݒ'6Df2ĩ|:-:E܃yn -s*:1X׊4ta-O7_r1rڑ/*1zxbHs^nޑppCNB䃃+MyZbsF?)bR}"cRvJ#FP@VLs,ɥhw)t9Hae0~{o>?CrKLDYCq<2+_83MIұDI\cc;4 -rr9N]7C9dqĂFv>jNᵮm+:v.(g+Mru(%_ǙɧҞ~0U9P.ӢlU,nu4TSkKi3ݖ'@6yyR]4+{58\r(^lWp*QdyčE~CII]GA]- -6ԪƘ1ǀ{^@%(uWy;k0$!'|7D/gIXO8XG:L϶1 '|0=  mS?gSBۆ *HVfYjW2V[ӜN>5t(wT 谿B|Yd+$zueG7>5][f1{9 -ws*YNm+cߘ rL42Ln0A m-c%lB- -:>ˀۦ9v= ؓx P\ -#,O9 Ezkqy`Q|ܝ1{431fL[,1FcDbA#ޥ&jLQ"HQe6sOy8!9AYAa:Fg -89618G}wn~C  -zךh[])"f#76'k -΋뷺1Ouί{&ən%{ CICĺT$W/B-WnlEw!ؤ#)%?-?VQ߯wֺ*Ocԩ W3WzQ;kZpIMFf?%PފQ hFGj|y!Pyq8:յm]u1 Sܬsܰ527,NRd. p,ᵱμ: 㜡QI<ͬkK[բ[0J -Gls+m-+%2\kS4~nIl gLl?gC -`aVSY1Q.LpI Bu<-* p-H/*gq5}7?A~#> 6Wu&F_G-wGM0~G>/vtj#>b/ E=e7;+mztgTH%%CUsAޥ@&o_ 變'mI9_ɿx)3-y0YcT&=` dLbs68m4H_[FSE┄y\"+9weC\e3.[l -\S/@.`Ԙ76P[QUP>Iǂ$B=6t?Wƺ쭱ðm.e[.(PILZH;_m> cJ x{^+=?0G+^6n2mM>5=IzU@k8r2φxSꨃ D+<~ּ0! $Dé8 -l/,b70|܀薹Ijo 6t@KB|@/n.G*}U }a8rOC%=[e;i)GqW0 [=S}#Qsp ^enLHMȿZf]6ѩcr:B1OTyS/@K>7 ):6ӊ7̔ÞN-k']7>oQ qPUIDh-73#%)o $^)'f72|Rn/ubޅ3_bOd:!ǑZP7NL[E7EY< yH^m+C KhY(;ͺ2jXfŰhYӓ Tc" 8}kQ2 0Anq ,ao7~ xaL4t{7P %.|1JKt&vff.- K2\MJ2^hk?Q6XfN$ݻ"jU>K}N@O.r:@xR7zJ#=JLcdj\*ؗC&ѿ$"4“9A͞$*˝jh* -{etu3-ꖒ^YАu}ǫ_QG*Vɮ -6}} -W:e3:aro#g3KԽp)1Ikh <_'@?#bV:s_Cν0̢g@ ~6τTs(,L1Jկ.!m_Y9[Ŝx*:xOI Y8glK +ſkŧs{J\V cČarƉoh0ڕHΗ.9*>xRˬjaHSS׉ܓ" PIbSEH?ҐV 5@|ҍɱ\.lj/֨sO]k?:#dlytc# ~<:lh3 -(̧cg;dr\_ii;̞c$1mDMYM&&Ă5VS -HtL)v,X@t+HL;be_{νw|gޣ_@.K BpC(*դu"F{=C VX~tjj Py8rA>L,)pZiin|l  ,m6t6 \%&kyČCY1Ǭp/1Jr0b2.;e?2/%tdHsuDh9\^wo*5eg pY01YBCl;ur" ⺶|PƅdH#gw9 䈎mNM6)J6QcKklWvί BcF2c5D5'Ι͡sJR IAM(!PN-(p(=s#{)Ч(>vb!QIAa|3͊Da۹Vz$B# Ituv Hg>5Uscw6)ܶm;^SͿ^a2}r!pH> -lSSBJ|*#~k -SylTJ2п^xWs.׫ԩkol3F>-> FwQP~MW 舞>'Ofn[ږ5N=C>EהZ|eLOC|7P <|<֘101Rvo1VpL)q:3 N0+?YjbWrYZ${5\_+d|\`UstCƽ9F a22{{gב`1kcKgm}qkyjݩLcJ@M]}9gǶ{irwZ2T $Q@t{CKK)G -ðV%zQ@M9d|:2̧>o:a3w26{ jGq;l_V^fǑ hp]o[7'g 7_-r=Չ?R>9J\u{9r5ϲO,JOٰ[d&Ҭax[GⰦ#cT8EJPnH늮aIPIG8 -\ҡVy~fOGݷoU'tbM\m.[t((;?k*LQ]qk+? nx@$'t=TrWg?d ?\>>)fJO~_l|sB/dgedc&'n!X~3OMAC{K{ƴ5@ɦαUDvD%:f0;[{A m ҇E8PqkߐGJ[7&kH:;m;lU1tFDĦԈM٭`͞= 쩅â}yY#] XE*Q_c=_3_Km.׫V:%^^@-&׻ZrU")\WobvSG'8I$` ԀQ/m~N -P1=&fb6.cp= ?JPF_2'퍣T*z8s0S_d!\~pK&9ks85!߹ԇ3x%6xk!fiz Ϗ!Eǂ]Ocyb}VWuA>1 _xN]?酏ɇ*ef1 -J谌i $ O.lrJ:Oo{3mniڬfLmݼe)j^ -(_LQӲDETT"\$HvgC۟y>^#Vpg+g鐘^{bk$M\O{`C9|!:;ߘ,W~jk*`{rU$P>*~_z.`UeI,+/uU8:]K^W,:wo!=0 - Ůiba~uQXsλ<̳*4YF,;2 PAOum&V9'3ո")7ߜ 'P#&7*^"%њK_JVաQ_4E%DլPe6LjA awSwCƪX#&iEcJJU(L^]-Ōv91=J|_ED:&Jm -;A&?;QW+˄(eAëBv8{ WXql=udf6PSmA݋l9|PCk5ٓrXgE|Nwl<16[q!WRWQXvSjzYLM(ZZAɐ7B THjWRLNw=#-1 -2@|Oiz<0X{qz/z\^Hǀ}Qt)E=~OV$oW ѹ9샬 \ -1uOQNS6F`!h{ N3ISt 怒R1Κ`0r\g# y9>> U 陃g*I1 v㰈vp(E'/Դ򝷘 [xn`ݝVi~eku\߆ , }J|iewq'a@^p_r$yZAs켿%g=s⠺<³SF\䚇!gXmA): - kaq=&>#eG5ts)otK4 o -{+.$"T& CS4WgYFWzg1%rujNYm {^.U9rӋ>)VΘ80RaHZr~K]{A5LJkmp &p;ZrW$&4bzlj9ȄF -ԫ 7XwM7쓨ߒ&m.5af#OfJh" UjC@|cׇ9uzϡn'խ踞 rV_$dpz{rN=X%ϓJx/;OnuT~z<}ι뺯rY6Zr1a5w -{2Iw ee~@E͊3ON+SCj)&ր`y1mzA|>~sگ=z97̱"=bLDS.J0A}hTu K`׻ lEj]zzoEEX_bZrec^^|x3WB\p> Yװ,fd5fti7etn@_d,ӫa)cg,DuA-*ᵥcrtOpIJWqmo'8琉l;[1 %AKJֵ?q~^$x=ɩ?_?#\ )t>x{/x)k: K$h؛mʆN<ەPN5B"bd?6 -ғ*zbyx[ؔ j)+xtdg(^8 8qy7oNv^^~u-ecu(۞6oN[}9kwd%U"a::C)wٕ;ӂ)2$̓n+׷`KCZa,k{,"(d<"2,朣 -#|@,KQMhh,dw%rUaw&TA]/1EA%*D=uFv$oQ|+)Շ>@˂7̆|pˤa5 "i#`[#a`":aw1"O:CI@_&՘2$—%t7՜rPgB0cJMbT.Li 3sh 3'=3%>U>5n"AEN+lP$!č܎*%UfVyXARk21!YDk/]/j6iO=#R[Rh̯@3Iiu)'mlCf!hէ/6?fGqE\SnI!nXMu ?'m-Z0l.WePLiIe1-Ρx3咏VEaہ+?q ^% 0ulKvجB㧻bȿBivbSH|W`6X0Sn(!5 -3I_ttH/#%2V"^V$yRvojiY 8bd6\_`VEAÿm,2*Ģ*aj z Һ?*~u54_5k+iɭrPG6xT7yK*Kw_9sfXܟ@]EBk#Zju:tp{ qN&`['`OAE L̮Qy-1if5;G dl+Nwb['6[#?}r=ڇ0з;K̠ O~̬0G!^"Dꎜ2aG{0;^L/I+>|D5zT)uPB%};Ө/✓=dIv'n4`wtҸ\u0]UbRv1-vHaw3&^w@Zdw 7~9ސ $-ef9.;?Mg:ΤN/)NK}’ZVvK0}E+BHHBH}btFy8眙s# .P/GPweHکR<iDm,n%W=C1zv*P (yI&Ӗ7s&Zs! 9#U;o`xW<) zOc7 )d8gTۃFNh7gR 2G2ὌՔ5 [+p˞M8)[ʑLYCYR/%j]-oci%1[f?jό]LP{ͫT[bMYx&_RF\/9~YPBڈz=aZSF38Gs;+n)T[C(]$s 93- THx^1+{sF!<3x.:8m9[hWVQ -{+آk\IG#?zY%|rݕ1KiVb_ٙn@+^ܜ!])VDx y)}HFmN,}}⇈ӷ2ׄ ? :B?+)dBo-*:2QXЊO"oTy}Ya#dp?&LAL̒Mx޿6huC>^E/ג W@U#E󬦌EHy8S DEo(a4l-n95>@Гvt%+t~Rjn)\<֊_7<\D{hevBR(&$5uߋ#g.Ǿwg Vt t1)y1VSʀO9Ĕ |1p>4PrnhV& Oŵ*Љy~kZ TŽSY=p uv[9C>udte"qM¨`nHhLT##; *w0:OiOMF+)1 ӯE5քQпb{r^ZZ&v4H7-fkz9c7'u̻E2멭 WMf2ffNd]xv0ŀa ItX\7\%!zE Hrtt(^g i4G}a6c%Rb -jܤf #Y[ӁCU$]rN1"%ϱo =}ʣnS.,;F Ī:Q2fW<]"61P9/J#-{/)rs\f5w#kS.`U}M'_dyW@M -Z*"cѩKFڹ i+rw4am>8z/j蒳A5 -N.K8i:4RyD,0p833Y'NY6gZ._kOo_O 4$9 CJEl{Ot7znQx@ah8s+&| )"jfzF{-.?'o*]֡bf]μWؙjvMWxd2NݸVqhz&)]Qq`8Q>o: :rbH|:$΃,I[q6]-2w{@R&IK "s :Aq/VU whֲ 8eWsR&v{^]̬l,v$,#X7C3]7g{E=~(W'ܟ/-S#sPyW$n\BH ERŽQڞp Ae*Yb\~E8<bVQhxq9ca7K)0K;YU0}ǫ?#~MZXi+:e.wWDtmDF#zfeEe^]rY-+>)_p GQjը /_ VD7ʾp1^-a࿑g0ȬCN=yܨqmOg滫`3phG+jϒn+!kh|t 5&Wπ` au0s(KQ=;ӦKJ[Y «bʩOo1aѰaېiy]%@쀛bJj{>.i/(AstMIb{i}LH6<Ю}wZNr :FELOĪX5}b<(E?zK`6a[dATĄ:.#,Q⟮ģPKIv A{9-s5mQ)nLy=tAwl,+FޟML.?UF5}Qzgu[v7'E)eY7. -jim@ -v7O.c'-B<&7TPnm!R%8Uw+ʪ-M!0H9/ND~_qM>;f EsTڄ)p*f`]Qg~Dlŭs,IJGڡ"Sعcp'~4Q|g_ϮښnlmϕxNf 3Rua4L21wɩYϘS֕SEsI)+lv&-F6"FU՗uALd6lS\OMY:U5U=cSV`.(*[ % !B*- %! YHB ٗ{_2~?{y -nkȫ⠀D "mǿ6^M9Hأ垚9ȺL*k/H+ ,U zp)>GԏM?UMAo>1 ۔Q]<+++H+qԜ>4۝= cگ!bI|wK1(!(?lT -GЯMɦK mY2 2q96 U}K}U?-*FvEc5T̆tD -OVI0ĆR6m__Kc&~יG&CjnZ]kκgVfFG̀ܣ٭1=B9FdbC9Gg2ﰅ $j-\&]2rq X!N>iMoҞ&j$:raEowvsUw&e7;oj/mrG.K;= >.iu$ } 5ݣqk}#ʤY[t:Vmy!frA-UV;cFfCFy/'۳9L9Xjg -D# -G8ǫ` ]oÆ꬝Ҟ4,TT+h1ׯ-sNC-ɇ]Dm xE|i? -;I|)HB@d7(vקu`-PK*b_M(Sj&ZmnWmL:\çRN, yWQ+FrM)dwo3W+GzNKnAl|R'H軖Ɇ{)XP;To?8?YŔIۀzP$ao2̯!?^5IJXs3Oxpߧ&=IVMVkbD 6|'}yT LbࢣRD%ٖ p32ŕSu1Q=`E䜬}X:]"w-tM0ϲ{|6w i^͊_iu<(ƨFԪt*{Y $*o:*0ѾU*N_-$ևKjoWm[9mtij,|~b2Ho {˻^Sw#I12d`_OVxMIBJo4{ujU -d6qbzꓰZL,G}褙Kd wn_+h9V[ˢ9]qT꿪[M910r0RZwA)R]d,a@q=#LHྜMLބCpkr@-Yzv*&cgRt m )٭!>B:bJA_P>PzE'\ -i JKۆ1#3cwVLUpwjWXJW?;H-q+> 3!513DjakˣUM(%nҍ;5<'m xu|g{Ew G;":)яw]OmHr\ 0~c7->O?LNOhJsO1Χ|{GQbx[F? p0E JLlNӼV2+ȥQD绾Orqeǫ<"TrHEN)+ E|c\1ѨMD-3- -P3k0>z262vmǃJrIxPY}Q|~OЛ.^vVC!`Kw/ AO:z> zXQkV>;-M%ڋ^%S*-!vk=6%llTy}{ւyRWCoL؆Wa=G`n^5i‚P{I"&VgΥ79PPn PwRX%Nڝ-yc{皣=T{sipB%yjeʥwKf<wYRK9 È9E-PI;qk8KY%C5}W&pa=;\oW=D(JϢ+#k"Wɨ=͑P`|b`W~ El^00Qْc{xwYRu~}Q=BIOCQ}DzܬjrF?BPA*?qѬK(q+5SmW(;v JTvnO_S3 EVc&`e7T qc#dwoߝٗ\.Az^oNݑ4Aq$#֘P^OBg;՞{k?6w*Rz ႰBlWXOCrNSt_BPGN.*X]갆Z%gwg;n6irFqO>pw^ł CI Q7ڑU]9sEI]*&0fFƌALYxU5j1RRA#/j?N %G(9Y,i -cxUo׾0L9C30;e3eߤ^"x̵?/g$ܜEU5; 䂠N%̂2g]3W3VjCo.0ׂ*6E{FFzCHd,b4c b~-5mH^^{4XXA>9xsI+sUr7$>jb&1$3Y -Evb YfgO)3 -SbB[m4ԶX5)ԨP)[f>,|s>w2ccǭKc]?̴gi闙>q;Ο&cN,]όPxꍦ/SCmzϝ͵^|08=Q\=9u90xgg'u]p+#N|TG^z9\2AYnkZD 3l cص!g/,-T+*r N(ޞk\I=E*/RqZ;Zf҈Rvi6NU=HR*;\8 -N[HA5~>Z3->iWV+U{ӺV5IҪx*XJNtm[.Cl1./Y!lw>stream -HW[o}7>HJ;QRcs1"'bQ8lhR!%Ql>ly,Zw=|OOItzYҕŪ΢0z/"Kss5H~,e6g5S~\VѯEW|+kx=/U]BCb34 -a3Σ7AheHP qnUs>T4a&DWRB2&<e3ev@P`5,B(ܬW۲Y]tu\&mv3̸syWu 3`wNU *,,np=[ -Ͼ=yϙzW]^7ӓ}X^ fѻO eW\C~oeExB+~kc֧g$NA?5(mb;FE]4E7N,W8J'"p-AFSGmUDq]6V{s41g?n?u\ݪُ<>]?j^~.۶vV0Wvۮ:&vInb>с'm:8vaO&yxO?(3^>=޷f瞭UQ{+})eH׍lR^S;dp`$ȫWB y]4ǿGYOO~?=A7pǿ'4zvw 4G謁*h +1x+0yPxگӝ6gﺢ)VuݟTu82qq IhD&*щIl$I,SҔ<LUSԥqiyF2 T)T3+x82D&.O43FikMbR gVYm=s6MmfsGuq'tigu.9=q\,汈eb.Oz ELX!>i1Ox3Ë҂ !PB #p" *9J%BJFZd,?J((Uke*DS4BKF[t ӹ!f'<R b0T.p 9 35C:g2DXD6jBfJ8r!c)-Y/;:Fn#>&Շ~E4)h|b8ID,_f51G$!)lC=:NQB55%:SXzܫF_ U  e80?Cmf,x~pp\urA߁s> a= Pe0h& C/WFҀp T`EeHtA4Mq[Σa(7Fa1*@< $[0ۀV&#L8fP2@ :y6} H 0t -" @=Ǧ&Psmhڠ1-l=TT-.^; ҠÅUKrt$Z$0:ʫ@}:!XNaUȂhp .٨&xR:<RQ݉ct L+S`bBTt L-S`r:*IAϠ)A.DS&hW sczSV۠+4tn≧]hw7H B/wS=0^H~z)y]Po AR=$DvOye cڿSmD"KQu} -C>h! WE${Y3Syx_{G\KQ7fSQfd/wɑ+?iL>LR-ȒX`5Ԇƽ|Vk5 -bkYY|ĉ5o-ZM:2m]rkq̶w8GVA͉0bȟT.\'gaY*,Tz}<Ég{tBlg.xtiZ7r,<0L(&SLxDA7Dadn r9%]MSL$2o {B>eO0ɍU MF‘Ch(yN g([S;%aNf,w13`U XaZNtk[c[u5Ȭ9nڬ.JEn˫=qNyCmq7%*ܻABu>Hu[/^$_^@vU‹JڸEw7l('N.[(vZzۻk?I >c]%hq%Oyh;ю̣Xk { @`3wfP$Ϣ".H$FD 7ג!f`DčCm#$,R1"{e #tX3V9Q Iv+9rIX`(`KU [R(Ŷ#4i3fW7CfQp5ؚ[aKCjKP!*#F"54QOMfc߬ (`}Fa୚SugˆuH\_o#v`X~[?%GOHwct8{aćlCaċwݵ\K~X Z$i۶kvh ?݋ 9q bd1N؎؏8q,'Utd.qcq3MHv~qiߒ v'r+Imc6m2'4[?C=h#Eh7'5O9zUc$}XSv gq1}H";uǥPu.zi);uC{|7 {=hN=^J(S]Թ|(*c] -+m: v[UˀϼI?qKh=@_^`'d -w6UYk ?B[@۟?ׯ/_/^x____WmխZa؞=MYaC،=-Xax6 d ɰd\XrhvdKɆc"(W p@\*`f֛l]7Rv4b!dY̒Ũ.\-]g%K#Xab($ f-Y.60jqM5 -7Uer*śjLJ]VҰ#J(\NOw囪 #O*?VD -P :٣s0*AhttH p8T* KؗM@ؕC8~,>3hcp9Uqm j|U0h74 =X¼PPѱBvFuGUk|* g9Ik˙W,JA GAÉKjDaħgoDs\JL^KICvR]Bp$Òo%.o{i}wswLWer&o -Z`Ӷzg 1kAF81"\i}Fa୚SugˆuH\_囪;v} -0bxCWu8{aćlCaċwʽw?FZL5==A[CCM%! t &mLČ_2{g:,.[vs% ( cq3%mrk EQ#)}K~/^؝ȭ&$Ѻˤ.'}΍nXum۷:Pr.J>WNUy8}W;sCm.3ˆxG#^omp@KjZr!k4R7a4RṔqOMT5H`M4f`AY*ʚFLL)U֔~ph4ݳjdDא{T)פT?$2fSRor.g[di;ksmOA`sŠ^u~seVMkҴUu_c'6A4(NR^,UX17Ҵm_V)#$$]kjrM \;%sT(虄1‰o8lH 70xy;H~5PBz-pc@`Vha?7Ja[nPĈXnr@߈`( -Hc"'22CR ؈1G(|.'6c~P;ˆ:PAuQu3GH -mHZBpd<hO&m|×\4?x%8/Y?Y?]D+8n D&W%_bOHez}YD[/ H\%PLP֙_fQp *֥.]컘yq(JHP# -uTPDI+5+2u-r\qF# |]r+3ZIyWY%¾hVViEZQ/xWej fW?[w7v␝z᨞|B+U:zP}TPⵆKr%{u>q/}^(8*!*V˽Qc-sQnnQg%cj> ܿdy dɤRy#Ͱ%o<1 ͢Q:VO|wus? p,og~ ==jgCd(+#zt@V#?: 2Fanv=Xq0T~zȟg=쟧vfK8G5Ox^#0\v@DUкzxkǰT¢ џX?!!>81yCX4 #" gE[&俈"-Rdт\gĄy]ICܰ+v}7:mVuв#cm7xn#n-SRO0X/RjU4 Kv2ׂ5!%1:av}|ŻV%=E>O"&LߟU|D&gq4Dގn8VQNTәس\?,dk$jbŧR.^tM/x0K8S V2=ΰ^ o$v]`hW8 c֠-`8Rag S\ #A z^%SeX6y2]S)OyKy%ou\- D X ft-]aP7rja_=aj'F%HNݕm2PٜBYÉ 0{ 3hk@ə9ъxTZo^3x*^7}N۾`Šx+.W/w|F ԫMI_jiʪUUbQmjTaU9WoZoRՏעʪUUThQLFQM,먲*򪠪 ϯ/n.reժ۽2[t1ɭ߫ܜ|ݫcN=6_n nksܗJ#~0A8X@I͂{0ĘC&DJ\\(9&Cw _vV7A/ Q/L$TH Jbݺx }$Ai4w_FgFE{ιsgMawB?j2nPf/l:ab9>&w]OPw;4y}N{쏴qu^3Й>?Yٯ]]352;|I)QJw|g>9u%{⋪l*67Ǜ0uiOjH̼ #DLɉ)O4=V5uXVvRof!\>+'DAbP=?6Ș>hhhqz#FT=jZ i5"=",U:bhA%$Ȅ '_yC -C}.NoQ4c. 2QWaGՈ2ܬf5Jră9B2tnW`"Xax8k4qB^aKMj}+x=+!!㑸Ά>h* {(<*MVqXEjLڤJܯXiҺ+i[AisV['NLJk71v sN};SOOǶ :g)8oȸGҪܡ5H・nMnWs;R7(n6LyGnOuvUϲTGWkcK SIKIiKVhG6[ѣ+C浒ۤ͑+v]\(I|XA"YzmPbJ(Ul-`+O7R y$D!^WЫ @D`}2MC%xZx6Sy,V3(,Üσ;ChVyF8 r*Bby솮(x 7=@m{+CJQ)_HRဂΰR$u%EV>R2> -+@ 7A\]S3 r]`FxG x͑ WKFa'~Ta:L2 FQ$ R{UϹdI]kv확qlF]C LEZG^ ( MIZG R:gZoFծ(ߺ})F.֖7~nx5P2B2ᜱݍ9131Uws :43J.ph\oH{\75 E ;#K IU6 LEFxC;Ү!gJͲM7} &H'Z[+uW-1Z lqHJyZ>ޮEXdSڎ`%P voЅM3s{'wr!ZֶPv)ƚ9EAdjI:mK$い!LU¥=QxE^îFN=fpJR۶([NBpo[ϥc&ѯ$n@R2>1В-uo_8sIyg4crW .>#BU8[0Lj-K\0ڤ ;L.hS QѶ,cP[xd}{R?^b؅ .읉N[=|ɚ݀HU Bw%/bl :2ʶ8lCARN]j!y ]F 4lc9/qEHC~ѱofjw_3I:$mw~jf3|I - cɨ+L1 BF^ָݯ@, 'Otpw>f?{; 77?/>Zy<\6Go/ˆ?"xkƧx_J|:|ou:gH`ORD\Lѻ}M26_8fSHۥyE=SJu~RWP( ;uن5%ΛCRpg-zg(wHn -Mo߰\zTb6r-%&0Mu|f!X4 n,P1Ckݯ|} M(Z79-Yڇ6C@nܽ\MyO![jm2m@78ECtͼ~la.IC)kdi~{6;%diV& d̹3rRؽeHa>$BjX;.- ړvA|0r&mHYt!4}v2u+`XA{r'lpq:j~`&loy|GaóT&zQީ`7jxHt0,L=Heqpڜ5fMkT-OXJܭp]S31XNEGQToy1>|d)mfF9[9_eL1H8[ֶlK>| -slm2lQ5B4PQRP[|5s9ZfyoYka1AIK׊Zih\-`B>]Wj{ dEE3,4:sb vε.w蚹tI/JYjf"$4gLT w$\׏lzѝ%;-$}WxTx1+0}PVVH |xbyzS +3 LĔ rf\\WtW`<C*ݸRc׼=tj+TaۈKj,k6r1gkkiFg{wa -O!rlW>=6&baF<tj}zWsu??镋3;@Zc0GeC2%;g({Qf:"4*(.<,xE'ǃ;.rG40C4;oNC襞tuqVqfq"W -~hX5Qk*8CK<5sh|}nk+=ve}ҝZ=ygPJ`/wvτI>V>mO/b] `1gVumOI7*"m&R{arFUAxC,PNSJ~)JmsN"Uݯ~_]\t8'}gsn Zc['wte85["AQѹgq Sgqz|-wPW~-Ln*Ξ6O \VOzYb_G+RJ*x=AыPq:#vmڞ}UM `"B-LN}Kfv}YJY=d}n/co$nboqkd c֏썩O[=f -Z -!-,Yn -dZ˚=.N3;H;DB`uc}Sl8)+) s4A%?u  -A?H6)E|i@=26+dAz] !i촔Z o2ii s2ä*O<,|h][[ 0"@T(=Q1*}=;#3O< TpƕC׿s b - 4}P+lnĆL E:]z6? h9מLz#~D:nܛ -R;1)wĸ1Uy{;!|?$rŠ|OɡG(9AXM)㴏6tw`muKC_eh7(./@Q -#Hsx,c^'ާ-O3eVV0u^q)|K}}+NPZU'y/ghwSq(lm6 >y%f [q%v̂R#E(59ݼLG/K$:n- -&'"9V,];%r;ĈѼ6MqccE8vl?MeFSߩVmJL%.z -DJ),%6R=`Z4܅MXsEODthq-nUNHFSٜ5{l$#u5wZ(u{J6?LP=5;R {NycJch5 !.J6w;W/6 Ʀ,uwX򄳯;~Xm|4$}-4)O7@^~K1˜Wh -n'M u">f[ 3ǚ voTE!_;*Luw΃uñj+Ɠ7]KHF{K;|)xXz/Jq-HQs}n=p᣽ *w֙|1O -lzé*g>% s v~ c`Rx10BLT1'K0]ΘB}YMnAC_|eqہA'x@.kg`dt8áM>@`6CfPт:py.dJ8A+\VؔUhP0p0+Db4cq"b8q sƎRW`6[Dsa2̜3Q_])X-)օ.=[Fg|A d;{+pl4F(TU=/v+C35it]CG 8/fg )YbܘżA$RҐ{ȋeKt"SRCnCZtQN3t0+\D,0 D0N xQ%ZB'4۵'a( ,䇃H4_kr?]SC )d6.=cPlB&AumjNXLJ–Ԯ)@YtOz޳B{dBIqPHAjp5j%LW4(f:SM'I r40w!ǼqQN -s 1 liR160Y_f!|>iE!yVXΛbc"Z1jv e$ۚ#mxdxd*lozn5|J~iC^vQ"deVFJP-Q +s\0OZGW{MǵKɤH8Jkv@г֖Y9S$6F^mLFCR&jO*G.-'K -_ϕ41.PRW'6b7_[UX1 -DOyPhO2aA,\s_ېˬ}f$ _ʪJ]ZN -_3}rF7tEJCYQuؑ#4'Bl,B;:.ٳxBĠc$P" zgC"6i|KLiTPHv"^O\㺦]OvH,T*A> {a8":U *^ -r9egN~%%`dfsp;'&Ēz,Xx@5), 9v(rH̠d\Z}颵{0ח_[+'̠B4J.mN -!1 -Yy(0,^2pnUcWϐB°H`sf>_ງ"}2lm]g/0" tCm-JD0ӗNr_<#2bٻLaT Mnl?[Ї!*@( ,%3fVv)YuaZ]&(II=dEiK31" Q8b,+wcD! x 5R,4}ݳq SV]cʧ#BAm-ѧ@JF}Uxi&yݙpDxFLe*S9[zATmh/ǕP#* ̣qFСL Ķ%wM63+%xQKN,%ónCa Ƨ0]@(:KCЇOznA1E$0]k^}9'HN;; 0B -endstream endobj 30 0 obj <>stream -H]o۸*7FuNq15ФJQ_dHM[ͤjm̼dW?~ZXv&KYʼn~v\ }l2+pd!x9"?0vVjTG5'K5{2ꪇC)47s0;5ם♙%&cgʡ%pIF"(ou'Fy;zmj ܃\Lgf&Qt_s`dug]Jbf$9h @ooOU]W_pGA$iIOҒlDMة@]xXeyf5YK7R!C*AjuL9-~P FPi1c,؞GW[ͮs*yxɳXsn+!-8CKNNvʁ k;=69q yX)v%$3dhs kqr/ʉje4͞1'Zc"PL) _* -ҐEn[E_˾Uj<FC\ZVz"^oUVh/#'~VVz1/)gF4i7.Eb4>12Х)_#/D11yzD1fAHޭ\36MozXM/P;dl2+T%10~8Y t};3Ĥ@>;3c=AtQM:+D67 7JI׆6$fbq#KV]ԭ'3=4K7ine wzא'/vK Iep pIyDu#q.fIǀ{l# -70k̹̂.K鴕 _lּGm1j`?(\KW_D%=SK/6Nf_,"1zs{W#nf/-8C7R/ıɬSİˉ ʆJU:762wtljbКJ8[%Je^&'b2 Ez+tj:MjԂp.L:~Y/f1,-H 'Oj/¼)c[ Ld:]r4i9&p7d*o 7ХRbxxkn^ZIל2=B 04qg=C g1hj -34{2ٴO#=P3m[lt|U>񋏌r{>y hgF{Af%'A}H]4Ĺյ+N%[ghv$MҩE\3I*Ei_ɶX̝d6"1=Fxj8:9 LIP(q "`!Wj56яBdLpb|Az%/sOt`y&H7QgXވ銲U*3?N1\8uI-g]cW4Q \Mo%[@C[h;ZvWiL4PuidJd_ RI'b2XZ -k2a%ju>N s2Ԃc_y+@KF*9R1d/0$Syof}N(m楕t*S#ԛUw3 )hy֨ƨP8c|O#}Ѿ'.M4ҳ e(8C. f^no޷>񋏌r{>y hgF{Af%'A}H]4Ĺյ+N%[ghdi*ATF< F3hp!{¤MOmvJepҽpIyD?}{; -2ӿ7M/a!Y6}sQj2IЂ3/EB -YHb)VM|[cYW+>Kb+)+ t+(HvTQV[Q$4*G6n/hs kѻ5vl;mGNnKGp[ -5Gn`2W7?ě _̹^k^+ gfNuT*V╿KTUGdz !C\!@+(tJ"d]ː5zS›gQ&Jjz(q h -^!i56,SPRR/HPgb:n|q#N󿻝sIȘ置OfOMf$BF'(uVעU|QNTTTE3S<,WlVԺ/Xю) KS zPl IЂ3 k{Ƶ,$ >OYbR*QG:re YDz,3jc vDG jY!y]DoDo.cz,}˜;YL֛ii?/hV@{$ziErH!N;'yFEh9B'xb.h3 L`gx\VҲLE}Դr83-_e\Rךse -I0cʥ_:%Dt.eoiޔ&x.6})j'@ RH4rn/pY˴WhF!YBTb|Az%=u/Cr"cD>oON{uo{Z2mu|ښa; TvRHҧ؋բ9 c[SY$N?ʋ-|JjWKO4/lufA-UiQ>xHe9ٟ~%:ᇉ5-G-Ȱ#u2|#ԪXl`2ЁO^`uj)E8Xf"R,|]mJBbXoOw&hA7Rr8*anHz&$;DJW4ٔ0`էGMf=C#q-0 NGC'ixiصʴw,Fkr*h ݚ3)BA?*mTqؼpY(+PtP3Wpg[M)Ϋ\9l%=wt -?e^a)1@Pp`R|D k!9{T/K/AhkHTZVluj#UiacSd+()u?cfy*ZYϫ'~4 -jq_+dzԭgV!Pf+0a&Y)&-P%'Ҫ€WO" #|Fb?q-0 X)/>Qx2oӰk5iA;>jYƖ$7ΉF ح;P0,:zaOWIl4 rFl|t؁~[PbB(h{+|g\ï̷癱=SW׹sJz{=?ݛlcX -F(*;>Xo"QF6#V|Ya*83=v,m>Z -endstream endobj 31 0 obj <>stream -HWmO:ȗ.mIJKaKj-;; -C9gA(x$;[#2xuvXSL G8 vO|gK.3T7XOՓ,CH@bu^gù>;Čm3pžo9CjS&ӀD0ȜJv'( tf'Eh;ͿXP{^]>+ykW\,]Y/yA* i}0chDl%DB O[59Q51J pRqҼHYWgb*='_V!Kd>>Amʸ(a% iZ )YR^F{;&NN-+209t ) -ucVپkc}V -s W;"֡^RUϜJ˻!Г:Yا1-6mˮ4ڵlƈtA9jfS'6J kqKwgȂCⳚ'_V!Kd>>Amʸ(MGf] k߯԰bSmJZk2 H0yNPb>MOF.1Sj0cV]>+ykW\qj&^r{Tzk.b 9âfJ -7ӗ9G)I,(_b, -jN8-7a+hQh ŅG6Zzn5- )D JGrEd9P9J!e0L)K b H|,Q)D b,1qEa48V E@@Wpb\,k$vaN M $y _@LaT_6A|ݺ@f7Dt\ǭi6ԸYX8b#|057Q1S,h!͑'%@2mn&0@-;1#W,1aF, !qWYCܝ-YS!d]-z61`y ҷ)R&/cȘҜ,#sWD - E+~'b|J}cppsּ]180ϊ' F QP5al?l%@[|k"̂͞ᅒ<*~h W9R+m -CFԭ/b|xP>̷Z].Bլ5x {}35p#[ -4GgF~Z? q mtsOd1c4"Y -,ո*_ ArEDQj"1gq؁>D5ڳ@X $qļ")Yjj8I^2#d8b߷|DpE׵$NkdKɛN&X,Ic8GpDsX]V k߯WlTd|Xrΰu\ӗ98chk 6f85&㗘 " (@*D9o\!T||B\qkŅ^P6 [qLVl6I,(~^zzzcxm_qE /٣nM*a`Fe6Y-$[ l4Z91Q#hWBOn -S1BLC -c>m|cg,_E$X!ymV .oR? ==w "!fwI ,kZUe䢮YX)Mty;~N?ܚi՗`nQU<}d 4Ǖ§oax(>0x[8m;8iĵ3 -G|:WB2ādpK^IF_)#j\^mV׺Vur5Ҕb5~^үH.[n@E2 Hi!v'( ލN -g'GsF?O:cti?[sΖ9#:)sM;a0 -wRVe?.gP}s{u~놇AS^}x~Q{ -}e)V2пat rWvAw0Ϧ >v{CЋ~[WAgCv'ڑ#IvUHreO؊Xɝ?H]٦*?A!̶ik}{0S{®G>1= V[Y?z2qfe}cAގ ʱfɟs<_hܧ"l&"Qz$4v5Cr ;'n7bHpu\XO~K -:G%?Uynbݘ3bٱ 6s =02Be#Pmh8=v-cug9z9M"[r/xyO+B Nʹ2'im!rPMl:Z(3t>^ce -$/.9TׁuQ>d>(fAtpbEuoC-@.8OB \YZ`;a_'St U\QT, CA^(%yShcC(Gꍼ+vN(H1e3l] -Foyˍ@F ]\.y] $g)ͥ/$4߭Tt":`t bX24N.@NaA%5D&7"Kzn`dQ%}1  -Y(& -Fyp xcfWCkeRv ɘ+F2b=5S9J)vɪ!@, ;۹g"AU_WAw -\v>mVsjdNt\{Y9/SOB^1(x^jhF/\ciwNT !*#z hݺ5f_"1/ENV!]MZw[Ӊ=D1iU*K^fKXhr3Vk*GMLXJ-zM:w=Ց*A⳹Vۼ>o -[^w `B/ufh^ڜK9Yœ¥ Ջ Ip6?UF''E1K#+z0ާWD]@.[ HҗDG2^=^{'/;1^y1&`_ -J5+UG) T &)g6Ρ+2E9 -6 -#tqoʋXTg%q@̽9A;]/Q.FͰkN1~tSyl{_V}/֡G!?N)'N\0X4%IJ>@b}:[<\!ǪwAi:)=7Pf_;F$y38b>?NF@hè[.Dg[V|Dު_bcr\o.jLe]BQڊuJ)t z20ʺG 6Pk:ksU]#ҹ}52*<䩫U@Οz|>[9i'zKO#}n9~\rK6)_~.wz,Sg`Y\@k9Sr)RXf~E͹B# 4 sqo~ 'Hi iE^c;~T\J@(L:ϥp\5";0Κi -ğBBE9" $$egDjICByD`/wj;ه͑ ψroC٢Y IՠF+j Y6(JE,>oCߪf*d, :qVLts)PJKML2k7-.MiQ'c)h<dAe^!Y>|1=og7ؐ(] 8 %aPr2ʃQ$;[YAR(B0ݕr/Խѹ4&o$j@4Bi"GHEo6e;wh^˜V5}bp_:Iz -c [7׻Bt<Be2rYPgM 'a.(e^KD碨qnuGgc6I" nYWvEn=*tA<%MMdlܒ{XX'ÐpcCb [SDCiBƟiL 㻂⚷%O FE0ФUE&QGZ 9)\*-хG*WRcS,KMz*PC5a*F(7나hTbC\ӿ C QR) d&(o+ko99[ ԫ _'C+øIO삆.\z.~?)?HLvdi|M9UzijAr)g=$B*b32Z.]5De2dR ݨO!Hoԏ.+i]z -6, -]쉊\/UIQ7/3[2Ǎ# |]wcpI!fb5"5Op<&ȘXб0BuZؙ4@@78 5$9XRTGd("=nݼW: ZFCH9EG^q6Br ڂ$U$BqLǗm&7@B2qY^ N(ݛ'`k6'A n0bFM '6" c6'aK6ؙd\X~w}$LtWKT^o<].e5gW:zY*UADX :{ߜ sLߟ13_\fSyE]rӹJ~:ϲ -5V =W׃%P."ⷣe?9jg^}܌"n4|tCU[pMRJg*^҅| ;`MGEgAZ - 80R:{-"X`-X:Ld;)b%V@o(sRE_9ɏ95e tq$Z?+l9`$ms>stream -HWn$I(d>̅ܵX[s2w#YZ]G0YQ(5oG9<#zoSq㩞k?gp#Ѣh1]ORůG`)v<\e\m%iҽY̼0gc3U5nZ6֐Nj±֞_9J5VtFh&u;\AK)c}oE/*K-6pD@ku%몇± ² -s(HŘ60εf g(M/{Ad -#ysT+}p~O􀱏H| T|MGkJw-0fb7|GVFs3ӔؼrSLF 5U9gF|F9.1B;=lD,ɚ>;\ՅW9`Ju!w3Y|:N|G#ZzjW?ǜʌ#D;K`g2;3UFK /u/۝K*<0`Uͽ%Ĕ1c5B]N&Uֆ`o-1q \UԑdFXU]geu*y`tTJs^ @$gꋭ4 0|-zj{MbCfu1D xg!D3YJwkc>Ai1̵W^zMdvgͯTAХ&If6* wK~+ݠK lz|+2U=|{e^&@[1Ʒ}5Eæ&JS6;uTHzQfS3Gҫu;&)IRoA'%5Bq"yZrBBh8,Jl*S3N/`7N3/MEj  -tj֙&#$q~ݸp$=?5X,4f LF+Rf͑>VI:W^4Dʿziy˜_IE{Wm6}jP=<#\D9FQݙ`B񪑍l7LQC κ2 ^3!jknz'zK])" n"!Q|Gk"4Ȳ!htg&J0 +'3N3NsPozeM⩴)Di -( um -XJ9MHo)rۜVKD$^ųda:E*ʍƎ-sd<^7[Z1'#&CM'sHzԛj)&רÁ6+*ގ0LfA#Ҵl)&| K\'օٱU$w# -CI֤2dV:^ȘDnMY8H2sA{$ Hmq".8Q^h6d7)M-11d;R\9=Z4{|h+]A):L<+&\&qMbkA5 t6 Qh+ouieI{x (hpN̔6Arz2t'  ̬jmؒ<[N, Щ4#HuqK@ -quXד7=/UM7R\RYC)p, -Vl@ZkH3OKed " P4THȪ`Ȃ8U]`zt٨NR#QEFiz+~p,)|Qp4HfK=KbԔD xI6Q-\7kWErvO-Ju>Bms%haE덯>P]z$^Mf˃ž|MG-H,9{ڞ>"g2y{Qάς>'Ӫ<*e܄TPѯdoLtIW6N7jŎyDDa1{cB9=^ ߱| ?[7ѡP׳cZPc9kb2ΫPzZԎ ՓMH3qtITN!_3=?Q3 -bn-neAdʝ& `4윇f*.a\f¾ZPHü׎ ;yh&5iǰBФa7ͤah1Mt^`?|Fei£wG/pdC; Mmg+;<[唦B5MpHnU9g";νmU;~<8BºG% |$(Fo@6-$+Ѕl߳ {ڂvxğAHѮV VT'K[ ,G;81 v~orr5ZGb|P(2?iqݕ@f C1aH}-xAGmۈ#:bPo}*Vie3=Q2u% )e31Hz1t mNږ~p002:ȮmlCGry{js{R r[JnnӛXh7̆CL qqaC-k.#nk4POʉ|5PdF([FNR!Λxs>c]}bDh⅃PT˭M9͊]c*ƺmg<܂h -ۛVU,ʝJGL)aoc[͸4rԐRCJw±xxKמmh8MNc'V3燧Ϟ޽]ln?kޝڞ͕ӓfz۳09d -O|;l[} u1g$z6fQj0;62l3{ׇD??40N83v{vL=;ޭX *̕eyhU9;kboѹ e -Ff4.0w{NBIN.g*N6\LbxNn \N.fW#p0W;t/w7KJM_a3Cለ`) Ҁ:Xx2/exX2)^LۤIMpbHq<7'&o.zDO_ٟ1w ]p}'Uڙ~֮ukg>PP]UĹeǮU(AVQf˞B,3b,PR~-[iZ bW,ɣ4:kP.') P"d@Bg1ٗ'Ұ]Xp'I~~:kvlD PmZADaj"(U;F>azrA,pwn ;VށQ^_laPM^LG}r;yw7ӋOH'}ƽws$bsYJvztOs@a㟖U(V>_C&k$ {fu3OϷ7[={yٞ~Z}E[X={Qk}vwwq}7w\VNnnm]]uʔo,w?~~wse嶙}{q?/8# - ]<ئ@!P#IA;3ݥ73U:ӻ4P~Dn)ojw|uZ?# -82d,lBI}i'IY^(2xͺm%Jry:!&&d |v`D6l˚Kڮy\K7Ny]3\vaHcq-+;:Wz!jB!|fr=kN9#{n.JBՐ -g:מqU U1q.9LbH;":uh!EjʞճnSQ uf}rմ s{& ? SySϸY V*ڍ;)`ᒯ@€e5h^c8 Qk]cŝ6*IŠ~l14~g5Uemں!djk{Lc]]Ph,q/h"k+GZ1qdոڶO?؞f9X2n; v󆸪w(rBmݻ)}OL4(;6[hud "mzlC_rZa:E!}m!vha7rIlPYz(|z#{F֛iot;D,efz|}Bϯ~}-u۷tpwzOyWԍ;w{oV[]_B&sdzWϞ^]^~;ޟ}~?NN__|W~|˗]W} ٨n<) #]!0$7Zѹʴݞ`jUYmL _\sh;FۋXhebąnR%r`OO VqV#29a:)'G^)\pb3naұ2UUR7a &]Kzi\H+, -kb\7ӵUΪ"*fUƚ9G+/航])VfHuqלnTHrwEո-IR23TЕ~яvU{1hp8VX36C*:@CB HUc#!W+|V=$o;ut볋ӧ?:ѳ3zዓWG/O/[\>8zS_9<>ç~8zˣ]>e_Ǟ5D9T -@\*A//3Q Zd+`yV-ʀ|`zЋ -:@#eˠW-JgDCiefWl&AOSȏJn:P@"b9 ->+t'z -&J~I*4ţrcuRa)"*U:'N -X\N;®R5>Lfp32^< s%8W -lÏ`~-Qu",`>FJrdD ? A(=<6!F{GƧmL8e<[ b):@#.f[Κma~e8 LBOe5IbGM8zTj5 8 bv3%j\?`yrƩ)0c)$]d-yݙq)ׇ2pPQ pg@0 ;%Ռ{'`SΠ t'qcZW9ƠɁX)vl`*Y:GSL$eJ@7$a}ZJ9hMՠIg䳎eUe\4☝QԚ dM_Y OQIs"ΜJYE?z}HkRRf[APB;9Z` -bI1cӺi! -R+0&*T c̢AjjXDذsB"tKD{HKJ3qDjaar `i\2w; fd&UA[?0ܾ3RR -ohS.4p戕RNT6rptRu3)OSDfwIwlIJ s# -6R -Q"sU[?"MoCK0fU7/dn>5x0!u8 ED-Cd)/ȢaLܓQZ}ABiK[UL ϮsH#BsnWj!U4v moimЦn#Y6YUFg>cFk4p^iƢ7w&1AMa'Rۜr_Aײk` AȽ -3EϙX'ЈJףbm2U 2M{b_TrI߮#p_"Q!>iA0Q nl93{D\3gΜ .DЦtGZx;bA: _8iD%giLJge29Fh*J 33 -b{L!dD!ؠX$r;) )L]GkAFޠ? 5(]7чFGidJ6;iI%xUgrAh!S/wv }]"^ߕٔ`W ;k^qRq -H2*u%B>/cm a3xT!}^Ѷv(0 %g[qoh:xu‹Ru\+FvZGE6t)=>߅44am|, O"DQi9׃#U3r:*M8?٭q@CА8+ & 5 -Ce(Ւj,(6[$פj,fQ܄-Ȋ - MP;9 PVC5@\ - qR'C 4I9Y3!4"ެPVDeL|a450BRi#/8.&bGZqdbƀ%c%p|vAlP[`8hHrTL%?*QB0UGR*3-:CSc0ʼD|LnS! Q#%+>E1X.v,+GDiaVr?EK3M*bK#[_zaEJ*LDBD(yN"n 4-eU(`iAN2⾉ajNtoఓunWIv`F!ƙ .k RN4, T'4>/,iAцaBX WQJ3<"|Rh4.8OUZ3kD*Q0ϣy(X1Y{&eqG$VP|vic^ -dߡPիoNnᄑ<|{svi+mGA1Ŷӻ˛_'ON?\?{wƵ_dӧEo]:f} yZ>>t~AY?ȪϿҼ{קJ Փ_/oOG/,O8Wz?O.>;tvO_#:lg 6t+4Pxkߛu7<_<_yw+W!=J2}5o6X#~3Nn޽OYɸػ)kտ(v?}ʚ}'~}YM~&[~V~ޭzyUoR.|o~ՙqqXo~9];A~Fm -%2z,Q=Wck^39 ?܏nQY*ѰNӪSM MI'M%^j7QQZ`*h%c\McgέF3⬘E*s\8ة*V~ȟĔH0@i0 ㆾ:tk(CBN #=d4 S7B܊,Mf+IP8FF7*OSw1tIL6?(m D R/tRpӬ>^u-jA1m&vWHMkdbFsd b5b!MOC*4&>ʥ[HKG1w60iʦ;; XJ ki %UpJ?CR7 ˭TFL {JJyʎu?Mzԭ\gXzL|!b:XgǖI`Z-[(+ ]L_)3g]ѳodƗ2#YACq}ʂ\ +*J[5ǖXGh#a+) - mњ貚rCԍi-WMy=hGhLn&mm,9at؊Hr,4WPS[RL8LBu@ !IJ36j[n&?TVL$6k`w7^,aV6ӊPz#ik!56?a$WDZ_m)>xB蝪ƒ jtZh'}˘ k[%cշMbU2L3Bj{ .jP#<@k(F=<2zV6-yիŸdk}O!2Xu\P'6i:āxGMIolYg_4v=~3Lއ=β[cazt]v}8]弟|P9kQ -7uY(+t,[nR)%"V| `:I~ -2No3=~4NHY1jL U_qk4uꋭ>`LC쥳ygڸA6drd鄙>4)ЮLMk&^.r~I8DWeH}jo83. .[_hBDʲ~#!9>|q\s%ts|߷:>v)*0S{TQSQuLKKP*E熨 -}ءeNo~f^Ց9_A2 t<$䱊H21-hw3)li;l \ZAɧ,h+2]COZT6m$ -/yq:gXozId#XhO*fXnٕ)41wUJrd"gBz:V -[6H|#4uGm}ev Co}^ 2DbWOCbb51W*jrEf^TxPOi;h{StxކQTg>sb# ݷe4"6 -)#t(zWܤ ,jm\I=Gr|@\i^ʳN @R|kJ)S-?Мot6 \"8DKѾ|e`TTb_#mPubŖYͻGSX/EQ2ڦEsƍu0"5s诲[p|,Gz QmXT}ӑr\-_дdA3q?fMH=Bt -DtO-x0oT'HL!h:2!uyj" w#Yݻk)hIڂ*fϴZ j;D/KOlkhY~k?e#6~XtOU_`iN #yg3e]K0@T4s jNdyEQ2R -īz+:u[B3|#c^s7o9\K}SV=Nuh$+]Bg^ӄ'ON*T=ةʘNSR"[~{A6uqrճdM+B)p]c -; "۬[EkiRF8%i;'!Nejb'_wE8Ps"KcͮSL3Yp˞ c}pǣ 5>?$T|CPmF۷co_+_~_/?/ן㏿~;/~Ζ|^g,δnuF%l`.c?Lyu8A:毥Ns$wʄg:NI%|ۻI Oc#z '0gy1Wjuâ;oc@'ɗw5IQC%iՒnKg6:AF=jNǪq.nnS`#+8vFҭJ#`V١?QlL粥MLV}$7+A_t*#&Ǵ|j0\}Y\(9[<6?RcK\1Փcښ 4hq=![w=t|$S bc%s;P{cô8vA E[»ch]عjzۿ"\Yu{ʺ8Ԗ Vj>YZ:(\Q?g-{Pfi, 얼uNjoc: -Mζs&vW1퟉0ݽ5S :1v]v lǴ7) -nbsQXw51WEFψQ/eTڸԘ6b~seg:$ y.¨/ i+ ?pLmX!OͧaTMxH DW!y؆Ǟ!z{pqT -*a]声8|f;"ݸGh:chѮPs2#,ZۛB볒D ; -z+:4@>N ZԵZ}ZA^Mr^1uP0 ]s -_kc@ -b\׆By AlDw\/>COpdTɉ lUWk&o 4 1)f\[7IqŒv|ͅaPIFZ3xoR;S -<#۸{.nK]K_;zD]%<,0#Jv5[hpB9H؁T&nԹGل -~yڋ|BL4K_hX.Iʝ@Hte|M)V=^z{ẅ́~ڜ gX7scd>x&3[;%?*nwm&2,MߔW>+FFy!A1܋?EKKuBQ_8V|yr]uQhJ67~^ZDzTm[^XǠS䖫S Vyy/?|_>/Qμ?f>?][ROFo6U_RX] eaWi$3IPC{ -bS Xp(%'9|>-Y4(R\EEߺBڜfhVȻ#NDںhU-"{(5(4tMҏ \=1͵ 9DesjI8doǬEQd܎vtK&> wg ]vtM֬hyUbV %t`d Qݝa]U==ֻxWU,JDBRgEL5&! -o  Z8b\$r y^GiLbc=x26Qxtt R#} a#I+țZXvҭ{x -T6Sn<4 &Ia5"餒V2,tkAo㮑wX-ŝZUk,C^AD)]_/u%oeYV)0kV 2 t@FkD` Dxu/ЙWݹ6ϲ"| EA11v%[f{T` sվ?K^dKͿ$)4A]*`A ֡$lv0ݶhIx@Pk6ۈkPf_)N&LA.&}>A f-1!rZň -Y:LZ炖_mu7݀ahsi ֈab  >1R ˰]RikYg.r\$?l}?kaW=4*Bp+}0˟?$T7mv]]HɱZt/i1Gہ32}{C);(]KgjH<(5JMɮ@TK$%ln5@7ـBq4芆QOk"EԋĩBSJrG8HsVXP%2G4uFUgJUL. -*Q-uC3=YC,zMNlYqEWiߔB%tJ\sT0 -{[t~{=T2p1>qoz&7YW*`(:?P{RL@qJCWlLA: 9?K#o6&.lh-UF,w8%Z2B1hLДzG/jә{IKE[+IBYjV5hv`;y:УF[=FUDz :Թ G^h9vEj6'RX=}vxssY߾]i{|+Qt"'Q6]4pklUgx,`Nɓ uspܹ(弊a1x0(Pz^_6vHکL]҄t 2 O1VugW*B `@c줴eKfkL(hr\snBEWǪ/(aMoC'*6[afF M "d/QģXc;Wڴ: %c'$Vs~DHmU ԩ!V>Xc#s -TzYAkrEB۳%|`8Fte҉BKUhAEEaP"_+x5==EHz51K YvM4(mӕy5qf@v`ez@I{y=SyZ9ĞaaKkSGo -XzT);uJfoGa^Ӑ:߯de5=ȺԳw%qQ -pٚ޿(꒨|Nl-iXW/m}TVOln i]ʤIPzs~t$uĊ\A5\o&"9rĞIy)Vj-ʊ5q}ωݸԎbd?NR|ѳj"U19F&h"0y32I -=;'ٍ{X+UR?>E!w0} ~׹dZ,m\jW9d^j1t =l#y5]_[ JyOJ̦ :=b=tmq]oF&/ɛ{ytC`tMJyVaС\hQv,灕C3M:=z?Y}J__ -II~J_*@[0Y 0;uhG朻!I7JgՀݬiCۓܻ)]oܫ njk߃;ܤ^ޒ7b7A6hÁrqvv.9ۤuoʝw; -:,|J_eLd\usnNo^>|ty+zcfyrB==:\Ã3^)[!t0D -"|prtqٓӏu8>?c].?Uouwd]û^X?X^_^>>nN~9\s^e Gѽo焤 GXzOa˓hy}1T}t@֯c 5p -CGX^$}0J%\&dJYnEa".^UUY)]qxyLUc{I+霊7jc_ŏRhh\| 2nJmqXEN`=635]{ڙ8'52*g4,2QxEHCvI8X+L51j%96XTq\ GB4#2TWj8z=;y FUAؑ2 7 -ZŦSjc( -endstream endobj 33 0 obj <>stream -HdWKd wD!//$~Diַw^@eUwϪ1H$z?ǁe?g{I׳YZVKCGC -`7t_N4Uޠksn@F& 6 uVR6S|Z4 CqMRhvo7^.9|k&mL@A-l0X@D& fj=b36 0`ʁ/[%@%Y8#%PRXuh#N`taJFg#=4 D,IFӠQ'FQO\xIȁ{REq< {M 6& 64qڠ( }@6C@鰴 6nC 3fڵ[ % >,#~d|$lۢE,6}oӷ"=񊢆% )W{!^6xe/Q v\]+k¹'1*b $bb%X-M^l!mbT -&SwB`@ -8nd0W(֫aHO0]D&VGR]%&=G']T~8N(vc\H(&] VGk0j*o&T WINcU`hO>bD!?'Smb {v(8zt JW۾՛RMx9(a}NhEgL( ~|>ڢ+mdëWC]%[j#ԲYDׁ&] :;H: ޶įfNvsySqT1⯿]u= όҞ -x=a6`&mɢĽ.cQyXqA -.`)Vc8ב`<Þ9y&e69B;eEyJA N]68QעA!Z$&(\\@7af<;Ux<' Y)aMe&h" 0}Q0l,Zǘzt.VdwYwh;"' vn޸rw?O.ϢWN_Pm_:2_Jg ^v~!" -PF ],Zq1.V;N2 -$3j)W MIpIt˳gNc!m4m3~tEگ 0MEhsG|IZHL@=h"8䧓KW>Zs:zfz~WHμZC선l)Р-(( Ƃڤ.7#_z5ۡ\t L*ΈmE7$!K#4ԩڲ^$gj`'t.Zab}j;8O!PX5s ՟7`^1^*=,ꪐ AHN]6,>l|txi,R!۾mhn1&7]7={Ornٽh]lj, 7P % p+H>U- -Gv]  AOğw˯#%-,Xs -% R jΫkϮpʂgȁxuэ(9͇2!&ڙ}cXTO/b;# 'oHزoǤ&N(^GKp_T-iV+1oql;w&1l74!R=.?ѤΨG=(v4\len+| `r^d%Ņ/{# id"(^FM7,%.|y=(ޔxtL3s.wn>TP%g1oiIxUn*K]>&BETBJ9Ml}-ѐL0YolITfbxMDkn\ 8cRrIC$\  -BnX8 ďb5\@$~ICzǀ x[͓ǠdДWk}FL5HJ˃q"3GJb.Hh2~ Qމ% (.)JjیK{sz,}vhmGnhJ!%NJqޣ}RB3pANrhfr0arBLΌ;Y}[Oǎ1&A,RtI_Druպx8qL#R 2uIl `+ Mٓ#:Xoi-xyW&CQ Wb0 2]1vI$7MGeJD~k@KsҋvA3rHfCVV$`yirO,)fV$ts=;&{ -"\q`DwwXG#IW8-I^DgLϙ8u:I+x}=՝6%Ȑ.oTgHLK<"*ƅ&sR;|R:;K YdW$FLJ$}4vR( Ѻ)xiXqaI=$ZQ򢐐4jcJI( &=.&OtW#x@'KXaO2y$HGk7Lf;ո,Z<ŷdov Ʈ11@KP*E -Z )KN@45FaH Х") ]s8ja*s8.edB(6tUb8L8ZIY'S%R T&h[Z lyҤˉ 5Bf z. (D: -*C5b]xrE<>U%-P\a%z#qh^lY0&O*J TIJX̣g /zʒ2@I݅LuQ+(g bV8'# tUK)SUN訷 +WEZeR`f DDjo$Yb£7YjhMJxIrSj9 a38yc2YOb01N'dtZQU3}(ȁ&=: < -T'(t%jR~$M\+Y%Ox$DXmB9HʰhMAlχoG-~w|L>&X^]vۗ7Wr؞,_y_dsDr(@7P&*1Njyڭ?Wr޾a>ii/.6_C.~'>ٞ65>bb^ab1?w/w|iߚ{[޹jxl]Y}f~[^]K'C.^*+Nn?`;׷\K?[n~wR?+nW -{{c#?rwv %w=^Fs_q~͜3_:l]xZ;?|?Do>V~` 41$V藡 ~<}7+kp+VSӧI&4WB6C"B(nlm ^ o~nv뢨g+ߊ~ȼGbZ{GR"-_;Za, fG$ű#?`66$@KHqԩTDV  nG?uƮ~D@ET燷[Ne^rЈ4@'=Yza.JUCD{NTqS&]ЫIz($**Ǥ :AxoD>!{ G78p]AT( –LqDwNeEV&z)#}7FE7`zbOS:7J*M (xc-8Ea  < -A.TML$ȸtf d qvG-)߾ksL\*?"b%-G]9$IaUaaO`Q-ɤruNH^̴#I\1ڮONmty^t1"e9 }8L"/`j/vLN\8jn$PBVl;ƻ788YՐ;-(%2 Ia.o&"~~_|˦AW)w}]k ٿ쿃0Yp`/y)"^ ixsGڒyu#l+ -7dc=|FXHJ p4|Asg=˟; cbv¹Wc~"27xd^mꤩ^z@a7"Ծh9IBefBZjrVo+[§CMmxyN~~\m4vVqt,G׺]8kZ/bW> 6بۃܴކRߐRJIPsVڐ @w`luCz1jx}Ц%!6LC&Ck/0"UksJ\?JKfvC<R ۤ&` oIr{_|BNq3j0qG/ax#icg䍂,Zbv`zb[-*W|#,G#[T m"Uwe -l1"`!jHHN9aX9k!uD78Ja;o+7K1ө*|Gsy7dL˧ٲ^IQFHW=rZzmEa&f 7=|FfzÙwϒi*}^Zs/|1Hg;ܫZm? g#M`P;ӨW6 }ql~"~#V&϶v}]3BC8!TQX1yP-$GhAʨi% -5#9ûos5 -;ѹ.}sNF?)- SΕ\&=0j'Ŋz`PrFV 4&4(.axIyzh<27F1:JU}B/SFݽ W/qgD8D7V =DAq8 @Xa~ۛJ͇qGg-VJP'TDu m^=e+B -\?% }툅|o9],Ybmd|.^b}"^'7Y?N@  9%X,g&ŏ;enaSet<tSpCl 'aZ߻cw+5U;#zBv?n@SUr) /of ;BOww0;Ծe1$ {;}ߎFETzfxjK'^N>hA7pԽv2|di[ -\NZ; =Da8OPfnx ~~AO}[O~ݕB;\,l0#_s+c&dD޾2`'kB(c!S Ff΁bğFcv}}CDGZiswxàPeeas$!%Å!J`_}d47c3,=)Ul vp12vXnS rY "8@Qʨ|x4< +We_DRćh7ʖ$iUڪ["Izf]Ur CHКdL3E0@w)T .52 ^C}8X\"]TVML'@CBQ) -IrsX/?sSvǘA4;za !zFϙ?N/52 <5bLSn" |{y;efc~"ġLB=L5 uCɔ¾.* :*n kwղ9y0D}.i!CeϏ !]-A2ttlr)aٴd9C:²6h8E2A{_̴vg Ї6wE2;+;,XXbgm|V4{:"?8lsTĺ+!VC,)B 0gҘ4+J@G ˤYD^'wV ui%ᐩߚ]]ILVi+yHtӖ{?}˥;1xŻ^ʢ 1A k txߩ -퉞<1n8YiUd9/h)hh1~wrΫG| vеq00.[G$}J9-8<6wuèHLv&zt4P&44!2Џ]ۏJz\P Y!w0[\J!N9ia~5'c :ѯIX\I"xlTs_ZVrZU)XRo%᠃;b`G|0ؚ5Lq|3T*pR 5怦ZFUW6+]\=hHBj]θpM!M̀C*kW8;.j,dAT&UUc-1bvq2t*UEKGtC5Ql -Rؤ8j l! Hc-'Vw>=OQD`1A -i4xzNn2UHVEX㑩ҥ*)P- -#U k*o5HM dS  dt$zx4p -KƞޏXzoSsd ٵgqUt}#A[uX<;i}DH܃zs)+r凰2Xf6dQ[JT|{f77ֻ̈X\̰*62U(.MTdnk2_q -P_ 3ԲdE"Dc!P/S2&El!sD@&:1)QzC23' ;ŖH_J6~ʗ#X'R?,9 -Bʥn|,T'Tu*4h[o'.*~-L -}xX"SJZO:OdPPa`[K&JB .?~p uȬ4nUs1ث1w. H1 oֻ |2KMEMqn|!pɤ/UL5O-3&KIGo3.]iUhv&9 !iBת7*vv\#Z#x-|!RmЭn'L./\SMkL']2ԭ9葛hzm'[\v'ƛrHVߐQ%BV/q}Nڞlg$je>ׯI1+WW"fABJt-O -_4KtvPS,Cgr$@64T_gD=rWbSH~u>]/犃0lB_&-! fjJ f55^ -6Bگ}1@ϤġknxLĦB2: TZ<5~"pC\ƶn~&[}U+j #N7 j)fvɼ}%5V%>X%.S! O(4WTp4+1jN:]u6d[y7i"DR^Ĉ۵qQIβNX0vI5Ӵ9֬,ӻUس=eFK~7~rI>{`Zy7E0:\ߟcEsn2bmMcwh)tnEa87M`ʗ 7#Z &igZ-TdFٔlvI[qhdDjhjaM q_][*[[̊ JP6 -&U`n{P\k/AcVJΆGdͣ_`4rK*9{̔P"V+'ĀܭyʷPA=jYށEYWlngX:*L)nmqԥܱs`ϔ;H*3TV 'l{c$lr)hYjO9": "[i><_F!#Q6i*8ILU(#ԝY@j5Ţm |&ៅZkP`@A. b)^~\t0JnnqcwTpdf=LFvMedR=MGظgw% -MnRCnz*e!~S*T1_ *MyV T[ ^Lw Ng$?is :Mm4BƒCG p >I?لE/[mtMxqbKN`eK*H|wu.ugX2ZϸClLzz4 ҙSIxV[g.ޡ~/*l .UB:%G/|47ֶ5Bg".w t닜In5 hٺp d*)~,.>7,)Sg/!\Y# RS v:hw!BI̳z5$ȷFN\ViNA|'%/daO:,dMƀ*T>i-2mh -:(7X]_JJn k CE{̃E=ÇaK6>;d&[&J󌎴5n.cg(%?b%@F5 TW..'T*s4:9ͽq/ZJ^k`!-ޜ[ 53 1R]Eڌ򕍴a*[j6 'g@=U/-j2{WD89М?;3m`\^9eـzWvQ,3;*\X`DwT 0gb9Fw}CТ^s)m - - r-n=$@Gh[| k L Y@ w/]#k?D@*\i{|e:#{nCHM| ݮ8XJ'~Hp5[ &m_ oi0[ /p=E 5ùEHx<>·SGyP1)4`0B”+ZlX\!{ - [NyLYvAAAP!鴼(ޘnIq>stream -HYJO. - * -Ks1} -V?<1wteV>ofeKO$;+W&i9! o3Bx pI^u2GrP+9ɯm6ob 041"zc]Q\D?IC^>F :W?4:^Y2ƾ -8SU>܉Nn"B…{z !xhrtɺpU%z'+f'Pu7.\Nym̥Q67tc.(c"Ϊ,)moԥaFG^np1aȓHr!"5FɊIuܞh=ێ[|ޤV(A;T I>AovQFXTH| & }|5!|v*iGE!Y?M2(>\Tcp09qܬi;]68>=3]-5xÕՐ"C_b3FgQH6wU%H=&M]Q@.QыHPUqJ;_Ŋb9vӤb2qs1BVQ6r6Y Nȥx\هw|}۞(1wpq:E~/!xE,{WV C?)5 G:'wnO}#|#Yştߕ^tZ J57d>z@6TlTctb jW?ݠ*sU+1J$ :ɭ'q}x$z}b3 IH@0,>cɨ[W l1c,2ylȼ%.U1?0wO#J4ougeȾV.0 3{nj ^=ǎ8YJXB&i{ -B}>Ѓrjc-.ó(|ջE ~_sVKd5Fsk,F#Z LrF37o4Hcn* MP>(ޒl B0&ʸo猼\,[狜'TIOX{Z-"m&I2A*M:GX6/zڦj6"D}:}Hx04IZKmܴᬩy]+SO7F$US7WUyqTT~fQ%t76_Db.~ Ga$RIx[QP!$bԀ_Idr 9(_Yst,EG5 y/ -Gi3lm ݲ7^m$Q$? -;tp+i)[Ʀ4xD1/E-/T`L_3sJ2V!]q}Q~,d17 sHGڃ -YZ^[n%b}5<ֵXpm| ,,'C%XR/% U#[Һe9oNt EpKy. ̈́n'z*mθ(T(yO*nt%un6['5L]gԕ:'wԲ_[#'Ly'?jS;P!MnT(7`E^.>"mR}B|t MFEEP}K 6Zjɡ#-uc6h%@%ϿVVxTqQ~+L글+̦F|8[ݨhU:j!s$wj-4jcHrj7X҆ [p&!X @A*P9wKjd̛7{qeSC 'mߙٕBz|@0e ">㘨_!'#7&PQG`:*рEwz.cOJ_`1QҔpڞCԸyϳ5"i'ty6P -W5B -@cIV gv{P7dRoq#Rx5Oz5pT -QSNRp:BcżPr3IFJҎ~_*9b)Cѯp#@&yVƠmt}HZα/"SI={N_r>-e3UAY%M^~1Osr1fx8I [”$@Z|KbG1UX -pUK'I(C=߭rIlWBmL>|Vֺ1C{~wT;i>Nj AX !g4tKbc |Nf=#`*41 -TRa=/hBj,fn O6g Yp `'op7GGDGl`uuv*Xn@0`-. @.1av,T]bIjبPh 󯤮byQWävϠN$,2GF-[̣jB/thɵ>+$=XdIJ :VWDJsP4Q E`34#HCT5F<0Q}z]0f~eW/duRJ`ADIó?^OI/Y`!uX,Z,d=odXc94=rJ4Lpf}N3[ 3}=hWjyڞ#d==+nZ.iP/Y9H,fsԼ:$A;$G9Y쏪"H5a4]ɷD#܊m'`a[#p2}<p޹7%CZ#j(h8}V%~OUJT|h z\mDoS%JIf)-yKْr-Į05459VE"Rk[ImLArU51 ݭY5=Mw|{z^wL*&݅F`{xK~[E4[0X5i%-ISL>rK8׺OTE+h/.$BӺLN7=_?=ŰwCNYVo#$0t{d|s谣{/OIOg(xt0 Uugy'8 nkU<:3Itl&Be lwyH]Dwq!M͝I4b4 -Chb}-wTF1iTR6}OJmwsTA߼4Y _80B4,H_}v&t?`ae+O|_<WGE!'_^uN3]nlfW{b0 zkMɕJ8's4y#AIb:>?,`'n>:lWCqYe/7vyPHٕB[qS.=)WU Wu%+76RȭX&(]mx%"el)M4D`lxywi-ht6QҬ2ʂ御oM4;cHg"ʽi B+iD@ ؀N50xܦc1 :GjOJ͵lrЫ!%, Sr2DwAS9M@k ݿ;/9>ciX6 +ϐVYAwc7 BckʩcU55:f3P{\=L,?fTih4^ټev7vmnov*my{$$.U6n.A;pn6 8ٍZif7K@g7h`7Sh1(Aj#J.Ͷwntkeq [yc%#6&A[Itٌ L SKКq ЕvYVCf䳫{CgZ`y;0]ptgu$CH5e78\9#Uӆ#c)&w^K8 *|mC 4}5`ob G:5p0xc>G=Ǿ>mjv86Uc~cfOՙx -lPli59ڛd794yۚN&mt%)XitNHɱGջ,DwfE(9bLi0'=rJ)CoxRZy~ KɀDγ.损Znb{fDY*{Zct? -,"sY(>:Xoxk)qsR?}yx9z%tb\`l AAm\AAWFeoU%*K սnҫꥹx*JQI^ݟ$jwz ׬^֑5`7 -3h.O+Y- v2^=z_6PNƛ ԓrC:3RoX䣽T{Esx~.B+x= -oCaYNz/ِOQk.L}%jZ~-\9;pTÎ"^8%Z94Tk$ ٸ ==9?뭤׭.ji*]Xtk&_wuV.WFsKx{e"YΡ5p' -S @ip^[Β[۷-&?# f"2'|`jq6i011g3ML4glǶQ}Jܖ4Ļ vLk|D@FTwA_'*7}[C24pLT5 -cK i 'gbEў -71vLvlSj2έ9Xx+C>~2= ]9ɪ%?'BƊ~-Pn:99F7ZPHqy^pPjvJzs ~Seԓy=O**1+d~R]7v - 3U59a~2zzy ܸ}; &7xtLaɯ qégQoWJ/{md&/&Ɠi9L !aL}LaomgX3Jm+3:^ӇI`r0֖?{JWbaEc4[bʫFrR*16Z{@0Y5Qv6t#g3W_ICOÎ{:@^^6|!]+g̑Q6Ij#`tu8ghds{#=~˗Gqnld Ps>0#:c -.ZQa)Qh}@E(3iPšwv::| )  E$ -/Cp-%1R:Eh|L"!K iR ZR$vҼOږb#w_7RMְ3 '7ZؔiJ>X -g5wT[XD̖FŁp~{Gq9,\Ir -$ꇵ!.B+H(Ip\} IF1"<4fe7M(0}ɶFoh. }w<2}᱋U5Wţ˞ -I\ېYCv#mPhS-Z6kvdA#qM hHx&W3D; >CX0JKzEGd-*T!j/IoLka.{^-)6UgHh>hGu)8RHJ„e_O:, -c_Ir? -.[]xnZ -t-n&,%lN1e|{ ՖAc-- ,ufSK(tczϽ :J)\e T}w+`LXn&9[a\>NӄK"H~݌H=͂{D?e!Tc"v~Abi,Hr:TzyrfW'_z&]ӠCQbJnvOo^BG| -x> ׌v}'ѿ" 8&\W^ ސ9y5q/ -\ -fBZ-|}Žt\2/HR2+ͲS Tk'iF)O֟_&B'wg1u`$>ŰTssZD %hrA6ʅf>Y lz֡F(-Rq(2yTDc6&l.~2»s֒ (P%"hUbf­dUMhS!M"rtyg.0m}sp!tulej˫q}eKv.tqb?Uږ8D [¾:#8 AGDxԙ+lݝ|<j9UuXǩxݻS9+RZtM %D#A^n}߀f7. x 'Qkyv~AkW͎ Ǎ"~Xd n GQX yP2zxߦ~5N RC4X {9pIEZ)ڭyc|Tp' -A&HK -^ hO2&FrP!,* Kye1ݔFd'y2]l蓩ZhZINBclh".FqbTuEُJTv1z޹QMehQAr09O4K"fٝʋ.Zd4(FVEqYnV6픸 jxNb]^q"oZ@A?ʡ*gѲD!*+XyϖχC#1n!})Cȋ7]^MZךZ\Kb.ߞo)":+L0t#fck"Q oIa ST'|'yAakoHkm#덊'}9JBoNiud|ߥ.`(!h%%NECÁ-@(v41x1eըg&]{kVwSڝ8oKYS,`lJĆ/3M6QNرI( RV`q'埄uy!məZ >\5-Pq$89?ڛa.RҰ~ AYdQ ӂp =r ]6KRvE;Mݣ ԅwJƨDiK}8%50㻾Gjӱ4AyQd>xg +O\d7l#S#CyTIb Z)f_{է jŔhd643i)97<#?Zs5eihf*Vx>X: C!qV -GSl̲ mh[{]`ҵFت)E˿׳O8N ~! ܠj8Y|ǘGj!#! F,sj|IHFZd #SZomujZiVwndQ@>Ns{j>fܞ07°B3J? J4ŻEE1ZgN -1lZJǓ`οP)6cm/䘵Q`e -m[>hVlR&/[)(n뢞ͱNbf 6А]rh/i[a^NB(}J?>?ғ_'S7U=L.s4:k*- >}Ux1O"o*&w!<-N}btZXqZlW)NWuk{h|Iȋ9K-/8AL+/&:Zl8)=$9%Fն:nU>[ JBu .z1 ]ugh_kt `  <;rmwMiāꮏAo _Hj˽/ h|vߺY"soڂ^-q4:>K . 3aV: xH/ey To"7V2T1JuvJ%:ܫ =6jȔ+nYw,LF&M_Aflc`Ox_+FUjё>>L -܀MQ! IipsEiAhv/ۅL!1# J*Ntvjrw,XEL -H^ fVl Β}5r0TeSm󶜊T'"Кnjx -(_ken;v i0L=.܆>5r huH&냁fہ@{%BWʊGzZsST2@502һ;(\k VQ!61ѳ]ǬAPTйQENrrqM N p`t,$kM6= ߖɁZ2#(nȫ5%H}RJ/":(?/RTeں=}UH` `CQ]]g`lª$FrDڊ`aV;"yxƅjwՆ_wGcP1ᷧJX(BclbB0RfILhQ L!C%V8>Pu{^͋ѳyzцCT3qVpmhLdLfMS?USCH[FzA)zpѡCA~}_Trԉ9s*yuL}R:!zrxIh8jo Өֆͱ6N1m4J a+RW ~,D>$kq<КamȦ s*k!0p3c! SPp_>JIQQ\K)Q{TҨ./R)dR.M/;Bx -a6|l8W{_L,RJ5Q3oy|+-5.H8;3gLYB[1yc}ߵq:͢t$F3Gz +ĶB,(叡TR>eiN%-FKo4XhLB;uțˎ(4n;TT=Y(UG65o_6hs=}ҚCOhLd9'C4GJBM7+{o!( 'f`텞 -;V4# 1CQ/J:q?x`F^(P7>Du|A +W4ٛ9(.Dp$ /4o1lجOM#r#fZC뷬 -GX壉`ƚ $9uۄ -=cGG5͒m|:k"lUA[; t&} -Z)qof E:O43㟁AӋ }PΞXĒMb=AQuڱ0Ɠq&W)FlhTleg\CQ`,t.l E.Gii3P@9 - @ wٲӴ][Eܩrž;U6`A5e=i,;fۅEEuvʳW ׭e.+ VPҹ1FdGdDѽ<ǢJ- ft . TCq {> -rبeZu_w2׺WMk/qNCZs~2$)fZÞbQN" - *lVG2d-6*H 1!"q8,{@X*gT ALї9ɢb +/ܻQfGڲ`K -M4$hI^I[vm_vm#I?\Qu=sESVj?=f+=i&(Lz~Gx3tq>XŻ0|!8Jz{ G -*& 6`c ,IQ撸ً&U_L9 - ;"*uIw^dڽ >EL:x3O$ ^^sԼFƣj W1gb]mv^@-%Eqw:\V<xwz* 2g}+]1yֳ}R ρ8%!n\lXH|Vˁy._iG̈;0#/x.wq?c.ďAO" rdsS>L' -*qٯvzP~Vgg|1͵!t׻ZHw"</r( 9bΥ/JA7 /?^H -=̗^"'cXσc_ d՜03ۄY: [JGz w]UJ~FLͦK`^>HH4x>±QFz@U2ꎖ/Ë+DT>stream -HWv}ڄ@ 6`Ƅ!Cy~K Nw9$vvǣn^;\b)v{"L -$E7.~HXcoUG ɡ1^Sj$b]̨ :CP(dt%Gla"Tr7Fȍj:rJaZkysORD9[`Z4H}>k.rCi5_?pw\sq8x>%W#O bӪyǂ FMA|c&-C ՝`4h] /6n8|qREEHS4;GELVAvG3 Я9*E^47A|}џa,F;ƞ]-, -"t3FdA#){(jGn>@hm!8Y[ah5YٙHpsyp*D7U۝%lzTW Sߣ /[iI>EJ+hxNnA@quKԖJX ITđ< 76F ş\b%L=~qG2%9롌Æ“t c#;0L'+*iشr%5?Wi6Hn*=ځ{5--ޅ}k -Ȫ!!&:hƜ, }8o2(& -ꏷϐt&D'(R{\^i(`u;N"MdR- |pL]H> 4}TaxQf8GnɃ4y9yp2RMB\v#$sD Re‹MLŨu,!vSD\H ãS1Zvgr}ִyO e/Dw -?* c.8Dd>@g cI-Tl^aki9#i?&"Kt#C~\!'Vș$֝1%m^9e;Zy/Hv87֪!"{iDPAjv=[q%%/K:.ƹQ[9}HL6ieA6L!11PCI$T֬jV7\PqK\SFB` ޔa@CQ֕ԟ?BbPqZ~>K#ȋbs=d`sLm&> -z> -WiW> !!BDva`(yrΦ゗pSIUSK?5[++zqUC\+Dq^r5RܽUM¥[Z3fN0hgxd-ۮ$bvGLS盨\J'/Ҹ''`cgP)E_͓g*ѷ&n +n@PWlrE*@q#CVK8d^aɹ&|DgZ%/vrZHthxګ,s38/E ׯ_1X]i;:23)*諤D 8"L H,@y-H O}HV0,fKFfvAb7&ie.p-Q/6>@m}~AA&Š4xK-?U~*0H,aÈ110bq .p UX ndJNairy~4ٜ|'ޥά:9. '&:Aƶz Bݘ*kKt3v/yI&+xfkEh&@': ˆmlrĿ֣#XAgho Kb }뤯;aP0=x'PV|r )nSaA!wb f( /ffsdIvV8%sLҜE{X $էbE"ZǕwhefL -} =A\`qUBnhT4qVJ|"|\mպ=ܵ4,r#f -gM/FB[c(`eϠp*=%$ޞrY$E'kkK4cJG[B,>H+bB+|& 6|st/i_轙 $s o)"kJ \, foB}.ydPF -N[W6lAca$7g -`UL/; mOb90єѪSzWJbm4ܱW @oB‰LS@ǁ&C! -aH 'Ay6AAw,ap6*A |`UAYy& = &O^@@ڳ*03/՛fFV4`s?>1z+6ouh#2GVOm0J8t hpteW:NΡQiX:R{giܵE_*ܔT9>Ӛ3m<3gZ{q|u4bPmSyFﳃW4m6%'ȵ֝ ޭ}+}AGww'uy@.{qN\.ie;t0iסi_ %m -/M< m=ujݗQ^o+}Yzj{ƾs9_m.:HMcf!0Ï&_* _(;؉îO$nXP?K‰i-{}2> e!Tj``}Pa^KӺP֗͝N)"JȯHK,}kby©_Jo#F.ee{^F*w U5Zg d: 0l[[ScRv~>y*gl\̬ 5\R @xCO΋d^Og uؕlAo oC?`/dy_L~Mw5eto,ᐕCD`OvȬCrAHO^76}|77rWo1Ȱ -endstream endobj 36 0 obj <>stream -HOo]I#)A] 8H(  -Ğ!"1λϓ0HnuSEm9[jmIB݆b+K=Dwu+|dwOb?lg1aq2Zʹվ`=^PqehY*uy5 -ﭶ4lo]="1RZZ08rRfEmJM,GsQ{&JD"ZqT{TR(БzM,mJfuZ56YysIɹ1Q2jAqE{R23QNShY9* g)zj7YQ(R1r5f{`fXP^@ Q+<,>1J6^a\Q#e}d)Qs=lB'# ",>3K0E8bVgA2_Nc ^a1{^}e*Q}S9!(1oLO0d N_r)7Sv"SYKYۇ}rh=ZC}?*#B فt0s<ӧˏWW?-ݞo21>K쑪wJ @SY(ũo}oou<nF`66g$ -ZzShr!wDXĀRݣLeDHDi9"_I#:,2ESKeB6~ 9gI=+$ @4HRE|aI  m~(D,MI v EQir=-6I_jR:~ٿr-n R[tʨڄFQ3h"(Q؅Df>iMb -;#]XF @uN:Dl;H΋.*=.,xX8"b=GֻF([\Z; Ej+v򺀁H {N5S[LjtZDVC+{…!,1ӑ\lyr*&hOd -mKӊ,VB:U?^hxJR@7h&f\ խ.4q٭:WR~ -6*{OULY-s6ltCHI' \@>ż?#OrN"+GAt2DauX3V=E؉W|E*GmYwr*<Á$b?1!b%0;GmxZOQ>=1nw߁ivh|~'(W+1-/on>'_^vk+akb=rE:n͵&k濪O*ZH SVB ASL -g)K{SUߤ ݪ]\3;Yڡ6}>|}oOj64FxǭgHԚfёE״N) -EE*q(P/1(Se6?B ݂gM_J)c(` -v9)"ZCFM6}(GyieoEU]bH9\4QE E 28bYJGW!J5.t*)Îj+WOn(Cs=2\3U1SUP%Xn- \V*ZO"tm(IVm5 Ԫ+ ciDdR:>žTP P}8JQL최hLP(Y1iKY$# SDBcrWu020#Fpңҕ׫f`%mƄ>#*{6R Y; -7׃hs(w]t)"kPlGƟ -+<,mE2˕9Vmɜ  ^ω$Yj ja bDF^opfԺ8[7^MEgKNw_2԰K9F.a .6-fSE ldNI@ZB =j|u%XkgqL@gi ܺ' HcK3C*H_*b)'=qtlquBoluo{!,V V" -ȕ_:U+CVxLo`70 5eUV;$~BClV;~-q7GJpvQ)P)X:RpD\0op݁aJâRs#\atʝDxk.D10U Y~8(ހ/#9h<-Mհ|1/OZD `f K媑=-^x\C=X,\A`Һo#N"C吷Tehl:ob_2GxP(B `GW 4ఞMB* p=`B0dv2XiEН :7jђUaޒ].8`VNxlqQ4Dx'mRFjt* 9|Fugj0قk@2[VAcFE'4Mr*-{i`Evx># 9B ->׽х|9^+6Φ՟@g[ P@* -PHў>2V\*҃1.#/r' &E9Gv@ACpU}&07&k&$|UTAwVaxD[@k{鴘tLTFs[eRjq9=.+ڞn 7#^?)q?ݿ}4.Md! C}P0mgp!4M/I@`( (Q} ye ÿ!ւRe#LeCY [`=uŷ -ܒ G_`\`d\LEpRnϬld\}j^B$q9@qu_st1b)_JvN0'ZL%+},E"ࢦEL}_tb(Mx39`SeY{x:#FF+DZD0NE@4+4}4|GdŀED-0[pWg7^HqW -Tg]s,#)j=T_қu3IY#E yQlq/b(~^S#R .kPU^&=+k<ֵ=5s( נ~,]o\<XV@7!=Š $/}f -E1%܁X*X[| -G EbGDIg*擧POsNYY4bn1yEc4қ`;fE!Շ%-h\bϑ殧GMUSJ;uKp֯1B` - f2ߑZ`ڜBMEbψV(ƃ$znNBUt.`o>>Vzq^}J(Jg̮%WmlfIQH+9c j64^ HS9_졯A{ 8Z{ |t/m -b;F\TW̖BG\!464P pN> (HC -&"F*!FgL49-~S) B(t*SnߠL1Bjќ&8LjYB3]S-~eœ11v$lyIf–?fV}&#s+qf)bMH.,I><]=#`H^gxጺL'P3ٲ#/rKrǡV"K=rNZʈ.[A>o*2LW068 &&CxN?,'TV<=VP sV֚m!R\9 ( Uڂp ΁ڋBF{E I2é [gš1;Qÿ#wjf?CP$:d )u }Ǐ&"1W5B4YpY,}MJoӖ`{Besbxཤk'ͅ1RqZa* YF nv 'Q`,vnTuG\gi[mVDc >aT ri7SluFae(L%CC R;0YX<.~cgm rY4FMSG55!|:5H\ld!x]ԓ41j^=8 1}~X -j0),Udrn=00 2,SZ qP,jH'&FOrCf+̢{!j, yj@t'{2@>$.w-RDRaS>u:m:Y]ErU[Cw :/hG VRJ]fZo8"!]bΓIT:Fٵԍ|$Z9.\ *Y`,Rt"O=塥X\LګD&q>t&ÐXBA>htQ4.ajp{:a\kUqX-ŕJ jƎ6.LM=}gƙ@w=vkƕ#V][kNʍ?/& 1\l`FY_`)`g>o??O9Іd|Hִ1YB?b:u tKS,s(HRa3}Ri 걅M"Z3+/hwwVm3r |Sm>Gqq.AlyI%!i%eYJE>eER -SqiD;o-4&oiƂݺ 9+!9<bnSh!{":p@/ 4njx/r{F3jUʥ6q ,vVC2I48b*[I| .][m 7~Havnc@Q6wUVi>؂C:nݏ[⦰J%@A'/}@* aF)&%[Վ1MXTC&c/swyg[ܒ\9sxW_y8?dC 9c>?ta.)Qw8X=TL'ߟs{ -b"=ehđAjʣЎ_~cNcs_åԵwХn 5P-r?^uR`aN`A+ ]Po}A JV`bB8 gɈvbBfZL"ѧ50pe뽬UݱPr]f9. /({PJY=n.s6wڀ]xY -xT䆺I GDXXZ9ƶ-zlm#^w$_Ё<[J*a&]3T<@)cs@BaȵmQN+]9D8pk7B𐺂Df$-tk#Pʋ%~)SsH!isuy]ZAjԫr^"~8R~:_ T%NRN*v,t#Җ݊Ue##|PVtH5g@RDy㍤xTqJ>˄̓,1IݺLIU|!/ -"342oA,fMO`U{Wl9=KXf5vkdbRVېr!)FY76(غHYP9 `ZY\U.L@(Qһrwxُ -a HїK'fvYSmjcOH0Y,SyQ&"dcy:(bbWY91"QRn4HDŊ1XuoՁl*"F~6UMr/G. ѥz,ʹR^E:/>CrZEݚ- fp -n䐊܍7- a_l;}We,[0҄3^8 M6s#;$ު mWwd&&d?W!ۍJsV -5rtlhYH4g*8tmo !dubuޘ?/RԎ3~6ThSt6$  =%y)Eݳ괦IJ)>n0K:VqNeI" DQMWe -e|^hv>]L5'nIq^7 -9E:tMm,#"8(nipXRdgkJӂ$+(~7$:!7Nnٵ\P{Kּp؎Wtն#زkq&{;殮S uNtpVSM;ooOǗ!7b/ю.\E×0*8?ΣPĠml`Ͱ( Io(wJCoOw/oP?{p{ o_?G4%woa.?\+7O7enQ-sBpfН̪񻔘U)VJ ⋆aTӏ"B?&BmٓB1^vK #@{A4~ )1ʥV.RiJQɌ* Gl?[^EB&7` ,^".d@m*i>)$ԟyԄ]R:YC(շȮE:~gwOOw}y9ʚ0\$}t$yvtc*wKTDY-C2p.2"+$%Tֱ Oz;AQHxݤT~۬́XP28 @.ƀZ Xgx6Lpn\qi(s Dn$yu `5L/+J{3UD@ :bޙTpQ} ! -Y=(+6aeS:9,"$)d%ĕF(qt\R$ǹяz#8kvOqMEA+PIJ?)f*j,WRHLIbE!S%%]k[2F<2CIkP-fe-PuQ aoc-ϤI_E8H=V -*Lj -2 LLuFU_G)%]x3\B1M"iCAq[+&( >鏌N&̬9 RfT& -Aq7"%cQSC *><䏅p\!>ѤW߈ף'Kdbp[P=pr-^H"gPgԍt~Bbk_!M gn[;ɢg%1 XfG"n@u)#T?r ,tK#S,V{!*M?<q\+nm _G֓ő<9|uHZ܏?i}[!;d*)!n0h[o"S>c'd,zϟ$tLe#F7= -j8K?)VޱKnheB -K&>AnTBv49F8K 9WZ%f̺0v80Q)v? 1w„4״![[Y|'K\Alv˳OC ]#S3MVKg̿ϼ;dM{# =*_ia.rXQ;52><}Ȣ}Sj -q3 '5s5]69BK]xmI.9lqAg +[GH4ilv߯460WOx/-^!,\}B')}M_;tJTojdzg] c]Źa[<'}W7&7lYuxz_bݿ^tfU6 \5*lnl*Fs=a5=)^Ӂ ᐹI&@mgB<~[rQThl@?63cH"⩃= -vWyE\팼F>Yܒ\tCG {y|O}>j3I6xORR͢A'q0&'N7̗Z-T g>؇"ԺlL%/ -u'WLKO;"Q MtG.-}K89ɟzy3u刭Z˜bȘί^lCϚ?b@8AoMOG\N|22+膾tDcvwtUߣˢ蝜ٕC-Bwn^Uލ˝).?-b&t^sx)>nZ ?ti|A.gfw1,OP;MYodAO 능&lnN~wdڻ7;3>|ImmD|cdhC7vig -rnWD;R;Y c/3[7 -|uzB1'ynjV޽?UԓY솺#ᦣtFzo_^jRnzFi={wGz>V%=i7}ԛΟXbI:w/_n/|-3i -列#<Da9C2 GQo9s.ĕWve۰li.A /HBd nE+a)={;)1 Ekk˾Le4zg3CGNn- # rWhc ʲHe<58&/\s{67IWɇR9W*'<]e7a==s/(v$ -xU#M-8*x1t'M>;\ElD5muqAOS -δ(Me6 k?T$W:]ҤL`tm# -N2Ǒh#LnTnI7(͡msh=<}Pb@V:y@5:5wjP_e?H~ sXH9w")M 5S*.ϱԝ3O9 q -Hr/pϪ0 > {~կl>rV]|LsΧ^F,/:..w3ݒyjo #J9`< -1llzIw͒ݳg}]D=ڗxF!+gH:bxEL{);s1HP"S*?訑KS^ {A%\ ,yx5%Fבb(S%0%i $ ->Z4I5ŚEGMyV+ơ.N'ы2bE_mbV!T.Ѩ @LF#l*YؗHp@(<ѷto$:_/_NQ,jlUb:ڗ9pw@g -貅Y|i d gaCXeW@<E>U1lg-1˦C 21]/@ZੁyhȡK4@ ]a,E -'ĭ"PM[<5)&>T~qVx:{߹;w[z{ Xf*wqM̪Tee,K2&BJ05#B"V8L)ԑB ?Jӈ41bmbsEOIf]t2ٮnL"-CBGa8L la@a[tAeөRIr -őA9*.×|e9$Fm>0Q8kp]&,/J -N#Ơ&(xuBK%?Q_XeP u1Holfj9( J2̤2bl !ݨ:gd*>_aނ'XR šI#"& -9(P4**pĊ{H۹T a[t8[< Oiӑ@DW="/TK{[ -kV3 8Iq?w<-ruSeqtyy=_jUc@i귐 b@ʚEn}( Tێ+tI 7{<ćuxn~h3XV 2C(sUQc8Vs[ǰY%?^ H;4<7=^'EY_/u.;ÙiuHx-{~kut,hv/~s@]`0ޗLtNʴx8N6oBG( jAT$Fq3pzҲn3<tK~#lDe (bx&jtVRQu:ꎚ#P$S`SlJv:˞cgP ϩ VViL*9LMk>BWXBxڕLx.:ݙvQ57-بA[|NC|[AñS @/.J%u egF|+ȞK4 -aTibmyOmN $r[ǎՆp8;$<`(.:jO6aYd'(ӼD6zQh&1ʱ´88.@hE2/MCFB:q|x4 ܇~L&n^d^+hvG7TO>dP#Kqi2fb v-xZ`6t̚^$,z]kv-{K!OjƕSqe-%݈/=?/i,C*ލvrq{- LFG+r+_9z?`wK0hhF;q7X<=ߪ:שH$[]kGs:-h-`r#9|;,z`5i͜I/iSsj ! -W;O -`wEzNRNQx@ Ppj&օXG ݁M=.2 ֧A>OFQU֗votur@/;ɼᓣl% v"MpnHKP`0t0mÉfu?ZueQ$V dw2ܡN}ܓI{o@0v0SO4;WoT;܀e :>z@4&Ĝ ."F}@Nx*N:/!4ja[ JS]`:<,Y5I|j,Mϋ ޚ >[_h|$8¨)# , S`J^+Ee-G'^p 5wJ!gG#tԂnAid-zAAH0;fsܝ0(Nrׇ=S Xj6lTAV\#xN P/Q;Tr- >M"(kGSĽrۼ~T( -B`7؁;oZhQDOW~L}EPÚ@R,}. C QY9vVLje&M~y>[B ig[O_ GwneEtPS(RJ=1aqJ.YŦb9|ra^zpAU!S^28 aH KxiLRФFqn&8DbD\||16Wo e=6 'jڣot ل@Tt1Zvzb?%@Z'%t ݥmjQK`e zIOCcNLr&SdHpaaB!1 @2yG\!Wzxy/hZ.XYkgE=r -"ֱ-SzRSIAK'*SSS`OYcB3qm7 -J!cS"`q9\5O[)z iDQe䷒"fXqw2D+om89oBUwaAŧ8T "#ޭ9ZV4S#z@gaSP@b%}RD -J*a+qg?k ce/J}^bs -W8nƯ /a1-T|! FAY<ΐhBkiAV&0Fit3cVt@A#҇u[`$b5JT])* +=Qx9cE(.%Φ $K #:1tK)9_F֥_k8.3֢D|kOexeޒQٳjf4$EK8)#*%&=S8qN胺mt vĿOlUd_FDmu_qjQCc6C;l87}EU҈ړoCRFi֐T -B!R< ,WSAk2XT@K3OH]W<@V64p .Qn$'*ܗ`,9@[sѓÇ70Wa (R[qoFM1p-*2n1'\@p -1bŸɷXYz}4[I -V̱rIJEFƗ"gUQ Q.ux E= nTPQCCj "kiR/c :/RT*F4gdTَ-^k=ٱɋ0<zYŀjD]xSB[0)S5pzW2A%-'N/y a̠f/$ѷB!k0 q: +93k[*5x#NEQŴlS]1^D RY K޵ یm ѩ!t#侀R&VLmF{`țr/eW⊢C<$'^C3 K*@CU&K -ʂj/#h}LYNsF -fD╫D7 HOmW>Vs A?ʨvhoz#74u?]b?FN-.DɾE ˗b~oo|/?|[-~?Y_<9A>+M jhSHU; -MbZpg#L hJ 2'^N LV{| -Չ18ɸ ZsDG[kzzil|46P>6]Yٸ|rٴ2aڙ{1="trt6"ArFȸ:8+r$ꉳAksg]y9\i݄#yLH1029o=vc,~ PV;+)r zY"4CxťQD4!j8Tz 40OT{ )aĴi9 I'6낳5pA4v!hZu4 Wԝ3xh^dY;|rt["{\OzR[5-; 2`!(EU;]X .8!M|@LڪemcT+8.AH+trQ,iݔڎ4EFє*_ "eqޅr]X2lPH'/g֨SNWjn7C_zTVܑa;9-1)M1eM˂F!Arh8b[ʨn]41:09%' Yf0u&+^ i> CÕ^CG>}a >M?@W9ry FIo{L:!|^'R,TMפ Hx:=EeE՟}U*? II:/%849X!ȧȡO[T z)ABFORU('OP-'Ih1$B0i&|f :(&UB a`` g -Yx))D'LMmؖhrĠKؙ *}&)`\CQT2NdM: ͦT7@Fd1Y*)Pv d9Z?j?klwB3,) -`x|8HfYs|w_;p65#k;r}&ؑ`Qys4OhWލD`$<bE:Eە͋FsA8R,gQ찖 |:& -SࠬdB}*rfHP9 -Wv(,Ԏ)+y@tzm\%0+ԃ[*SO5ƜOxaG-X6asjWf(aŹi*lvxc[d5TdќB1GTJ)N)xH4v#W_*DYjFc -vJ?{qGZi⋝8F'_8zv%{@vٿ. ֡zܒPZdH/pl -t`@k=YR߂VD# gr[W\8pw-w@XCt=/Fc"nh />a=}D]Ylp{;Xb(J#\7 -1k{Gx6 -^IxF @Tp-Z 'ȇ|&I"\1h.Ñ_w!툏Q4챿\8Dޜ\;3vl& TZ;9!>e[=sV`,}솃$sJ];SUIqOwgC%F0o|/t|w=8K.96ڐŃzgh9~{).9)_|cwk'u/j$ dxRӕ܃b8tƘYA{=s895k澥@r}"ԑe{PE<4~?rK=C9*}5S*6>stream -HMVz "ϥ+Y1XHv0F(D}}bR po8۬4?lVylo^9VǜQ,a3,iEwRh1s+gE;Zg_9z;rˬYGE؉e~>sKI\1Qyae>>'yciz<,2ɰs6SA%2kхVG8:6{+VgN?-=nަw߾xGhOU>FJ9͜׬/JEivDT~meތӕMw9GcrUI|kn[ꃂK}y8-hrM/?|vh$襦+*i*Ձo癙ڜد -οHo=ZʂgqbNY~AzңοOz:j<ǘg~߯C~oٟ>}}[`ņɳo^wɋ_ߟta{7}zn^=j"o~CeaYeQ|FFPg-ҙ8 {|j9 -](a"C8e*/`^h1ÛfARc-s:wx=r#RCL f7kWUҤ"6RR3fTЮ-kYƈ0UKKѨ vP&` -(~!\zY=ۄ^rxo&SK7e<]+J~b9°DRTJHqh7Aژz ֶE1vL<~ldˣe 7XfXlSÖÖS= +O!{&{ehNw]nZg=3Y1u署tr,Ρ8{B7(cTdo @j(5 cJrX6YI`aU:Dlf۹#P:wjN~(^[( Br9w~a秓[ 4CFR,!B$ۑ"2/[ ԇuEЈ-kul+Ys6lCe䳸H/ɕ\*8quy&M?j°IcyT3`gRkEGA#IdXZ'qMQ V2j0o_ $JJrȒ+iq:]R0jH]l=g`JeݱA\iDSƤ)9_f`q"Y!_keĦ`pY -˅(CCڨkb AC. QBiB7U?!L1lna't²y$2гF+br'mV\eP<߾c @uɊqi}Y 31c1b̒+F(=DB;7y4 -|MXѯ=7'y,P+S7.N ꅔLR1X䆩A -^MD@&KCv,!thHH+6!'UqpT"hH"aOXH+Bi_'4)%}ej_GƼy46D6Qx#6jQ -ō@cÅj.&n3w> +#%~AwF3!ng^!Ig{R}l)V'lnd)P,Oӑ") Q+t%*+>mԝ19&R{Q|g^2OޮV;s]<&=MDRfᰶ㑚b陗D*Mu$, RxH.e\uAf(ອ$\́0/fвGin8 - X4ݸ-ZV1_݇[/&ʲjs#1"DAS`z+ MOx~Gn[vGCBA_g>rUJbN)p玩9cgó0~|̯e ` F Iӡ2 *A> %ƾbJYlu'<wΌE^b|Ť}pGL:4DT/N{'r -$#{\ǝO"{@!4> -v:81 2 --(Y*[eB& #lU@I%6Byi:SliAJËp@X7U {8s=-u#6 +ֿM1 l #25jDAR«CDW1ES1)xt%jYcL0>w^NaEj7?~؃(xii^ s;n Q^:OclJo\dsO=Utg6N+``4*J g9*R^jWQXhD|jT%E!t`XԗWŬI5L-N8rr3pjJ`=M^CZxSCQPi??A_'ps<$@@}αbfPa }>Q!> Etg[\O΂ F(Nm̘al A_!`@!`(Q} FX:@Ajlɧ`LN\f&mLZtsT똜Nmuڇ]fD֍N:,\9#`Agq{߮uB'  -"ytePePś(a3Pn֣n ݐhA+y] Olr12,#~e,T*EaPJ1zj|ME $:T&EWp}?+c}mBHOZ owA'OR?ي"G -bܘ7y1s >7Ϗf҂)ۡsXi9"H8"Hᝉ3cPbb8; fTU緪0˃( s/QhS#*,^f4E<<wO-Fxd؈4,Om޽v=`B?uXDυfjGstFf ҕvwVځM82ECq`PhOGQkS$ȘcboCLˉ>c'pU;_X+Y}B;h|v-,byg3݃1|FtvA]J FErh0 dB$q% N{k Pv [ "?ŐIXT"xa<LW}s9`^A1Ҟ/9/GFhF8W֙5%Y48!Sk@qV (a-&=V >%I14?kFgA*]IO#x6L2ݪ'ٮ8o, 'v@ 5SyR}ѐKs6u8Rj2o.bnV)x b צT1ϼ-aG?eMye@AB>]F -Y -y=!9R^hpy^' L=ehS`4L~[l.H8B"G y\Bnʱ̗GWJ - vE5gfw#!FR_&yc@_=UjG %BCb 5xhG!FD1C|?t E-'C/ecIqWKD*a[+7{ѽ@̬' -*2Po2' fL 3Q!KKEδCENTKbqnFe@"vTD(ӄyPr=ΦcA,zl;{(cN."mizK)r)n}$m˯3AaJjԥޒݫO5!MΖg(f`)ufw4]B0Zҫ%K^*춉n{.TQ/$`N|uHP–xt(y$F_N~ .=JjglB.R -JD$k6ɆօJ.3{0rhk2 .v׶ٔ"H~ (TUzj9h9../hh*4E{,e339*,>,MEr)kuD$U7̥5jJ_<\ 2i0zכNsBXOĔVfaa:5üN5=q<W լ{ &IR<PXR}ĕь)wL -k)}^yMB뉫WAqdÃ?ʹ.f c'DAb(Qg7EHy͎:A=@,W?f`j%܃Ǹ)J5 |=HNAW\'|`%K+|- |qM 4P~VLq0NsCf$-4@2S/yXҢ̙"Qciδ\Ͳ> |J[PY+]`U\B.6XƵXd~Dsز  MqϖݼXC(DDc8wEj ku*1|ڇB(?{{AP,ZP~KYI&BB3nu"<c$=pO$Y&!mfXU3031,?9Y2_ ]e!h 1Luy*'aȼA)"t~jEȹ*<<%\yglMd0wHyʍ\9Jkd,GnBAC~,N1sbmYS3H{)~헯_~o|LݙbZG{-Y( -Ux3qE8MыzOrP *&xo_soos_/=UJ-mNgDfFfj$,ˬ,ceFDVu{=5  cWGFǹo曯~_ۏ8}o?{7_KGr?'}7G/_|so?qٯN^} ߽!˱>gϣȗG>KiXPOv=V.$7+ڵѬǧr\[;-ng5TTbŦ\T2@& X#N0 L9&o {ger1Uai(f$^_m ^Lh%ܖ^YEx`Dž>>Akˋmuh -abYin^ -^&GttWB.D>kxb -%m+PYm{C3CY-*ܰ>6`N^Wشvᣗ[ٽ$#5p[K?zA?ZpHpYf, t}M;_)S j5̧׍ytD&}Gtm~@7ru)'@} dḨv8%)uP#EXx4;תKM[Vj:P-aizO,scf@wF;x{g&Xq ŒS%x_i Jp*HG*K089:[VX/˾uy90DE¤İM{M 3f*kܠ{/Qm# YT/imoR,!y@#zX,GR{yܮ3@sdIQ%l}Ҕ{jGTs&Mqs씙<]%@`#YB"Cas2nUC=C8tjLnȉYwV^0~dqnK%N سr.MRSv81:\+]pRAjl5*FMN4ad!,\놴ǩw]FHJX{5=ڕ3#89:sێNu y ۳1cϱ?8a PĈE)k+8eJ -,OU2f˅caVat\v䔗U$F [.2O*>n(F)|hEʩ*qЩ^?",_uo ęβDHT"k|;w4_>x[.sZ>>ݍHaf LKc*bOn>jb$`8Pau'".f I7c`\Re/p,Pdys΂\C2!Ef89KSxC Zދa>}F`YnkP4;NJ 51 X #L*:+'vKQ_ka^Zל#t8(!.5w)nDPRV0Oe\i'i'zD2H,OWj`rxa{X-5rƶP]˘-vh%nbۛAܨDnCwCqkn -F5(%S2?SXYPURLl;VڵgŸjЬt5Ic=niro%_.cyC`eg1 ݅0n8"^ (COP $C0=;jп?<)BРR'|;LkQ `8N4fOoYjn9暣ljۋY?Xz\LƥatuxysNdѯ84,FxbK.Xދ-H6,dMlCzn2X.Okй85O'8c[@_z*i3|hRu5k:D"Y {I=Ո*<`?s6і(ȌvSSdw=ɟrbڭY0a`۔&LUU$!'fjbbcxzRs"Z 0@u4MYqiO QATqD3q1߿$Zxhm./-LsUT6@jI.;ޛǨP+׃?s*MTHPWQnaqݱYD&3[/E:[SǩoS{ZʳNͿ2 hwh÷cae1+qYF{.ы UZmCjOGn/p}'9+M|`07Hre[tDRZ!$1eluvF:F%S$F}qf YE_d0`\Ty-4`-ZN,8-wl ߺnvyj{c]p8Ώd^0 }?&{DTx\ZjKd\C@†R:D, {`Ά#U繯Y'',ԙp _iGtyrq;7񻾫qe>@`VBd!ٶ顄ηFoMg{Yf9{'be0% bCW3r!݃Xt\:#7Un*珌׻%qY-XJ\gaBuk)1D\Qu9sQy -~`T)HeYuYeX/dǒ>{Ă%*^x/")A-UWez`v环3:`/CzepaK%.s慹rK/4zȕ'_yUWtKI~U맢ĠQ8|~J hB2q& sWse؀9X"*0O J&myiLGlT18:XqdZ  JI{ ORiHVHIG[TZ0?V1@zOEnr.|K|Kۡ -tI5؇j%JAȸh&3 )BvXAdo[IX"ٹJI;6T;m( В櫑ۖbQ>GE8$u1٨?9 *}s󘪑JV(P`kG2m*0>xZNgABn"I(}YK!LƐ2*h!H9$COx>e>7 -F [^;1PTlT}s\C.nz~/_G[#$vm' a(eE::f> ׂR-j|@K BD࿜;h@{".,蘁R`;jStDp - d(lp\zpKB, T ZRn:"w[2bbH25B9,$OA>uzc(񧼔< CsB/:y2xrPCj 꿪LY7vEW|Q3m!f &Pw5:1EFq]1lҠ&MNJ=by9tm "bT -TyVcP8vhxtZ]4f,DTaLP+\|%BL7Q{)j;nS.i&(G3;RkteTzfq,UXz^p?t%M#v -/̣|9`w֦>+5lycU5k"UJ-馸oL{ zFTRe#1) [L~6-<_@;@|(֝UCɦpx7*( ?Ȕ$dLOL.],pzR,+H-~ģv&# :?&΄2D 1Pe~Xk8/L rYCPCnYB02&ݭ#=5^oB#%rq cQ=D <{,`||zc}:Qpz)Ƃ%dkoR )>`0k ±}d,;/y%b2b'Qfv$Z(>yx$Y;ݙ:XK4bbX\(ޮj爣*Tl }BVaPX4L.wO|Qr؀j=鵠xC+*^ NȦ" t~r:C=Ā6UH" Bx@=+N*]-\ ݽcE]!PFdK4j}Ut&0QC!EW2-Vl='ohhG*fjU7cyHa/B(ȑYҌ'!CؐIҨ5F}l`}T~2uOiUԤY`a:9:034J spJkA+?LR9qV:r\IY~.X\^3[ FЎCnd$8 -8v3_P{v avB71Petyw_ہb~Ph; JFh9e٥<ڲ= G$'zfia/Cyw:qAztхK=*jw0kx !%+-N.וPA `}@G+g5 <8+_̀/ r>#|1RDBUŸz!/IcH;#ՑUYu1`2~Wrg0ڽܭ?3zu㯴y}fnR{gA_|!>lTHnxK erI=ǎX&b<4=INWӭƯ٧v82D&3@4w -k}){%SSt]r.+e%yc"z04 dgF: nkз$'>~Ut -rAz:+ݖ2:u#a$q'W(8蛍*P -z$ طeF.LZGQ5MN I|J:c c:܈E>|lU@7=) ]Ε*(-ҽ)sյāXa<3[oaҒ^`6aσ`29ɡ*!A˹0{ Ydͥ:Á$1T9.){mV>8W IZ¤xm#rq鬭bi1 un|%гZJ{ֹNkZ ^[U|?YN+eb*Y28"*Pm3bxnP w u FdC쏭E98ZF -c?%YB';T1 -/RD&PY#p@WBgbSh+vVmFKpܼj+ֲQg{f_]#i`ؔV3xJģhQh .A߸iz%`X;PS4{cTeF{9Ԩ 8 Z_]°uL? u܉q> С_nKhk_ G.8 ^9lQN&=XtY|6/&S0%bgQ+pu|u45gg Kcuny_PC+6}o˒(b|'`u; -9vVnE )j @{DkpV52}ͯğ`Gh6뤱G9S^(: !q݃m&z| -~Ou oC]|PiOڒZfcQ%"9!sI.4lem'}B0?Z@IL%e 0U~+TZ"`XTJ'&%@%XS-o`q;II8Bx81ѹ\cw?sFs$fpO-{v잫y"ąu[-\0WuInXh3^ <dO^J50 F^0F2Oz9l6dPOtJ¬]{A9unq]{iF -S{jd1"4-YY&!B7S&Ɓ`nȀl,²/m8ɵG}PIFt~;2gCFeZ$,Av^7Q`+\9w\2~ w^㼾uMC>Mur7ҷ'{yϿÇ\fe==Aʮ= ~ʆٽוO~7x2([7& .R=H- Ax+ m.'dvf#/ӀӦ TDR.K}ciђCMF 'oshNX~,=M0^szYaG wElK g5v7,YkFTf*b˃:D'"Ob?Cq׭[N ~{4ѩ į!n @1-.]0ES9;B 0&w1p$v85 /~k&8*W%WC@[RjIss<4.VU~sQ?RPF4n%nHCu:M x`'w 9 'XYSv𒻙ɳ;xXpT'AmZVFTqM'lU/*>`9lxe[eeܿydƨZ9/0bDyJ}N8e# RϷ=vk\8fCfڛ_Zvux1vV*,!ݾ~=n@QG[:ze=lqi+b6ցv t)?|H`zA1jrcҘQ!#%Zdo-h/Y_8xp qVc8~Xտ%m#=( $6YĵXI]xԶRNbW㮍 ` oY-cK*^Ä}y}+a8z JxID0@PJ˃@xom9iz|KO؃Rw]hfvYΥ m'm,'}{|Zpum;jB9 c mwybeTy783*@57T -}jnYHiO1VB%bV;,W0cp3ɪd>{:>r|h:J#/'L1&QM%H.{Xx("tt:)z@e=t3*LSi,&"Iw ,cu7x*"pT,RPl FpbU5ӆ42dPZ -{ʕV@N.SBM A s!dS.U%FlbTt1R0.mIY)3NP3~8V(2gGtMW;b%B -Xj {(=BA;|_&дV-U%/8#9 -cu"AlĠfX$"8X_V`YCRvQ]e7d;q2&#Rb%`AзU耢֐Ucͻlu@G0B} -dZ2x('s+<6SdiG4ca~Ke; 8'w#Qߞ @Aئʲ( A͑GH! -XzZRd&hӤɂ6O^QCl\F[%;*<Zh%bz}E d`&D<.aC ]$bj aJ!r'1b֊PNl_SVdWI2.uAϸjy:Bb]+ gQP&yqm!3)]k<~wƉWnņX.skE$Pc 櫇jR]]B׃UB?q.6H!b|,6 wU<R%uim_);PS"|mW\@`09F XVtz ׿nwahmCWpLI{b5limH!K3ļy1S@.gTTA%F82/APG*3D9| ;C-) :-20ri^ʚtMfC sF݌g|bM'[21r.c=C &6[!`EaE^! ϙ -)T`&&v. AC7B!2򄀷j3HE)g;ҙ)'F}]soȲ@m.+%9cl`bsb)6*&We9DAAEc9%3@%y]cY(>&S`_;Ȧ+>h0.4 Cf*vllQHZ27yǽxTůx`8M!#JL1>񌍈?I$H֔3cf!䜖*Ojŵ֣&sqƧˆw E"'D1, O^ "DZB6K+(\+\cnbgiYuעA'Ù[拓 -h51;Q>_kI+F?Khrq3i}}A{bxMo9وJ^8.TcR}U'p11&U`:)ߛdԙ#[|6Ul.+c_^n]^ˇwLJ<.-^א}%) , 3.7)I}VE۽=6¼5YP\~~yݧO_><>{o???p᷿~x|_^^{_$oWoo{`owlcn/=o?GGd[w -`0)W% 06w2Pf/A+UA-(J_Z:3KBXh+@ݓy6ڈ1珫bv? geVm3u?dHL 2 =ϤbPÀ`즃Re2ˠ*ӟ@B[!DȬ׍(͏ػ=3YteG+)cC,LTBxS2(Irdߺ2WH^JM) A=*wI Y8с-fTP1fW5EUI Vz`݀9n]o90GˈEI#E'z/ίy5psWpVoC`h`up@iR\r1vp-(9-h-Oӝp Oz -lNPp v^amD҅H3aO9dFUnEAXsWn@Tҥn^ZZ"L"M`b9:݀w{I?IH,6Dew,Qs"=X:0sb9ْBoPVVH*m# -;5ژ<5"<7a@xFJDBu$ -9XiĂP,-cS<^A:nTtx8JL=^֐&gZb G6`bd [ -(/{v,4{ЛnbD -gIsaF2I% b+]-PXL"P^NHD(Jn> 3/#`̪fEPʔ,7?LE$,;eVڅwu=gp[Lu9půZPȶåL -ɥ W&yC.r$90jw,|uS%T(֕)t0WDjZ*ߊ_@^ i(ѓV巠6!9 OC8BS?1lH1[R]׎<̑03 B=b)\sv(~9e=IdGRxDP$D!?Ťdf~%5[|Q -]cej< c:*Gq\2Jvik8ӰVqqHjGH~Pm]~~hhCOӺV!*Y"IAFU2ǨP ߪ X kC |AF 3NBUWXu< ֭BUIaXizUk?,GnC/v?ޜl/7>nn7s!o;6{k|}sfwysb{y8;&a$];^^_o/rƯyW6/vWۏ?lU_~?3I q׋Agnv:=|ˣw?˖NHPdlP^6ejv -لfPRQ -7C$q| 'NP&SbMk -B3geVE\~}G;( X\rx IEb5]'/q$wčC GkxY?VCeU4-\)RDR0%)SGAA@f3}d!dJBĜx.f>jԲqn߁mgD -fFs|ܪpaGcnAE/L=5 f F7Gaj eXP@ @,fq7$eaR4ԋoEBW1ri -ra&{jb"NsDQp59p!iP.L -kfM'Moӂ`$ћTVgO_+:V%ƀ[B4,|"RAE0C!еA͗G# h;'Ծ[?Z-6nC"F<=+X`@j- !fn7SZ*Vr2sYˌFgPd<,DK2D8kȀ [E$eY#6Tֆ%,jeC.rEF9h;" '%uh3#&)uepܙ aA1Ku"A1Ɨ:"4V.p[DW؊>@ Q3XD ͠9RVxXHrBu$r7DP?AfHӐ<jV0UxzžC<\Z S@"lvb5! ӲVQ$CT, b .iau}J Ea.׬N!w&$6K7W/iK¢(4A'awކ98)}KUOiJ񑮽Zm f "O Įj@+6̴~lJ{tq@SuKT{C -eF@ -1~Us<3Wmu4ێ {)hdw|")WqJ =-mԚ6b?#+xT `ccD$5_#pgU3P,j_kU3Q :J*SwH׆ rb`+^\+V0sB$Z1m.Yp^f͆E$c$Z"\Xte0Dy =/ -Ҝ|J9 f@cbw5 ۀq:.DU g;փXH)-<-$mfmӔ6n)oރiCayT~аF_m -g,-7`@8=Rh}`?g ]*"]YV`xwO{yV앏v >? &6!q --뇥BJX$T_Ȑ56~z$DM K'>0džO蓽iνQA=g0 D*@4tTxBβҚy\ks Dϯ7Č+u17UR)P#AXr'?(5C2aePjPZ)NXS0}*2QN5ʰB &@We}e -zqOl.`W^`gTC2bM=\ĵOFKtq7sCBt_]9v=\`[Tx:P?wS fYW\D(,>W} ldIR2,R>hdWfȲ .Pmm z]i O .-h2w)V>G>stream -Hl;$ D}Ehȭ Hj2~oLt! $95mk]nu[랫7~,5#Gυ#"sfev5Ö2,k[ߣkі"k!n;#1cXq +_?bt \#LY{ntA0e7($lXeG^2[ a~efx1ǸHF<:˔S r܁jWWb1 BFw6}a]! ZZ͌ajkz8 ;1NU_ލH^~rMrQ$N|&aKۯ5 - N#ͤ9=6cZ<ӫ7aPB>lWu[%2h3[:ғ -Y -賁8+qDιi=U ͺFVTSӊC6T*شBHwQfCX.P,Oz) -2B)d}_0A ᓞ§ZLsXg9COݞY_Zbq8" -߳ gtQcXě0v79 B 37wmM t]񡪵^ L,e|@,\J!*w撓%Bo?GH5AA v*/ -JZYgY-U|]K[|>D"i5vj14nyr߱n:JQ`/jb,Ng5Lo}QmAIcn$c4zBk:'>wjs;˛PMeCoē (4w{#eUA' 3D{s\.? bj.i:3 3oNt>UĔi@hOAS>9(T m͵hQ`jhi^{Pk"Fhoɭ?}w̢e4+sAH& e2V,uHo#A.d9q F -eYʎH=%ې)Wt3ӨDZU(^۠Y~=΁`]D&d(-L;8Elv4KtyKi'بy.f:0`\> #Gk Tr 1ŃBJ06)}ꌵip`siBAzt5Vg+JFOBH+KLin4*WgIΙ;c+n̡ԡ Qdv`<ԭS:%C[DjR* 7IO/y~qDO[(DN%E*uRB鰰KӢMrx,zMb!.Qs"\6~ىgh#=Nλ%]tGC]VA?%e>a&.R\G§T&JSr׏n4aJ<7yXndEVD"( -RA5fY8Y| 4zF<w@WGHkD ^D܂6O) 4$(@c@q,T)Oؿ_BKZ#[ŦjH ʒHh M.ހbo1!+Vq@];0>D޻ĈSF_cS%[)<3U€<sd?21TsK蒉.& E5cF"%=A#a`m*e&5;8ϒl"ԉT>ƄEYz.y\<;rٹ8! KDHYIAL$>W#$qvWwUkoʜ`Ѭ} W3Y!"0 ?4k(Z't${NG#ևڲ_PH*#h59)P]i1g]N刍8Wsx\΀ hu2"fJ|p#(0^dB!B~'7WFpis͡)a"fJrAv*5K!Po -@Rc"5LʎHXvk_s$gD E˂5Ryrx:1-7MJ5әrn^vj[jYStB[ .z縘@>:]@I@ -_VS/5XYχQ5{hht!vر -fXP -z:_؀WfPC<)Q1EFw٨ `Èe$'M\3Xze]ivHN潩X3pqֶZD4|e2<)+^͘$%*oǎֲO V<4虛 o53UkR4 -z1=I@X$m">tHf" -"bd:qiaʗYR6HcQ{Ջ߼wO{߾㛿|>7_>d𗏯 ?|{vHBy`4 GI#(}9~0">^\HW oIivO:F"j|K,čG c5,Wzp5`3R|F0GL" 27]Ix7R%6\AA -֊LS[soQ"ApSPwZ'.oH~b8vO/Sr,-2m;s@,0]N@Úq+d.vh;8.0BlaCygǽ*Q*CW|}#Zѵ)ӝ -?صwXK]=|4ÖG4".I4WFJF@[0 +i2FRar 0ljnֈ?zeTCb Nw ߼TrK%lm^-ISAvga{j -bP[],QNP :5/h׶'u&WbcLG 2oeUhq†\N`kMj(dqe<6G3vQEDbG*QaM#jSTavZDFF}?NuB{Pc+jUxju/@Rf?x({j|AZ怙*`酂jb_K'(=EiT*+AK?ʖ08@&np!^=glD6W_"YDNd35UBNWhڦB*TˆeLsZ@KrDf?An#j~ W6d -A \M-&'-Dx~F۬wY{ a,*}W!tbuS}"D~VtTp5MV̶p`-nڕ5nƓx3.%岾ٱXfD@J|Z!ɑ_A;EqгU@M:G-i1OyY|W1pBTYAo< ) P֪nD&3#"AVתBʣGt%ծd>S>BHtAt&k45 ]@BڰWqt&Gum 졤Z<^Ws6fvNVK?0T(8Liq [rDnp{!Y3U+j3 !SJ0T Wmb )"jk|ކ G&@.\i6CSnjXeXvlI9 '(h־S#Tn "SEmQSP|a@7^9]tƖHm)({ '.:@,.vg|j?ιd2 &|DT,IUʓ>)mq)8j ) ,@(H[g`s6v u#&"qa#tg+$l` )17 -F`nK=fAYo0˂[Ҩ5,qW*6)-?4jUF+X~Gޤ/nH]rH ~R2֛Ifi *aVI#iEz;)ACOyl6>J{aH"@2K_W)䤋rȾRW%M\TgkAɳ jAv@Ty١BGHi 6١Lw%8ݨw!Q@(Xo²@zbJL>Qh4`›q.\ 43LҌn*d N`w߱1QD.^Զdo9֏ڎQھHtlR,مzcr 8ȇ> 2er'T852{@xN|+)>T 3X_N?ݱSzE/*]wUH"5=^q +ն@10D*i9;&v\S ª,~6B=''ЏZKA > l -"$ -zi"b\~/Gj SQϋ)nMLQ΄N(VN hh PL4DgFI web@p+bҽ *E]cj(IHJ<2 E[4=lYH&*EW htVF9[ai$|D]B!U`%;JE/@T M8v~*I*T)Xj1VH)q5U[V(g!$\ -}T\Wq@ƒ0* ^eaN*͠=?V~8 M`M@&pH72bk' |ͲHi٪f,;02{dTΌl"Im~ujS*JSAPed4zddhtG=7 -Lslg\0(%A Р؈0)9B- ӮR^ah4"m:McwS"37Q5fO6-Zhu#-FjĄ= @n&3!CrgɿSUs̡JYZ%\A#HW9ެ=h)A]d:PkD2HҌX4]'ی{1m%m)9W߁-0K`+P ߓG -'_;!Y}>^AZ,"2Y]g'oE/;2tRQ>LBT(;?PV{yExj Wd #sRUp{7Pw)w{;# -, ;ތ _GJ/3BJ6QLƍyD5RĶ0)gIܟ&I? #! -e0D2bH$Jg Nțw2#]qcZ:D@ڑn󴏤g(0]v my7@3K)G -YF#L_ؖTJodl’"o -o ]l ͂ض`LlX^`E҆|*FUة;B8\UO%T{Êy#82r$t(= ($)$5lY~!b.DIgB>S2CN(7шNDU. JI/mG5M?#]v~Q,^ŇὒˏL 6l? m2ٽG41ÃFF{3 dh&bb)8I~44su.[{K -3,ubN$! {5CfHyWtTm;*3)m[yx%uPŲHIhuBqm#g YmXMF̓E]rNU -A0•`, ݄OOz,PD _ڴAl(}e7D͗"˄R?S)"Na66ϐxE+4CmٗkBDz1+ &$mâlih<Q⨁hM95@@4<7IF˞]Cw+|f  7NF.O{PfpGp,HScDXu>6}|0!J:^˯3~[)d0dmppGSP@ -GʀBl ,($aЗ^n]E+1 ڙ>ụOQi$ 4!1juv:G B^U^]:_lvz1JNEVYZĿCʹhId -e-}& -Ǐ0ᐬDu:O ݎ3 -3ᴕqTyFLwg7B<}h4ZEPA>Z;s`rEuq|^@~}8sh;AAܓ,/FVfCE4dqȔ,ɠE5ty*wzNU5[]L  s69 u ҀR OUE >)r?yY 5qD@@Q3Xj -HFų"K躤p@VJXzڥry=i[_Y)!7$4h'U]U@]GxT`G3ۥA:I}BC7ZJ@,BQ}Q 9m6ܒm -mN ލHpsU]D3Y> - laj;=YJ ̫&*<>oNFQ`̡ nzi%BHLDTLAǙHf4amGb` WՄzz$7A*8-ah"3m&+<ܛ0¶700q#XE+UD,7%z#NP'nCc"C #֮/D7hME1=#c}̑~ ʬu"m2ŏoѳ֪!qCJ@8ΗF"b}L}Ӧi4IKRC=bTh#4[Q&g' o،7AҐVN뷊((P ]l~ 'p}M"'"'hi4W|?J|BCa()FDwxm a>lJ<=qf%O5ub,H((K1]&C2I52.@3l'Cꍚ -&S@ š$ -2 QĜt'xi1!.E}X4TT!:\9BҊV/N0&`rB>xԩ~VļF:@d-'j'LR;/AlE6N.f\`1 zMT|O*JOtkDqADvЗ +ρQgV b`SwBgN%e(޿ǐ: x -}Pq\IX0 4#I%)2״3yFl7ńQ%=7Pp\٪Og<:-j))Scrj\ -ɬ -㤦v'=:e6nQz378'PH܊=^7B9P/5aOMDtLh' }:iLM'([nJT 2, -X hB,ڛ~V$ _ yBI.cB}V!$R` }O+0Ǽ3f*"M|4M$ʎQ))1j <07(7QKEz QJ؉b?B8%f`0MEsAR?2 V"A1Oa)(SM0ӓB,Yj0/⡎CZmT)&&D3.%"|H8CT}<<CM;!Gndw"!włm姽El|8UhfAFxL%<+YX23#7 0v474Fbd2-T%y쟈;3 dp bFaKMATjث;)n}Eӓl(7䍴qC;nX2"G+M"Wr:WFNO&F@st|BZ]k5 -&dnкC|ɅM͇Aڌ ob/ FB;p0}E袪_`.d w3@,9q[aByU|AJ堣lF0:HvFIXiN1s\Ud]`aXm1 ] ݒ b>GDLէjb٢"J*6&!er/Pʮ2 h w P@L\m*ם _WJlI[tr}¹![E70\-Ky((L SͭP ij~A@/UQr(@F7T^[KD;^Ị0\d (''-U `"}0sXcK0x %a˞ TJ&=u(N{EX老LUk" ~b/@q1W%m$jx L)!}Ah PTW0D|ifȊ)(z*M 8=rqBˎ[vb"7}" !Cy O>p* F赮P޶tMFR|Qܯ_R5=).<:(Oꆔh7(Q<^wLĦI@kBx>z4Yށ)BXEV]GU/3L9Z>IA!2`*E-^iOlm =Zh"0z׺*rHUpdsiz̸(Q"@{1uPۉG)#[n% #  sbWwK R"P"US'sKWHZ)g$@LmL'MtB6DC`¿FL]sۼ1bhP3t!_dԏ8Y<ۨA4WHdWs#_& 75qkA9X霮4~s zQ7-<* b@EmW 줎%ӂdQhJ3pmtrs6%z-/n[Y6RT #2x•&qb # -&6Ԩ@'.&訋"oo#hΥV@<BK֫L#6nLss^h /-4pяdƃsYaUMb eut9TjhJ:sT!z]մn`5cU B~GvnȻ6^zg CͺچՐ&);I @ "(lT _v2"6?mj'kT)5?DU57$}OGEš>&]Y yk@}1+D&; -gق/=.|FzQrP% F",Ho]XMUu ֡A1bjU}U{ah6ŭ8R۱] 9et/ڛ<&zv,;|1^;2JRSd@@ cX\PoL MWtenP)X/Ohy.C0j-͑"γ贆J< !HbȠ#'ln owm<ԄchӁcn+ -ܭ pYev2%)&vovIag Dӵ&e~0PNYȣj_Mܡ&ʭI><=d\EK -Ʌ㾌If}r@<2kuΊ`Ut\3lJr"'Fnt .a§!Dӎ-;4Z`0ܸN)=uTۜV ݍh9 F,l#Y -5EcnR3^M:jI'/vSJSsܓ[(lB$2M9yf@߽{_/!;7˳??||o˓/~^ ]s˕ɍ𿗟O7k[|<4-^cy?zxx퇷޾cq|wo߼{Oo9/./|{nqPP1gϧUQŪ4I;Aa_(ܑD .h@BC3hnj+7"(eU1EBIhZ/(} -ƍP{GBӃ=*3yp7@l ISt}zHx.]}^=MX9<ӎ@'QMP7 %Nt{q 'ݽGCv2EL !z Q:MQ}\DY0P -la$ T=ؼeju?Ҵ:nOm^kWf4(ƇŸ ˑUQS/H9| -[nh޺S}.AQN!nB%n%Ӎ2s=6B遂Hz;nxܟ F-Y -`NXS$<*wJyaHq_>Ƕp#z^XAԝt8.ʘR0ëI>0z؇ F:z %B4 <ӭaiBͲ1YwwFW Pf| ,[&})tCs}1[9# -aԙ(G*dR'tHWj$PQ^ I!T#ں%^A%[ Ev8PCR -OΩ+#Wto߈3җ@ZX:WKz8ڜށ"% 9 &P/"-;ʒk 7:'h*}Ҽ PRp(k:ԁP֔b Ǘ㤍 $7P5InD4h=m"?ٷ o=&&TD6_(#*ed T N S$0EuԨ|bUBh`۩*r Bo$ҭ 1EOUܓrt(;4L@#p}vu@Y;'4ز-uSe.K׃m \cԃ0gZnZJp8lF<@0'Qk'U0b؝&lԀ&"T{ .Mn ͳ+@2ĎLE A3*)4Uޜiʞ>AbgEXۼ2 ƅp脌-@1 BVQM[n#k%%HH@qbCz7Jѭh߳YS-[p' GfV;Y,F[[+e L,M3: QT Wi<] - pZOV\*8?4WӂҺM6+-C1VK!bijB80z{f_L[JT)>ʔK BQtFEL|<4tw1c\݌ hR'dnR1 ^..Nh ~$)G3*smҧ;+gpDj?{,uzQpISG2OPi۷W4509tMWV͟ %s]) O<+\Mޫ@D2wXRD.%Er2 4ԫ. LI+h;1%(7-$ˠRQ7~P+< ~Es0خz+'J.oьV)3^s+~p70S9JGQ!;b:}Fʦewr/K A'YAmuy|n>S]v;"p̟l̾r8S8 4}#4BD^4U)P3tC$`n'JU.^-{\(ɘ54r,q8ھQ&Pyq -}N -p<F/QeKpn3JD7I NC1 -lz ϬIP $Kc\7F=),ʏNϛxyú@ -ڑCQ6.{$B3,% -S_q04tȵ88aa~]sPү .f,m|{9½a/SVLj@=!y( =e6(< 1Hou19>S#f=GBن .!_yAn(Or'̫+)ZQ9D~{ߙFZ|rgw} 5-@SϱtK_ za~_ tNIfћZJΩ2:IqpҀ$je߮O*6Nj^9 hd"Ǯ|igs_r6oFqCci⻢˿ǰ -߶ogٸSiX?נ:=6D4Ə$+ s}$@Jk^:JD`M&ɍ]xJsވPpt`7s{OQc*=>VO50[>Sb]mゆ&n6}S~ x>՘)hib^1,9%H'jttpA e ( d1nح_D¼f1:|[eCġmߊmV^,܎ _ }AX]=n5V5?5fS?"G>C^^YuԔa5~l"`jh\U`TWы) gxrsEIXbE#ߡ]1hq4ur;"L3{}I찊ܡ}H͊✋js:i"8b :fC0!1-Q>Zs,@ ,9Zc;xkrg[r+x2o\L3YrfW,(Vq1vpn ~j8rj"Ja}ˆ愘&Ia  -*sBf,'Oq9R7x,A&?'`C!+Ɲ -s OES}xP86D06@L4\֞{ݕ4LI !eK}trJ"yWSYCߌe9jP䚏]3%B>HuzۺpOBów5!gdĪvo0m vDYjtz7(GɧW‹O$LOXij-YŦot>(!_(u,1SB<~{P$q8H/0nhctQ:;ж6&zMدWlܦu HZskz'*^ `?M4'A;egă3Πʛ8ԥ,e;1RL$ ->7^׍ 甩6oȍ)&9Jߥ…P8.o;K-fGp-c6w3ܰ0>=^a -ʬ9\CRIE@ -$Ɔ!Z$oxPai˙zBU\,M MӼ+{Fzpjˬ`GϬNp7<3sf:IѨb N)~e|FAI,HXteڀ CɀkΊy'G3l4_ V"IM&% 62@D,_iS?]kbgwQk<^* -kiKQ[) K@J⩖KHw -`8sdMfeҥL] -D=9XO:q>+ DTjz߮?M95JBMkP Aܗ"#uyM]w:(u\^E`5YhˏG}Tv3b0oAԤVbi}er&s,_=쇍tg kG*q)8qDU7(?M;D'Ⱥ㤾. d&1+h'-nUL}(I9uE񌸇0 PB#wg"r}eAt- |y]kT|/+BnW6z#Efto~E_3%te9ؖ-[CJMə e1]o\"/R6w{ܾW#~#oW(+DOM -N~P(O$j1OzoKZqiO*[z1'} dd/]!ǰ=V)<ߣ>1yaX_pݮأ(E6mƊV7BQc×-{+^4Zm9#pl G P]Dfv!R']_Zmi:Νʘ}تlW7Ȍסb -[DVxo@#H 'W#$ 64ncdkbLGL!%NesڹТ떋 Cd0neDE܅\]4ќ"&`!xÂ]Fɖ:sDH貖cRFhmxԁH -F@1 ÓX)J}ϸEC16Exp҇@Z4J^op0PVv5}>h2th6QWw{bD4`$Y kɳ[]ʧs)FaU`rjQ8kˡi[<GnP0[V#F*R69OZ).9-—ϲY|| +x;h$ 5aN C6ϚUV;'3z&BIZ b82)$։Rɾ`˝@t׳s@$h aaH:M𸖫RV+*8Dž@ڔ:|#["ðwaʫ~ "3࠻"EWg[f'm*ҌpvZx/Gd KB^Ui4&YI/_˧_~o?W+a}/?W}Ǐ_ 2ɑHUxf/ZtC*?[D31ňr\Lŏ@/Ϡ!=|@b]_UGQ-:}cwNjc;E=L.\0$:g=jfsF*a$foiK:HD_2C#ZXGpN,UO |8P4ܪ|/+cpG-z<^ ]cҷKE`T3@8`JzcF6i8]ՐefK`n /S`jSy%ݫE%[] YT/כe8Mi0pFBUAƅP+NU.&D4̏k{0MMd(kEKFQk7z]݊Á-,NE2ai g2;DrBk(;iHx^B\_!  1L!+b"-f"-gv51s82B0L3x+^3H3vٓ*e"7nMZ l^Y.MM3 ްLl~jjE1a]BIȫ>[ PwtS"7V,+"]l+qJ-h(~#NZ "l 1k -᎗j2j9i00k;Uǻ6؂`1 Y?+bCpNs*z)\Z& =IC9TRPPt[Ng0xԱQOXsy2 |/nW)kKYmůk{MNyk -lwܵ>AW^}K+5OK~9Җr<#-)sns@N_cAEZ@E-dK8ң^RsgR~hu+wv w8.8W5>S-|Vf|MJbN%H1Gt^XokhCM-qT͑]Q]>(J5oYgh&8ug:TY@9nADNj -E~s먔{DR :@Zz%~g$ipj:g(UխkiAپ끛u4WlTUG eZl~2!9RǪ G "lT_^Yc#*M;sMz:mVFߣxu"*;Ȫ# :<7\M~̷FԐZ -YW\[=!#nhzǁMP̑򙩁vG#-P A'F,AS7GqO $йE#\ays n Hc`.ce9@p]nC|48v RF@eK7HyE_>UYV+f@4$op -4*ji Y.,6rw20tP, -+W^gb#%}}߱r1KyU4AQ25?RҫlG{5}gX q -s&ft{2)\&wm?Zb}qӁn!Dm٧GS_GJQuө_ OǯTU;\!ij2Ya4N!)} չP+4"7 AD-zڣ`-u;*h񝭲odM`󣖤VNB;yĜeFc'pўJgDv~;b3'Q}lj Lw}*9 ;?R4g.3^&t-+xi; -m|(+fc1bѹaVj(p (b4r< JAS)P٘|֋{xύhGU˳^) TO.@n6hN@zU(-Ϛ - ύP+O@anDׯ"YO 0G I -ЯMz͑@9yb`Bj n5-kf $ÅHZ >ﴘ )OtKZRijqP&LOXCƴ> *Ȃ,2kf|ݘ("kiAv~ -:v4N瀹Nff ^x*AW*3TױS _t䯈Ujy l%\ς1 -<-썹E8Ma+,xQ䱏ϔ+d L-]`E `?Px֏BRHW~ ,AIކ@R,-Ê?@DSPƣ.ak;J}?:IoBUOFɠOeFN}LsLVVq$P$$Tnd ݩK%K!6y -w -Z([@IEI.j]Z"V\TJ!lvB -Fn4V HUZB'M'[uB3>RLp>M;vb*aKX1OEl ޶XSFX{ԅhM)ǂ( ,ޓE($Ω!*7)`$ oꀅ_ /0YLC|?|_+2g? -"(먂askI4/F BqѬe@QXiYT)%B- l].Ǝ%5CHaE B3E1?aBT**pE`mm"YiK[ T^>jtJ 9AH. H"g` A hT׀(*)MyDXSh wbYU2I)+DE0Zq+U!!G`R9ʊB:4bb0 O(PYkF> %Uл@DBW?HT=ŪJRdI;R\QL]U[`NGDFp5U=*uT7*uF+w%uc'x.ѭQ H ))Z.\L>>ȚKspiن <2ƗpSoK # -b:e-|`pIVÁڶ&}acPa}}m(u% ;!.hΡ7lT9`f~ ]ˡh6:ãrj`AdAT 9\>+`#S -vV8cp'" --nJD["°넵-D69(U(p6 +y=v#H^@?&W@Q~SY?sci4,zBluCQ0P76Hxڵ+< ->NIo$VC-e4p -KK 1sB%b7aXm^a?B;Q <$i3ױ*_+ԪZ~lR$Ui٤*Q`.0"m\͹4n]]4Ob +WaF]-H Wj$8D[o?|㧏_>OH |}}/?~o?/?C|s~?l)88>\\??a wy-\ȎjN܆][L&A,Tm<$ &FQSRjǯ0`` - -endstream endobj 39 0 obj <>stream -Hˮ^GHÞ %tץ/0 Pd A)2H=ߪ1FL">ǻVZؗ==kGcXsG[2w5ReqoY2]bF'ޘdhbO7b> -dsۮi̽.nsk{ۜ׷i{8r>qnǍ/o`Zݛ]M'=ld^t>X9fYWt6foRvE#EuA>Lvٶ1Vu1RsJ}q ːmr!Q|ϙc{6gʭ_Q&d|{_i:ݺRfxѓZo}1Z;O"m"qNm̶=ߟql&t?l|_1ĩ҄_ټbr!e2jF(,"e$C f1njtXͻȉ.kpge:E2d2pvfeE;nwZ*1:@;*1(Di+.E] -( EUou3cP2uc|2CS XI&`@k3bO=[ js!d@!ϛ~BaaE2V <*7.2AVU Evx -W8ªx œ%3ȰնGLPدrF"^&i 5`H8qY򠍸-U*23| -&O;V5ƩˍgFޤQ㠍.!!{E)k`C<9& _r0sҏ֪l  =H3|솚\&Ou)+D6y(mC/QxT]>CCCrhg4rݥSso - Clm\`*WzBCKHy!)TK-=LT@>vTTb'1X]0+s@] U@LĒ^M -sӻ %/=s;F;,jÇDxW"-T  zHd]/SU֪B(x0Jck#6@ʣyv`.K aeTS+zrs0$媜 Urbk~ ?~1RXԊ ڇQgAEV`]#rxnpV)c$@( -8dSÅ^cؼwŷ!;P =wHksIGYd3ݔ~rJ7p+P1ȗS7Ksngs$!{JR\EbjYd2hP~GkE|gRЊq Ǥ7/ qGX~(ft -\ MA!/s 0b$-AFQڼÉtG< tDYcX['" 7JrDXHw gXU x'G_8BPQ‡r wH&r8ҙbq]nT)X44'pmCTmR.ԟa1vGDR;س'B%jQR)BQ 2!}ĎV#} -p -a@B@7ҩGĝ>40h폅& -?*LX8Nv%h-\Rh֭'FXk߮0'bxe"xv"b*X_ w0ac -}ջћ&\DH=F4i4ͱ;(5ª]K4NL.F*ǯ6m].뎐`G4Xy%@J"0avǂh*b_DPy}v)9An%}ڭQ~lxZSQGY,4~3aw 37儵5R -ՀQ --ڎr(^DX#D4[o7#Q㳕ZV0L9a9xd"Z|'B sX}Q5h!=1}İcoe/ݑ{ID  ѥ?;̣) zvM9 )ѺiOn}.:Úg豄5YN3 BTdX՞1F r/ P߅z"b3UhgCѣ!i}`1z6hd3]uH%PCQ-QE_= -Ey]칌zH=3MM'4Np} Cn - B -~ 0ވ@ZRy 벞¯nȠ^T@c~Tn @8T75LEkpG38 -M>]jLC-$th`HR1AWr/XiV Ip3KLGFSE -Ãrs yqzvTr3,T~;]PRn: >x x`$d0"($u8t|!675-'22#mz$%4;@\DY&,,!C<1T -!y(WĞ"G+u-A&987Q$~ӊMHPrsn8ğ|@%&>Q %b|s$I; rPrS#x56πဂߣP[5归ȥ,`PxWU[cӛULwTD̡dWDw_8A>=0nQ}=QC{g؋*nh @"c 6>zrl n@/q7]6Ga\M^ۭ8"G: % L⣊ծQ(WJJ p,8`Ί?Ր;&`w@ -!co8ˤ$!K1 K/i&5 (.ض9~ ⠆BGZkx~!qGdXqSNNĆB,*4r?{gߣpwH}j$:-1gH4]% -zg#c:FS昍o @.fgG[)1Z]Ṫe^)V7S^f?$(gukIͽV?'FiueУ!y}e6xq';g~H&!CQ}W -V,i-T>b]H([>F_ 1ۚdZiz5g̢. -@&|` !,\P $gTL*g!@9̳h -?Cf?Df z ЊRQx`c X}@:|n~^Hx5skkFɵC\k-LN^_giP|>BP8u\B>ʌnvѰjf;4&d<[lcA P~Y-o3^kzr*Do@^/\HY0H0Ֆ$b[`I]))LBz?(U}Hraimő#]P29\첹L$6/,){O3%pV]XLQ-Ht>&cFeVhh=|C2l.qCE'Q"> kqeN6s?z>b⍶CM yYwk3f&i?9ޖ1bto$vk/A$R$^rKn3zjFP͊Gd$0 bCjJu܃{Y ~8(pu.s6*;6Be#rԬF1MT$f'=<^}{4^@^ʁJ2y0.NEuIZ -77R)F}!ʌr;D/^_j_9!fU&aZTH|}وj@>3kݵ{%6gP?e,C23uMӧG -z9ƳanB$ʝtܜV  -q}27b=!>QAA ׎N!S Őn(z Lz*A"gB9bu/mwе>n9@\vl`nLQh/Yr^_^&)Z$7Lˎ)~SHtɗh3jI EL. ۋ>AX9r>hwnӯk:~Qن;wQUW`Ud8%Rs/!096rIU *2V.9$7h} ;9u4wl@zowt n/ȹ>ϺkjpD.|흃$ƛT27& h^gM})08uvZYd3M|^ٴ_2i&TIlI.6PtS\(P 1cMusH"B2Y٣9R1^DQ˜FU [f|H 7`•gAs#Е[Gs--F#TVU{QgL{ -fSYgɠky?(ҊLA}GYaFA/ΗR=0$d(OK3J6 -\F BF -u 0940g`DH^jn$$;(\u,}gmzmB|kCmIB\Uh\mA -YJ{,[0^N -AmL\ -Od6hULR<#Ha -( E+?)h rM~׻!{7v΢Ə tf4Y Gm |I/7`hD5}+nSC}#WYi2rơh*%br^Pc")x˱dsm!vh]2ːOӣx-G:+]zoJ$p/if0e>-O+/bNѧJjPUs "[e'?  7AZ#0م)m4KH$,g)2T&~ ƱهjԄVZo(nԩ5~|_K"Ih.MM`g>ˁ q9*]`#Pه. /Z,vN8Y㳀G:3όy?.M6?Qk˕_GV#RzQm r"l -9dqq}Txņ-0siM4|C;U219+얽УfeK|fnal4J.ߟOk(*lE;$0*qn\_-Tew5fM v>\h C"RII0ǠЅ`Q@m<ڱ{=8×E]ma Dy! ->־o +jV_8lg&`EKO7ZQTxE>'ܭ3 -,ǚ,8jA\륋! ..#^UDl֯9RpIb0qܔPl:U8o6B!d7YK FV_l"#y&({;"p/:δBMYz>m25>aRl+E|9 ބ܋4nd년[n 4e 1?@!D!U @fEOʙs@>g__&yc(X G'l'nADY^xVKUDb.הHmkΩ# eFьNhI1ଷdR&/!@eK ̇܂06uY.^Ϙ׎ w~%% e%۫7ouR8|HeJlfX=XtĦWi\wXqH"Q4W|:6i{o8{jt/ T)~#7B ց%oHq)JtPBT8Pwk/ :_ -&hCYhCPHE,qj=҇ eOz+w:DTFe.ha:\-JJgw*A;0IdDk%Z?kbY u6qW`D61<;Af g+tH,@9WUt$&b+#tw^a:Ҡ%qeaTWڥXŽy wBJBoAaBjXÌGdP@{񑙱ɞ6ளlrVM[C)jV&c);ɺE2l8v@rW֞jRUon#)sdR;@6ZD$Qb" SBYWUY9VAM׀e9n~/"2# ]Mч(uCÑp3*؍z33"q{!.)0q+HǦ|ڿ4`) rd`SmYꛝm*Z~]*~rJTiS6B*d-D Wf1Ƶ"!2*ؑ0aL[XfWIʱ9Yg )SBesWo%mQXE9| SX*gK;p߯8 5f*.8ۮw"|=H`4TL -R. - qj f?ցԯ\02[ - -9H0DMw3.٦dڲ.LB?[ tb,xq*i \ xRuTԀTp=tݨ cSfڜ̓Xԑ*x4(w ucF9ɨ]=^hү &qH. K -!VmrAn!O -{eb ))͍)8bnDWLGЗ%};W=^ߔSt;F)PL[>08&Jek @]H{a{h68EwkG̦ 8`F@9 =+)tAROg7arӺ5143;a$;N>[K"myyh*'ɩyM*93xt4K^;;auUzO'fF*C1L -3XCPxXW* ^C4-*R)Dx"OchGptbyAco oh[[tdH]\N*H`P'M_i2d{78"?zc,~+p\XP3ڇ -f_mS; -\ -mx=Qľf41Fx_aV#Hi Aȳ $ ,4ZN //r7j,J(\|mS!@ шZNd`^\ Cnj W?;!<2y(A(,8-DDZ)열l@Gf+GJeSr >HI/&w*A.lOB̍?J/oGnV{0?gm2F@?!c>nLr* - ㏌ob9[=Ҙcԫgב2-"Bm^#, QmH)R]T[-4dFƂ^.;E}Zyj< o9U#[feM{2*/p!ܫ|z'2sNL/)r$% D)MLҙ^NAxЌ?D+9߈}z!pI5rnB7ӑBRB[6 S"GCl)DHb["#?0,qQWP8ZmiHxC]oXE᳭#R -f]n;%bes;]O?qDt- -NI!Fr,K^=QܡJyZDQ٨U*Sim *;|VQ[81czVYXRV r3ʲEJ95o9%)Rm3+ -', bV-0TEnl ġn#F80V #M00ŰDP݈dV>ρKT[Xد(I6QX -#um%ڼMa9p 7oՀZ^Hg ;d:"U[oZK8:m?.dv0< ڠYLv'jwa=XcpRjgMEPPǞ!ڧjALrz-O/KQpͮy0ZP=GEpm|br 1%WV*g`G^'"O33Q[/wtIDq;G3^aEic$[h=cG(aGug{6πҖpt ݫLdK-rIEJ k` K H)9Z?c<>2I!7uphrwHA$R; JixMa?+dظĺ44vQ"ZP]wFYJdw}1Jve4Z5BxVX)Wm9S FQ0,3fgkK#]Dj6ַ)4،d@ɸJ&RZApAUq -ZuMNv`H1C=p4Z$Lo -C4(cob K2 Xq8J,JXj,ŝRIijӰJjql=\8_oeg˝0m`Eo˛T@uIv1+ZB4K]2#a89Hf}t[y9 (*''e@)IVKOD;)0}v։RaB6Nۻ3ϰ 1NpM?DF@jg=2Ҽz:5Y{k=: -5S61; amn:U(M{xTCCQPZ>C(I{Q?Mq>2ŝ|@5j8Xh%Qpf&y+#(4<{}_÷?|2,뫿~?_~_^7O:#ZNʐ-i9UDpikL1G-iz˺<cA >yPHKA#œu@S8Q v9 -7 *NQ4qZ.nE_u rS޶=.jE`4(z+f"#4m4G( ^(SUv`F]NWt߃۬:k@ص9ڣW{:[-)/2w28c )u3(mկڟF]Ix=A?}w|Q_Qy38H96P1`PWGG0vP^Ց֔BP\m@F$|>M&RCV*t d.pbJ@$f_h7{NRݪ4v)UR"٠Ih?$;8#-qzA -lf6W ˑT79k6ՃAb@gq[,u^;ivi=!t$y]ګ@/e fPkjR?=G61=e1{B-qmt5G-Y^4eǨsSrf 2X\1P~%76⥞$`Q7_PӷpL9䯪Q6C[_^̶ۋor$*Z uX bi9sg%kY9w5ly_R弼gFQI``Je݂ vqrNcW伯s^Cgu5sաν[]q%߶b -5 l<͜A!`rL$g#VΣSAJXP,6νʔ$؋.! MKT& ]$ؙsR۬LbK(=lWQ236I~t%V[|y-q(UuSWFSAnǢ֝*=ۢ5j}啃XG((/nFVT|d\庳΍)H Ff  -_ m2}Ifz\   [xw1ߏo6&S-<ߔ7Y@@]'&mKM3ӈuQq$m5& -&)uo WcG YLV# ߭Z)w'G3m t?1>J)L%H|t97o77iќ5%;SQǶ^8H؇n3@r/1#MфN>$2 $#MZ*j`1 # 8A u7]ћ +2g&xFQ3NarPġ]ghI@X@K>z=a=."HDI1p D84|05SqX9}ymMۜ3,eWX{s=Y:ؼ b[|k_$ c(WةOXyC >_ڛxM?aIڀ4l:Q׳SbɎ^R.'XyFmuT$X\jjOفvDe:Gq)r3U Ż/(?qwFց@װ|`>-;SqB~:aZR ;Ncm|E( mƱ$:|U "ތ1Xx -as'%KK,}.-hLSLrR@ֳB2<O)>B_5|a 1ꈈr&­PFSnZRv1cuÔ=լ3V 2"bD%XOW2< -& w$dQ)Taj- WOK1L=P+IDqߪn)`ts `ьj|AEyUovq8c?#=γ5]{&+ -; -y=m=\j!41̝xd%\Gv .K=JwFˁ4kEP(.1mWD0kShE+Dz]IG3܄=ztUwSԹBn5[ ^ְzBUs =6pJ˃ȄU^%T#P9*w%P Ҟ b:Dў0t v΁Z.KLfɤTV0F=6v:uTx]^+0awQԅ0L_BAwBVRC5F,*/-X;Po)"FX IXfe@~ku&=A3bl± J]`4gHƨV<}bL"i.sw9%aĆ ՖupҲש7^yjU+i1ehΘzX"ZaL5=0q9 1Gܕ =,XOKfYFq!*&S{{="o49?4iesWgRCA(,[PBFf}^lAkh B8p^F`[q<ǵ%?F4 T>¢:A?#j8v$IT7U(&{`u {bD5v#rˆb̈8<{KDT b/mR^df Ts5{d)S] rQd]#xL sjlKuh[<-Uc=." Fuµp9-iB G aKvdh6³ɔ7^YL9z_ [l(a\o6s{|iPzt#pwJHi {Ur=8Ȟ$ĕDqF&JkA a[&LQ0-ANN4Bߝ'iFĬ2h"2[ (pFV5*FW0; (;ھ:T`gB o(nhbDD`@026z; m SAETRTa ji:#4tmmj(TQ9h83vBT*=.m1fhC*h)..& ɭsv^"_6$!:;fE(0F b& $JX p;3zCت.N[mM>D 4<^C$0iLj^%ri8_mݓ|YfHF8A }z;(1a,雪W1c|&0< %@fsYC˞u:m9<0JWM|%>D\ndZ׾bN  3PB**BVea,+6x:Hr" ԔE1E$rQ}١,iT~sNA`+|TY"MF{l#7 |ɐ)uciϸ]P|% XMH- R޵ I]d51 fq s -V xȄ;%)k<K`*Ѡ*?-ڍc >UQ#lh~03`kt\ N'~AX[? _Hڟ,glU u!!"FoqlՊځt +F]`4q vAu1Lhe'm.4@/@p?rUCIq;7W?|w?_?#*Q>?ˇ~_?ߙ;S_?l{/_?ol6B,Qe@^ÝC\^(#+2G ]v-ACĊ=-s7QlP־1B[1'ctn2DljKlr=M:NQ@GO̲u 7.x5f$bV B0'7GMc(y#rپ9fںA[uX(Nit(}l v@,2hpg(⎖=h_Vɶ:y[_>B11]ތcRKZgxr(*XB4"7 JK4,5CHG`OB.3BXгK2$<}gySEW S P*Y!*_NxqcA9gH uw&F78lT4H =Jj$-! ~@]ϒ)\}@`1PTBV<]-$_"g#(KP~z6BIt:U0R>0e'BsuD`ۃy^Z7G^~]/0q1Dz^GQDL"uGb?4]bV`xl;x{ (`Hjt3a2MEN%߫TESG%N #4M1j!"p\6i$rR[u}z: 8u{UIӡ7O -ajZZnIN!®Ѩ_Xj㦱! ɂ д$YEAQcDH;Q!NQ(k=Lwkt|MΕkSu¡ Ǖ'8Zq!9vjXm -tMY$yƩ]EIٽOU '&Uʄtig^LpLL=KoEYKfVmg3"-?-5<8]lD64iqlrf쌾 ^jx/dвN5>-u5^H-JV3[ihO^u,Nb|).x9Az, ).i|Z&_j؜;fԧs>3Yt]0m -{>w>P|֋F< -jѽ̸-珤YcٓPF+W!BjASDA^8n&iG fT-8 yyxv k !ur@4; 3QLeƞ(Lg׊!{-gP1Z j^?Vg<y+` T5)#L`\p@GjHAq -z//s!#̶}7s%n9Zvo7T\ -ְTU`QPEJ,o@{UpoH NULZ2㗎ʦEÙƸW @ .aFE0v3&bAfB ͻބ F Ћ$-DKxwqJ#0t* \+0:ylK -heF.M1WeDM -k:=v/6 =<-̡Qd C{KOoAxJ4x7k -;S>RJ2c0 vZ'J\Q(e{|!cF3k}Oӌgi ]fɍH -O þ\l? OG(XV;zNb8_,bd۰eOň~E@^lSt -$B^`r=qoƾՙC1Y,@4C}mZ޵~жTJYP>h zgSq.o U ALw:}^*gE [`2tdx빸RNsy.o.DFtf >!ż)Ze -b#dI '<5.B`Fl=)yoZA,*]a kFXHx~mݒ&{8C)7M1 \.!\;~J|۲1(@sewIPo๾9/;]f69si/ %HRYO6\ҴTx+DcV ,ӦϗjWBs"(:=!:@pTb[-=;2dA),R5>7 - mbS5׸qi=5Xm>Ѻb,G`>G -_CDoِ\p7w%BP3 +hj[.=?p-uQMsAd"xȿ9kJt=h'q"hLPD cBLz+-1/1DC}$=J;@r 38Y4|l4͹)%^k*p9K|.ѝ&KsxD-Rlm!l[z -5M̑>L2b\ q/fU]>E#XÜ kn}&QrD蟯%0@vL,zχ'@=: *C OŒW/똳Kq \~W6su>ܮnt -{t҃Ҩ2#w,:PljG6KT7.h,,5/|k~̣(+(b 4yOt`ulzܻ9r>"ԫ`jzZ>@ԁotvy׽Â.+S7~a^}ubZhUJIK3+YM@ -:YFU f|гPfʊ{C{>iW: `ֲ;p1?q~ )c֙Y µnK]R"fF|_ˉzn $3=d%js ^$>/AURϊN=u@OFqIi3 VE<"YIG }ﰔΟk7)5ܿӊMjyc^Z:qLX} %\BNqNFxx@{ =g gZXYA=Kq)9NNz}9pAS'.V瘖}/*#Wcp؃C&rPO5=[>o;ɞO!ߙmI>< ڊy)ܚP 5Fœ:*32ނF --kx 7p9jv"%+w>|Jsmaf)sC^ϳ[SX@acdprf]ͳƒ^$UíG8%Ȓ -J$0c9-=,uOHqAFp.'rbsRoT8v:bk_oͷJg*SzOBy>o/+̬-lo -6&)LSt_Y:3 -PSߚ.Bs8vc,?{4\D( b@`4`oxApey+՘Sjئt 䝆(͏߿_|g~<)(Bt֫!h0Dl`1e V o"70u8J+8J{J.]- -T'=D/ GJ{m 4=WǠp"0R)_>kM9V!\mN zYx)GLdXI6a[CeaXUz΋.;GWYRD0lsX 2xmtlN:Xz n] Ԃ=9m ߐxŀUL0k4gQ583z8{c܃rP}[`6 Rd#ftL"̙bp9:9C+ځraUncx6;RBq, kN -n^Q)&x -a|$[aWpo߾Oo|X -˗Oro/>7_ h?ʟ_?LT/6"_t>ھGO߿~}ûW_՗o~xͷxI5abzIgl2}2#0"L\x@s~.0­"c &ʄ8A@PxB+~KϵQ::Vh3iC0MW${1s1uXkYE|gMڙ<ܘGF8!#p{k[T~dn -KQ$ /s8"SxQ[rMQiP~nv% pD-ϼƇG{!ӤrI:D - &=IVF]fvcl y.#%&?K%b=}`ł0IXԗuGr+L17`a ׬dh8AF-l[u",8ih4bL}4Â.$:)l A>B ؖ8d&*X.od<FC;Hۄ?1>dl -V',B1ԕ3RT.)y]FLX _]쇫ⶋ;C,gѕx;6 }^`jl 4P@aٍЀ&˼bp" -LPlfyDYZu@A&u,bWvaPQAY]SJ {L$ -O+4%KwE/uԫ}6(Y0mҊe^2ZNF- 1UzjӭkE|I:)jeKh|L|F¡ F4i]pwPZu5:!g#jN?V1L h!bSJ*"*Vb &XMzFEC)4 -*B"bjz)缎QRG2uo() UGDQ$"5MD%W.dɰGڜasJmEZ1AsY m Jln9"t:fHwz4%̻Լ2txixniZ)$0a `iP3Vik-vmz5AD8r ?ez!h˭CwB4wBqDp7>Rm:5zhm7&=ilx?aPv -5&TLM9 WX̍ݟj+> /`7C &08j[#rX'DpU~ύH&n%9r@u|#dE3Dc<ҝ喅!3"l&z-:%)èMsnq?2'|M]gxq%imZ' `qr0]; NjFD1/%}N\N%x I66_$Yƙr!HWOLn{A2p@Wœb,rk6=tёo4Tg4B3 -b}"ί^oN D -8+]œRJMJ"]>ƊQUN7 u:$Nk_ )* >b'XkZ-VRrԻ:)ڧr #F{S!5Hϱ@;T7epe'ͼ2% -5UZWySo F2!Cw S80pl.d!t)b1Q,8Fe#&pW)*"et 6_' &:t SSdDjqT|b>N -zޑ]q,AQX(\v8+ Ғ ByT%|tݝ77W B9L>[T+2aZ-s%Z`lO<~36 -? #7  97";>eЍŏ؈rh'LFWdɯJ2p(|"[K]pmRZPhѺ>8o}Mk XNK=5SDpATyj=UNj!a0ׄ&M8ل?lο9|+@An-=<˫pkC֤nvf^ ,d=CfSZdeDH[qaPtJ]&w_ڱofYӢXCȊ^)k]ѬiV(xZTAvP_6wxSMh&4NN|ZG鳸5+~Oek4|7^C)^2T0?I{hi<WuVstx*wء -x} Xvtp|Ac^&S/ BQZ`Wd%]mAr_7:wUU\6Fʞ=bW lD=/2P ȋQv;NmkWZals vetzdRl.0z  ]6<(RjqZP`oБ/yOP'P\8.alX%.=1x͚9KqT`āx^F_<]m oqKFjj3gV0xP,c89cxZ"ڂ1H0l-xHL_0Ot=02Xa? n#WAW=R189x:qt{anAA+oDNnχ4nx4?q7ú /b11*ΠDr93*hnjΌ=ȕy"?; -endstream endobj 40 0 obj <>stream -HleM s$a'$KݗAH/ x{r}̬f朮\._cG_W3s|gF1~?66-{+WlmWv\uYآgխ݇-sqY5sRžW1hnͮ4/)D{DpTkۜE -s6|cu" s{!"- +ع@>̙ -od-ڶj!"vj6xV4w7mܮNvLuj{n=A, 3f$`BZsG[VW!rPBaB8wtB9AJp($sҴB觻Ah-aVN=<[oGmtndaaz!_ WwKjȣuQޖ̘m4slk5ߺrfQ'R(Dܙsl|H P%r,8+8d4~O\!#EtcH(QH,!rRjа2<,I|xqqh*Zw&frT摬u뵾t1iC 5ی]rF(M"fx!mv_Ӆ f&ϨWl@Am䳉b+uyp+kPbUU.Ԛ7(}U(-ot^6UDopŗ> AEAm-2YiJ%7l6M \Tj7;$a"j6{6F(C}퉠-w`~FOWA@_h J7=Ci -']_3uGԷbX8^Y ojabu!L͑;%mK: ##!k#W!!قAMz]ڵ-t -x\wUqbNʝ~6.f֩!{4G=`a}Jݹ'Ewl |"N"@vS}5>yhύD?l2 =x"Z诰%+ Ip -=+, :/cH(f<2F^~i6%>؉AewpSR(') -5Yd,|HH!*uRn"5Z!,#X jKՁDpseɎW!mEͻh \w<'E%x*,KmxǠ67!\O\p8*_ae&R_?CTxQcDĈF. d=7Ԇwav1gK~mxK>oM %'XS 'GMU'w(\2m~ipj,/If+v\yg[/R݋ R+ y \TOKM@%*BNGa)R͟Q*L)}sT&utBӦA(fw e>2Փ'Єz}ۢͺW8!9tXWbk,@!YV*F.YS>i7Y7%c6 V͗ (!bpғtFb6ga9`(R0y?^6X9K :x窨ƪ˵YhcQu0$+7؍PDct .,L3"fT2pq$aϔYSNnFP} -%k} z*bC9uq>W)k6H5-,Y'+$I0te^0čK{Q}AHΠv4&5Lkʭ"i#\zE7UWDy\Dګo+ IӉr+Ɛ;DNB&="DA=P Qp#NO/m.U0.!tަŕS/'܁:Xº=oẻ<ɼb,:rݮdMȉb܀ډ֑4 GmQ5\(߱%6mB#1ۏ81)L~6I3:I 5+>|HflŔٟ*Q \qYaL xC' tv6"L\$oRb B0r!cF/P$_emcoǿӿծj{srӶ\"o%Ä?ob{0L=vW uH&. -1RCy#p!]fy_*2  H^B4ЩHDUӴE)Brf ]1fb ajBєMD]6Qobr-`}ä@Bj'tf-W- r1x f'`XRT$0K\7?L&+%IkvZ=;jj3 -vs^(h@4Z͈t}cPV"BҒ=>H _􏿏i8J8džB_N/`iK ǫź;2Z@( F.BwEoMz'k8 -V N/ SvoF*Qkj1=1e\~o `hQGM\(bc=Jbӛ`|L[,>@VSSU*Ȑ[,RZ{O2 4{.ڒ!*+dB1xqN&CC(|0f:7+IY]0',DЀiQw\  DQˎd+). ݚ 2BC=}̋Bya0 1"|sVFЛ6,?'8'\[c;rhuEztrLk oظ1B2iX4a40_1}f[6I2hDs h2E:XjuR!z>2uu~|󧯟/?o1J?~>g>"o˟^f'ǂUДh(5LNnt }ņSUhUf Hx |]:dvNzLt465:XCDdVG%yJKL[!\CICʞaK,az0f*eLG:w 8 -Ȼ@%o__O#ٶκ2nW{-n;&*.G.h9q~ 28JaRU`^\AQ*D`Dl -D[x9WFpZ _h P8\7"n3J98̶Ԗ%oz8[$itOc?}B͘/ꦫq$ei[c[~y~Aq8 DkJ9 fD{ LD0(/Xv!`BoMaдc0C D;K:g+~)˩M34rhf049Ŷcm %O`orX-YaɱD:Cƶ")k4.n:6kw:yT0|}^zu;kA@KJ?Fj bv@"γq5Z=VY%`,Ր'bRh ɳqNRbspAXζ>[,!5^9^Ulp$&JҢ:Y:4E4}!qKp<^Co&I}|5גr4Y6NJD8{(?L+ Ȓ E@Ɂ"gCioqN{5 W7@_ YͶ÷#,C*53!'Jt;\k]RۑTxQN*1 Op#4NW\xslxY+!١2|>B<5Mybvڰee<:[nvweZ՚Jx ȶ$pCdO14uI7`qղ{mpd P"8>"k1 $;3&<ǥ>( ΄O#Tq,Q>(z/v >[ӓp-ܑ-vu£<.O;ڑ j2>]uen -F\_„$4 *fr<+É71f]nu918:*RM zHߞ>T2F\>hVvmAt+ Q|{OY)Jw 0ǣI].d1dJnq]B.if6{EH(sv&ARLW_1|P - kw8.Kׇ.¶ zZ+b=b:D Jf*}s* ʙ*#DBM4wa1}rc9kiNT{21Zݧu}{5yL -')cM]Xm*칭sqz+Cu#rوd&Ne=d l7B̧fg;gH1.[dq̷0,msit.Y l|"`1]x(NtS 0]1F-.QgZoSx] 9Lt/A9D F6 3 ^V -`x0掭 p Lo9BF/gB6}gXȵc3rk]fmtf' dR" -EyprmR aLUJ0ZF]$M#0d̻ cNTra =L=4ͨȸ bAZ3f=  ڧ;|ƀo7^;PbCNrhsFmWGx.ɺ`.l;lx-F 9*<]'U -L 3s msBemAT b@o׉uNNO8 zabOG-!Rj½:,>ilyOA~ZO -AԞ,Dtf/ WxsbKa㸫ó.kӆ$NwP@G,4RK{+ XTh0\L8ƻ*HvD`6.9` CL0"ESYTA7Òcxw ",c`97l4Qzp˃9(m~|I ,^#áBZq xiƧ>jXj,ءwX)S(u>f]ͣDcJ։ur S msεP::CG J"ǶOSѭ[#J̝3\ܾ=ewХ1(<@gJ77º톖UnneW@eh,'Zlq`ݯ해_MsPlۓx-yLQϑ&怃=Y 0)"?qҟ&W`K(6j4*"eRQsZEӨZ"lC|RrrSB|LZ7WKwI,%*46V^>Nma0rDh p&1l87'zuGSQ( K!3cAaғQ93x̼'σw}ӂ6˰pM}kFZq J1VKnw@E;VyҞ04 [3.\jr`ujU먐~5 )V|# - -.O曜Y{$Xa+6/O1v-B8q PxN? 7G'1"Ǣ;IT >qt|mX-UJ'jH]crU)QINց-y5`I;)A3ƥL+F^cGG}l+ hr"2gdn6B'4PuZȑb[jx[1:¢>DV*kOv@`HCF54&"z=b.Y.CD8.L_Ud~X[O:T{j$xI. -uW'tvޗ}2փ`SUff,0쨚Z?z 3tP+Td; 60&؊seTIc f4=tuU_zEz\G4{L$ v 0dn etazdG4n<2J V.4$i*SetrX֌ߺ4<%#fDTsla8ZR;Y۝;>.ظ Rgz꿬Kk&ʷM"׭^fHfHl0yNuK& cvb7=cKGƤAKm+ppfrC(CK8t֧ 0[/&Y;㝲Q`U#Ӵ/Te.O>C!g8#q<61O0τ$< l]A7Oi* M S"bڐ> HQ?G(7*YxVڞS3{NҰ 8]pY`3W;, ӅQ -%kB}7Dž` /KգCq5}F>=hlRܩ]\ܶ@y;ŒO>5;tVT|'pxyIA8ںLyjP-M!z1[_y֭̽D' mLX'%WvmvXQ5ȑ2ǂл nW3Z3 & \iS[S瑱Rᖍس> p(l*Frq ;5rjĈf(i&N44 -lb^=%1> WqOLɏ%U0t!CD,թ/@aë]&قhm"^mIleo -K:8X Qd1Iv \&uB0D.\UE91ϰ3:< E-Z6s|E8"?bzăWDMu*#޹:OI[Si -yG5|bx5ɚlQYFZz$xw -"X50`7vXn5TE#uAd!*7fG̀/}_|ͻ?}^ W-g+wϿ֏߿/o,ī5{(tb5M n nQƊ#M1̺J8<&Jxq>u34^rYH*5I}ES `Bfu, K}V`d)>¨=!P f9k(^)hNX g"6s)h fB&Θ+BKczx)lk ,^Nbw1Q5PDFzgW}sM=`Ls\ o9Pե b&]p0n9;v(k)B∥H8T< rcU*_ݯR*(4?(;ӆj"qc7/i327 1x]r) ,3W8Zayk.ۂ`  -ҭj7ǑCH7rt^C:ZTq :ٗaI&i_!R(_sP:E^Q3/WMeJHk}^PZf͛6)(=c]ه%?}#MhufS6׈܎7wN+.d9=,e\럐sTRz`G|'Z&Bc<ҲXYz[y9ŔB"pESt WO< qD`1cQ+s?C^5v+[DE< dk٬lAZ3a"`hH B?=YKGLXK"gR+ V>M'9Dps!-Y˕=μVzr -E©Ǎ{ïDTI@z?u} a.MȕX>!rNuֶEyOƐwQߚWmz~ c Ҡשq/ιjNSDJEQe/6DL -=3ֻ:Z`ԂTqc{6f!.۵KnSw׎.r.GwQmqb;y6Ϸy"ROVq٣s*mhV*J7-yD *,/DT3،DA@\ Xo 2ITDob*ISVY\Vةl -#vȶ:cp%2IXSw,Nc+t 7oa&'2S79QEyl1z'\*WLI}6g/B'޲&M"YhzfEl3Qe㵂'&ߴE_/K2ɭɡV#:E3=R r9IhmP-\g;h8gu"MrK ?*j"Y`㴐hT1S5CC9YTI5L=wҘS=~,(/8l:ʫ"]Wu1s7$]$[2ؓuyVlwkQ1,cް^ۈ@`a)<4V̀EHy7 - Fw/4qŧ_ZiXyp񁆌<rpn7 Vht=_Y~g NZ*Y3{y՜=5m~e(M#i t`DX5"$r@de^D ];Jf/3j6{%l{+WD-򪞒Ye@bOw;?ݶŎ( EK)( /;Hrֿ`? A=:]^0b -i]<(|X:ڐ -oæqmF,}y8;^p8?C\86AEްHiP 2'I[싻u%Q -˙L -,q~)`v?~)XW]>`)PNG|QzRaa1 =B{ya?C!m 6Pe>q`"uS,c-԰q)-C)-F*L#|W|_Cz 3,+g EL(UW B`<w>U. [ Ff|ν͒Sk55BU[,OS08NJ3uδeHXqAB -{#Uu2g'ҙ,D.ѣw߶L -:14 }+wcv%"HNĠn"j5..BaZ߁wP p#v<1zMTQKS^l3 kB+.cc8"? ǀ٠C@ Hw¨fK3.""W!RhfåA`P;:0;aR ?5@ ~p ܈pS dd: \ۊl'Ƅь;o(Syw<< ~2=QӿEJ>X000mSY糁u8o[ht -"ڈs?' 4M5Hyw8̋&&T^D)HHq28A/t"ϳ1,̊FOբt9tAT҈{#xd^0蓆 -[919[_y#f"C(+wj[}%^`im|SNHPQncF/iڰzpANك5_ /PbO22s0Na v!s'ƦmZuP&@/vwQّat{1f`t,$Q&(ČgE0tJ٢P/gy:AR. -CCU\'F`]o J3}7|ӟ1FcUTd&B̯rFmȦ&%oVa8J{  Ԉ]#g%P0:WN'z7~lj.D0,zx閤ul/@ 4'?i?'nވiZ2>x{,WC3׮M+zZ)(x6/gPNИfi؞ᓪD,ɗQr&r!{# ~jЍF]?T9:v>,KNWx|(Ôg"ZWZUrG9~tU|mBm *-?4ȾNktDElȼ0^$SB \Ј+ԤȴG5@OU9{plI\̥5fk knnޥ"F/oDcqM<1)c.Fq:aegalW.Eoː.1x%_O<<[ o$@0U<Dl4?x5c;cfޯUDLg^|~}Gqkf?8/SDF@ @xR |!.\Ul?]?5zB!="iר=7VsY19[ qiz=ˁ"a@ݽ!qpRE)h(]Qp#<^I-Br -!d?9GWL4y8"q;\]>z-P̻!(*:vpu<`0}Ec!Ȥ &aC2a&Ĥ]1+d/t`{:V`.eD0'ىbჶք ai)഍]8#GRѿRK{파Wl@A#̪ed-^s΍jSUbŌvg~{%+0ʦG#$P I a@x UtZ/e98,g_´Lj|:ށ("c|7Ɲ=. -r[p$ v&j:۳@9U~UIvm"7 &Ӆ+z 1"Ds n%ϥy͂l/S˜h&\f})E,E-tJۡ'dGYB.SwXl*rؗ 6A)X^u:زwCPb'>"3xnq_w^[]v`n Z1gϱFF0O@TڄzIG+lOU ?&6_g#sEYo|o=t.KY7/ I]J|%ggĹa XU&e]:LWQxsa]sJwUgyX6kWZzlf&60'z|F?+\E^L։Qrz#"NO aaI_FrBiD ';ijGc <DE,}.|p&[P3".$ -S]H)M\ϬEWDQq (+ǵ>caEnA#=Wj c.ian*au%M%:rl^̦=XrfPADZAf7ʺ"lal<Z"ׯb E%C FI2M0,Qr3l] N@/|ޥw!X7r6!sv~y-jgM Dk,O @Vc j~N}eIˀG5zG/ަ)svKщI'8v> hGV:g`p0|ojPC/WaQzx3\V\ڢSaѵ(8`؇TEKd}qUD< B:@H!#kά5S(-~nO;f@9xc]Io9pO/"!vs-n"Щ4 Rj(E*ƁW_3DQL8R {aQ1ӶlkL]qA QYTs|d|a!؎ LF .OQDȖgXq0ʡEMcW=VPߡ@ 0VNN'p@# "MEvҦJgȧ U*4 kk#,Uao]ۅwLUK"nZ5ICqܪ)NGօ2I D+""3y1 R!!U *Pk BKkHfa7ʚ. Z-%TC>u -w4 qnX>=i/ZbA9b ܈.-(* 2b8PC=,Ze]iY0_ xO5IJS:Ϭ G2@ o;I_زak눂8h@y+0:dԿ9fb/.z׈}u @xZ 2M6 z -BSuj("'q-$,0cmcE?H&'ٮJ UA:VM.=_!; -s S;2lS%^=dӖȔ/R5Cxm͌m޹S -wh)b9, ٺ¸š#J-1$ yћַbREy8ٌW]8ohzi=>5Ax[n5*k ʕN'$$JxSFT‘}uqxIP*u,zƌ| <Ԉvs E@l`Up&F縲HN C|[AVȥx v -ݤ_t;zo8[)\O9*!\X<5BSk pt38-VQ*MXfIԲ$\DQG8M%L8ѹ?}篧ٯp`O/z/Fӯ'r9!i_^_ÿN^7??w|Oy7>>?|t߾9(\676nN<&bDa捠sOuF[(_lHkbnA?@ 2@D}M] ~$*A6_{];/rA4t}eD&S e*QF V# .DP1u3c XF¯F[DҭJL&¡H062I`5 3od 9~a D} O[bo nZnx&Ҽ208m@ n0I'p[ɁяcDbN=kRUZ(+*jl*Qc(k](BEdUan> CX+@xAG/jo6HDo0iQ`ftc򪙡I' FՋ9\٘LLb&jׄLV4Ʒ N!Z&L[(̾g6X*h?Yr8E+pp&]{9dWY/"?Һ܁w^-썁$Ezg+fd[60ZyR1N.N&I&QlgzLItxH~(hF3RL~0Ѓ LJ"-"[:J -Q6zbRífr= ok MȮIRԓ,^|B| w#wsh.Oy2Yk[݄{:C~H+wd " -fY Pu!3aペ?HT;a%bC3|={u !e"'zq@P)b\*!ϳUSmY#z^|4Q}!Rg|̧Dv.ґKؘ6+|bJ8!dxQmC䃰CFԇBݛxOF)C lfm ;=nJL4 M&mIs!P 1Tq(Rǹ`mzsSSAM#$*hy{(9]E SX`O#G*@@=,cU~& dz•#J3]]CմHV@jZnNt݁ vLmyyO~$N  ᆙxg7S$ Ew+א1T3"BoICV[>r ̕)G@m5Ez" -PA\y)}6j8^B8vEpi?JjM._a0yLgb5Ț: "ֱYC S0ii˯χe{~1E=qǖ*0;d̂wIC 5PZnJ/VcEǞ N9^rZ3F[zpH؅ ^ʩm2Ղu0N( P+=<|Qh6ur,Evh!"Xht/PPJ ܁@س'pgv' -gix~Nw*ϕդva(!*7l]Jw) -tՖ:R/ q3o^[ -DVs` nu;aZ:Q-]:5Y;nF@5l卛_qD;Cwc*pLCiMµWf( - #5uJoƻlM=bjÞ:"cFx1ȍơ u*.wrU!R>4os7nBPE(ar QoCeԪRu#jG϶tUAT`+vtբQhB[R2eB'פ w_M_X3`8^otB7'-;T 8 ˥JFIP妣y/C*ۘZcmqEc0}pAz3uv}q˧-b"/\f_eDpBeVy@h!E8i9 9 -Me-0)r]h&&"h]0Q@@[ -vgۙI-ɶDp([Y5Ri8 NيAjsRHr/A݉@%r P(V=n --w&ovּئW7Fѐh^Lea)E$t( {n~eTJU<ߋ3pLuϳ?!pRwѢo \Zkye8ZVxJp͍i%t#gFaTLlclBb#z{qb,idb2op7,5q` ވjtSf>,spm6m69KP}*&Tů> 7=VNy?CDs@5+BN5@Sv]IPЕ&}X{|X7kz7I#3T oXsƣ깃6HXt/Nu0 y= ^ʠ;{k`#ꅸ-H {||S:Ӕ6ƅq`{OrSUۓTLm2D|M&ڱp-A>[Y}  F_9az`pKDcA]3h8n t҉p fQYxˁpbOF ڀ }c#𚬠M>2 -+Vs+ Kh0Tg"qD`6{d -0e<*$Z&,3("yadE[BӵtM[oyiȽ&&MĪ: `^c&lWe_x/~@ϡ@c,?VӢϑ0=hO[֗u%79.[D<#0чjtl(Y>"p]񈲣??xѯI1!oSoӨ+XWƻ_\Òϳnx AHkYR#`p3a c3-dlB;Ĥ7ۍ8=L ]9fF$NU0cX$M*mZ~b5o]ͅ&za%LoxƼ :6k F)ALOËpO|` -x,D7"CwS, i:}Tth>topCQOބxͭEXK {sj.r'eEfED"h ;F E Tx뉠>T6hTI>0CDB^ux[|%G~jP nVU_ tn強{V )YT!pܴ$ ݔi/s!7>D>'sUUH?'] SyT"SO] Y`IO -Q9I# IA4¥/E TzLߘ cqx:"TqboLL mƢG%<ɧ00_/TO1VuT߱H2:_"]z#q29 s -:i8((dOo%ODN,: V\GB Tyj;vKj*38}n;pĶ($?LbSZiބP|4 1Ma@&Pˎ?TI2cI?`Lhrj y® Ȋ~}>Nef-@DGtJ_`(jpG"@}bk=K?2#X{E؊aJ \Y|0úuө0,#Oڜԋ'^ɭڃBKW>&毦$~h@yDޅO?rP{NT^,΁ohx G:f`/ry6C׼j3/˰s|najE -å1C؊'bhQ@ѣ=*! ͂(<?Є5" ;3ex^;wk Vt{,=GzHt:f>{߶wy?}+px:e`OT1raR.˷wB8O7]`yB/a?wYI6e#Xv3eNAFqKؤ\;e:^SQcFі1 _Pemh]05ena?)*~Eq?FI"a0+S_Aɘ 'dئZVy_5RR@ƠWї7 C{AByr#}'K.Op.b1/F<թsヷf?V7]Oȴ"0%aKSvLWT@'=`:f`4LM s2EwuQZ#m^}l&\JnQbhqj-0 DǛ祇.7VCcVrDɅ<߈K߈|/1o]r(#nHqblpVx4!)~&ظg^ sndJl*Ax>Mpy"ܟo;P'OaHkӷ91326| ꏘ!U7W"0n:OOrCM5UXe# P3ʮ YlE< TbHPWT5=ٲ^MFBdB@ 6p1+:TB&I_Fqs - Y"hFue,&I56إGQOa:BS1>95ьy/__L|w#l9 )RUG0; RDF"vT~TAɮ}AYC쑻h/ ]8!\̨&gǒ wZ e6t)Xa@Fx)~I3,nNFD_7@)`mDMCCK#B3h:E-o UEXk -x 9#J: {* 2U H`xF^p:DDVфEKvFl hx&$|Ba!:2cRq١޳vFEG|NZu:npy3[%=Z|葠7h"yAs1e,ATQY,gfe \x.P X_KMX҅Ƥj\2J=W/^Ք'43]Ey4xb"Y.wGm=R;X5'}V'%qFCF(n]o~_Y?UKn*1kqu 5ڦ?%bkaX4Sh^X>*̩UaBf U:nj;)ٻQ -Hd73Uy\܇RkiX|ѱ6u,]kv9A;Sh\ VT92-BE#L]gX_ ,t9O:XelA^o#i0EzH9h4OF/GAC0Lrb-JvY\R+ZܰD &b`vj{!wM2?0 &G[A2GIH! )3ZOA@on ---*aRL̓X3#x1._¯}O>߾+\~?/_wqD[ɳ (3˘[9l^UӋ-6m?fҦc97%x|Zg<_J|#=ezFDԬ'0wDkyU;5k~&\WH!4}1@Ȝ;#?L !^PW )*+uQikfU)Umx pIZ)H.KkE^KROX -F'LF6camэJ#1$O!pOs  ;( GҫFL#ptk"rZ]M}-ACtY6=xO(܊yξQdzɠ泛$5sm ʃJg/A#P="R1b)`!¤ahrl% S;-Ra&8QYxкWN2cSTt] $CfQ >GVةq굵u4{YDIo*C'rݴ]|vtT-~ήgY!IkY`4cwPx##$o21"4!-IgTASp&!G% ;>>G&k7bE"9~n`Ѱ5ULZsW"1*WOV٨e|e;XȮz鸑r+;p+k XS{(63fTx\P} %

FQ8H)#q`mtJ>>;wO$n0(V*K>BtESY`FlsYxdc硊@xeٳ^dC],sT5Ռ1YF-yØ:,d 1DԛdBȮ}TFNN -; T)ύcNoTNh]P ~K)ĄfW{PۃJ$ҞT3|((zRe]|wn]r㌁W3ޢ'Uԍ vV7y>TEZH)d顲RPq0C`l@q8h&U#yVcҶ9u0i&#~f߂İOMml%'PDuAAlyND-F0V5y -a4x.q ^'0H{߈,ua@wHU);b3&{nd魧B[V>*'b8|.LWl;t) 38X -6nY-;s$|x.u!+ʋһwN3s{>Cn4cT]9 漢E(h -HVxgV\YHc-s-{Jު WfÂ_*[h;tP}v3°\\ˢ;cnͅcؤ1 Dj2VF1D0U Pȍ+)cE0qF9̃IN=ZA|e8V~J#R1$69^}g .;<&Y˷mx`:x9'Bߠm@ږ`Չ\[2]UTarM/'0a-thpkNk#jI΀LMf .j -VXd΃Ã|AtVB7=\jk??F?lRSm·Ғ.l'/1w{b4̖rVԨxf/]ȆV uX&U.9UOUy$'rs, \y><_lD6aٮ,? qw&51c -3\_ ˾ xEE``uJuv}A:P9/Ҕ6{w Pۮ98TQ5zcv6︼٤ӱ+*@>%|S2ȮH@QTB(}Jw~e}NN3}r̽ΑۜjVsBuXݜKR.,IOln -^~*}!}O&m8G;d1eN^7YJ©5̀fxC"yd繢nz|j g3KʵspɆH|T$ cj @:Tpz9r7>(2w1qzm-ʶYe;4 hIń#XT(1\H.V<1w=ueyEţ3r|!Vj[xhUGzAh(Mh434+ÂOX\CN )h_^L,f%^߄.y/BYԥKfV8.T*E/V#@cD=h ϗy}q\03 4oZ믞.S~Tc M/=NJ y)1)B+Xŏ9eקBd%4?b i4,Y) `ba^Nzå\(W +O~hntxs&1dl" -Hmtġ|0ř[`+ĥH]qA?A gھ]v3Ot!XU+ɵ&{^ ȴlgd yA# &.8]ڲWHj'iD$xy{ŐnDƩb"K!CRGI^!>ڌ:#y{ŷg9%9 kd'08+ KWaxէxՔnЂ1+Mt:ֱId +6mJ ⳱sZ^XnҎi's/C _<XTo.W}sW:XL+b*ؾݙq3|+TRBCJP1 ȓ 4R^f#}WŪj?+-]v8fփ13N~C4 Ma))q& -G)jf)c7J<1oihgoPuj|2ǔ2lYiMN+T<"_#T_|o}|ώd *V"baz/G& z*BSTD2R*RKiqeL6wp) ۭq;4M:BHxOS$" -endstream endobj 41 0 obj <>stream -Hl;&A}$;MU売L&@/]v4ӝ]<S϶c3cc=g`w~n[cyֲ̌Ͽt-㏪3rZO9>8Ya"yiY`Nceuq?sb+6g*? b8sۉ>LJci|?`*G{a)یHMf' |sr$]^o768CwU/{zvigί؊, +U"Q -\T<}U?1 *T&4]E u,T0[6l:}Т&am[YtjX2c -5A3+o(aj/i ϲ෶i0c*nlu4|8q}ѩfP4A9LL˱9w{j9@way<ЄJtb䖧F~k>F襋GfYe*u -Jm ṫ:ǡ1~TD5%xR6knNơ-*<"T׆:6izZԲX|1{Glu%\s z2 -* `~Y6ZNLiq\[dc.V%4byD)$! (| ~3mm/[qlU[M̐zTi_G*f95kXed]>'0E-z(!zkO?f9oMџ"t}v/Ŵ ד=5==OM= /LD=(zyh:1,=Mc6zq0qNKf͞naL:;يP#N"i:ܸk#KpRi4^gE(G[F^,iAL*|[D]Kyҹ{MpEQȫǒƸv,( 4lF5PiOUb׀$sy5i؟-/;O!ߋcԏa.ET^3">A yHm /wU9j`MCq>=+1Fi OIDRtlhƱA=?D^S"G)U5ݿgS*egbI-2AE<=W0@:^oW6J #Z išp&XU+L$@^03R5Ț֌nfNP 7+a5 HUX$+N*'I˒XhC _%V0{fĦI#k{21z05j.{Lqf m -o(,ױZ- >(]KL:,B( ݖy g@pDp-FqUuFY-hJ-[+V&x7Q;7C;bj.;G& Lrfo3v\)) #JI0%aBt8 -!X{ၕrJ \ Bʃ2?$}J|f {HcAgE{mDkUX0CВYxH't5CIPO.J-Vqۘ:G<$_7; v43u'; ;iDYj]w-ClWYMӅG(%3;fM_J7XĻntGL -jiKC}V[-;X0%om~-% - 2Uq`4TC.E]zǏrٺ^A-?02R -6NOR!1[W)QI..p %n̼72݃]!*."x̀尳tA\#0蓱Edt":-`,; mF)e6m]3Zfj? թ~7Fi%ҽ_9LElLth1IrI:,s _},] [{|R6VJ/%%DFdf $]JƋ&)`+̾EqQ_daf+؁\.q;P45[anh]rT{WT%,IUfCoН}*eIS J|`)J5Վ{z|R>Kw "Қ!tnJ"XD+ˁ;@3vW有`]t!]R#=TN~[P'~hβA2L8k-%? r:! .lTN9D4H-repS;iR]n `Z3P;ji@44CUhxjA̠c_cLLu`Q#8""bG1zFYi,ywɯ$J`P6X=(F`jrߡ -,@e8]WMɻ08dQR ƋcBB UUj/Qp 6_s/i|ۈhlj94fs+0A.$@+H; D5X(.-fDO v8bsdx{G;|wkxZ΄{_4xxcaRL{vT*B9QK8OT@Pѡ)/DӞ_N ptu %.%8|v>{|F?"*BR4%%rqDY^ŷw+8iðL -W;G>}`HE' `<*X*A؞A6^}pR5^mx[w\Lb5 X"X!q@#cAePq'(AЌxrSfh1Ҷ2g*U}J9+YcDR),)6#j)(H9R5P*i`Qn?l=v0fj1٥BxXSޔ)p5--ZB̀at+$(;G,ȳr"٬sakEsI#vv_J&%:6 *03#S'=iZ :U!)s) SQS 6V3(u; @>5dJes&BwD,HZՠH]o4S4Bd*o ֶYMH4hil{4拒b -K}ʚQbShY,'`H71VGh] ĖG"!bΕ+bw)lĂ+ ru=wڮyg3h#V9`h}OWռ!y^1=@L lo?{YJJ-!I|ի,wf5PZgQd >@o V%ܽ|a:r* `1Ng dwLN -Bᑃ8] em2/3mlk8r w½OT7F {0jR,8v5//}4sØ 1VSCF$;(4<@A^6p 91EO:T񰀩¢/Ddkj87 &AAܹ\L}D4UX1s]y7n6HEJMW'q]s:.۟Gs<֌CcҴePĤGNU{=lu(|M19#@{A)~MiD9=מx,<2ηApX2_/1^>id6scwOnz= g\3@ W=gl)1btv,E)1NSiDd/y"|0U3:a 嚊+";knIW_Ogwű&g -sAgX-9=/*珢PBMw!N^®PyA:MaVɠ^ -W=cѺi> zw֮ᠸ_ Lz sطkg؁bhp g1:&cEG<퉉qjZPPe]NoOW—q"v0S+|$nm (G5vȸqOeAN{%)4|(39x _!IhގZټڥ=4]4DKC(t6< 6  @޺Փu8爏6>š^dC%Ig#W&[En1|yaO̕9m1LhW;E_a3>>2 -]>-Ë=\s];1<`=5eD~:|*EOm좃{ z.Fۗ_@`% XEr* S9MQ[{SƕS b5b F;XEbo4:ԁhai ^i4ce.~F:CgV\5C5qy& -'A.]*F1GG4RTQzjچz0*DUT]+J;G!ۦ?yeM-c|~@OY79rI  H=N!s63SW] 4%>owA젨/M#VWtO<(T~*YFn$@ E籱9q8WGilS1&M1i3sl1BcQG{*aڍKRdd85 %"v'`Ǭ:kޞީ[1?#'C17\AH*,qnf/J{3"t`RK -cА $<,q10t?5\+8. !hp8a ?4!hiQH(8L R41gr`,ϓ=طτsdjĪkPe&a@jADlDii~[{(層|yrC,ry -u8o݁N֟ua@׭AG=U3\jϩ@e^Dr{z&x5*<Rdǰ,\SqvjģDAYo ᳇q"ZFh'P' [bS̀%e)- -lEMrf{#~FDyhcM{I75L( -"c1+FKׁ2TgGРCGҖW -qgNF@ A;bB[N*NFm W׌r%#"ZEM]v؁q_\^A,hP_Im0=osB-@ZX|6u;qw 5zOU!e;G= JK!:KzlaR~f_ Ek_*``yѐE+C% i2sf2/XSIx5,F9W2CDs1 ݠ+V7a~X+\yDଗnh"0gwi49FıkED<>L.B V$b&=zI ->*:(թnE5e7RMf?1簪_:X8,C.-nI~N)"3i ϙS/B}qM=\™3dg -aβ<댸_[s6|,a )Tl~Yg#iGO"Jow)0x¦ԮυnzW (LS};gWq㜔M(-?p!aD] T̓z."#[.am>;w6("10ݼ8A#*(!#qZdZ WX%KԅvoEXI@2.b^D1/<M^5Ķg+dPr`rEp!-N*b] -g 6IM[<3G-jhjj{ 8 dgrcSNTWJq(n 蠅A!FI kabqGъ -XAba i&X(<>Dh_[T~eEJ,܋Dd: 4`(_aiV4BI iveʘƃӇ+<G:*h gp2*B(eM$n=xb'`p!yeQ!t [ 4ȆEEO`oԖz8R qfȳ㈘!8U"PvCbZḬP)܊1UǬ[q80 RW|! V x*auD-A9}PpjJ:s@p0A J c_W&qgWvXy9(MQֱ̼{/2{K 4^z €g$#WBik x؉Tɜ"4,-)8Nd :[͑:j;[!QW9Ed0tF?[) 煠Q߹I=&'BĘT.F@ΙfZ*%ݶ26?dji- azoF^k6~һese,D4G]aG[@0h=U#0+ :U~̹Gql.@ɹ]?Qw0މr)?p+a޿/Ν^gr~rNM cڟ>aHdꨓ*ToG|>Hמ0'RG$zRl.:!62&Ow9QnXU4LgVÎW~!J%0é)wbt@ ]B6թʝY/N nD(9㟀!˒0T+ -^`::Yǹ 'SSD?||>R +<[K"ډX^b~"ko BTʚjNpLMNe89,'"LC_9 ;ч2*N~UW/`"o%8v^++Ɵ -旅9(nF rayF8զG aVƬ 9[2шX'Mɘjk!]3őa .Ux(8+ou4?D`z> y((!;YX$aӗeL|AHE9LVk %<}w+軣?;&xpJ ^ S[6~T0َއ͙:-fKSԄh^v=vT+2 mJ PQEE4S R=ϳ3eZ۟ki8`4۸8+tr5_!?8NSi 5fJTr8%Vȴ= FA i/DJ*G(}|j容Y;CML1LG9.Ke\ qIGb {4-[Gg7}e<,dVM>{b"CvD/jOL^nP7S`EBp0+It0֑&җ#5 -.NZ՘6q>ɧ1#!˃SKvܖY7E3jbi5KK:`^@F< -3 FTPjDAA+")*Ψq noh't]qY\)pa}=_T;4/w獵 yb"PDR\.2Lqh6Z P8ZΪgLC^iHw!ri#5ra\ַ3GQG f髋eTiz4Ht*HԘ$:N YUzBV;bS*R6{D,Ķng=mȱ@]q=S>NԁÆ*PD |RYy.ߔ%е&EB´baϮܪW†YEf -H!U;`fRc(>n i\KҩJv*,(<0︬iO-4Ƈ\ -6մphLA@!n( > (c::{Q+ie4Ү9=/>^4kKT>x]j48Zr9F@yz - |ҤVȩ&CwHnܘK{ȫC4%K] J?z7>mr/UyllO޽/_}oߘ S~?N6Oۋ֯?/*=k[@W(o|~0j?k $FԿAv4Ę1Ol :SU7?rҮIbW' n\[I DS'Dꇖwm|%#eer@v!khU Amyz#Y1HoKAǴӣ`oC))ɦܑRJS5t9cx<*Rƫ/VUХ45 YXQx((>4Z3w#@KuV1Wm7v珵N-k{a\Ȯ -(.[nJVEyU p+<>VeZQb -/U<[k8 U´YkW'Ew;j)rTDDJލZ-TZ -c\ǧ#G;ԏ)QR .SQ[ؗtroKgy'_m3S5hd#A!i797㫡M0͛M?S~=o(gPZ>(YV:c 2 ?#b9@ AGS?"V 9lrł#l>{W1":~̅Ν|@4)] P- -{}^O ߽qʷ1]S']U?dL),ޞLxEh(*mtR1b*e=#S89.2;aEMi'Kܝ4.r Y04E-Geu2 - -1CQ ,bgJ/9s-t2 hDdq3t%qyP `٧ ӆm:ӥ6f-2lve&ͨ^+Ir'.^ m`56x,%(Id=̧9(2/8:u"D3 -.F͂SB4&!>เTax𶏣(٬ҫzU{+G*ozDb7+vxp4o,uζMGYѝ 5ʕY(&(5y9)^(w77 `N@Wr8Pã˥Ի^(u$)[=(߃D -=T7׋F BtVuzjc7u6iP Y~ tTĎT-b@-K$ZQ3p5nnI8!` -7.8xa"w'Q2[vsK}%miz]g -4ǫو-5faQ\Gٻt4s cL~F$/-OU.-XQi|EA+1O),Х›פ"BhT`0k.ӂDHFT۱c rA h"(}E2o89pƾRUAkb4k\0>HGUIgv%PTN@7w[6NNB#8&=&wo ~Bstc{L<4DiB*> :uݲLy_hN( -|&`/wF"}(旯 GE#>`n+$+oK!2W]C$~iz"k*-,߉(l#, -s;y;55%#enB.W KOTIߩg0]kr4S DrpItKlqN{IA̩g.tL1#w -(IKb5ty,pʩA;ΨbtrDť;p{ͅ-Yo;cR;(BÈB#_DLp=ݯzax/c*yO3kAH^ø;~oѭ۵J.(I]*+:# jp,c -f-c{{KbtՖaN'bO^PG `D麂HҒzv`3}A5«:^1rU#bVƴqgmy#o_ޣ3 ghmER Ga|AQG6 -}hŕF -/|V,.Y{nxD$m amHω:X;&Wzpk|C|USLJ!E{I9Ud=pu4%b0 jepWzXUw|67/RBMaXTyXcc͎ٱ3}:?Rj5>^jԏAȰF 9~7~E F]X+"@Hr$$2+\ -%擻R2`ŀ_4^(x 0lc·HtM?gdPI1@oQC*݌f6Ʌxa?+_ޮK#K"-U׼]VhYn6ކ8qW<}ŨDQk г53S>~t3(Qu%F:k|q =3iq rfRs Ȋ?ղ%J ŝsرR}? ȻCom;٩e* NWo>(葮>PT *ܵK䁭|Ҥ43 t !]Zp28f |Xc4aUF/aTAmz=4dR%Y -uw]T=9$U`;J6eaDh[$l%<  eD7=V_7ֽx\ -' -ӱ0#R G!ݢE7q3DWE,e+W+A3wl?r)x>j=rm }GQwj%  wG'D3ȚzBcщU2)ieYz:D״ -Hc"DIO(47[)߄w}}F\UCh;T㖈 hUȕ'~`HJ}me(N5&w)'`w5e VIa,ȇ>ą8^8sC+nziB;-`MJ"&(e'bҕa|{!VygdOHųtH-v 1e}^R$؊RŴʙ&A+ sE"[ZHeEDUٲ^|Kq_/"Ҫ4Q_9IaŜ@D),0#%?~s[޹ˀt AsGxuUaUj pNȑ@H0,@$,)oZԭ/P"'giv5A 0 p7:~nR>CeJ`uE$GF9 XTw@IA@˙G 8"nAs#@6'-`[ pAzHU_wR@HБ qYj/ܽ S~l"fRV'9L˝ Gw&ϣX -c288JJD򝈵c4e}5ETm[-'!lUBl\oDIOy<% sD_} - ܥdL4`8u~YxAN_pCY!Gj*>Y4 UjC`,;5:NA/O ՠ믻Z - -#- H.:8듿vGޮ홹# -Y"솩bT*RF #FP<|,AQ^+_V2sR"y5Y|9[L-OF<'%!};VF$ӊ٘ -$h:,K\m!!0x.t8ׅW0uhw`YQ r (A!je|/@$lStX,"kP3fH ,h>:[v`&-X$>5 ]K߇- "HU| E蘢9[1bAwD1Ə(k2Ƴ ]P]#f (YT_klb.̝Nz-gXX:yHxD?g9ha?K<>ZG>J%Zϱtuu6[6. uiyYntm}]0`n$l - -v@cD] \!wx*_[n*'wcURv yx<O| :بqO䂱::65^`ϢZxp EyD9 7I M;JfL[(j_bȕhp+͓ ^QR o@m&MŇ  -v|.fX.4FˑX.ˢ"xEG怜cY16&,5Բż LB9p%SFD"84!&bc[aZ9,|bx40Up`韷oǏ`8åY:=,9A -1CBݠ>k bHxL+*Cg3K!0cU?K˟;6b'"BfoODF%bsQ2(x?A¤_u6c<J<5}<rKBūHRy$ 蒀=,|]'[,ODž! $h6OcVFG77a" 5%TiBs[B׼/4@ɯ>+Ƽ(8ب61[Ac2[A-^]>+t5}2g#VnGaU&h 6N'p|H:z)J{KU -ڔ Fy7ec C JՀ흁) תּ8cyѶĊ3X2П79jD&(!V*@ @c:c -bjrfCn/1mz0*G^ UFC1J Z`9dAZħ# IB䘭8 n^qd TQj7A~?AH!.Sftde"NfwQ#8-CB&uZ4rE2]Q;\4fUx5K6Wծt JJ (^Vy4q{) -ҖKgr۬B1ta_ /\,`5P{koCJXK G|)ǔLW5:;tJkY=}e!;P/V0)lGA4H~Kn^D_f~z45eЮ4 @% -5,%Tjt`p܇2WE]("Q 7IO#I *]uif!@+I8(^R<ʡ\0WW8BC3M63gOP{& PQ~,w5x'0\^N'@y6=HdBzHx̡DOOge%y --BwZ JP0$Y?sAW֯1:M{VU[MBR$0UT5G]qU&Eֻ\:XI>j=%r)jnltoj]1` PM@6Ey5>=8x /hx[XaLnhBYoA1*j@-:atC& *K: v!Cw:qǷ1MD?P~縁Sf/Jgȇqh4P x$r{ -/ -#i?Ӳ$K2*)dx(\Ck("]%n\uj -:=s'9p*"0Q.i3 vNy-A8p}V& -JܮbtS<͞RP-6,El%R}U0 @} ~o- -u (k -GH*WH2dPё \py(U)9A$^2jׅ0x,L'+Z!Ď@-CBdP_' 7 ,P.SɕA&K*^Jo'%oNm0T J]p˲;#xo;PTŨpTQ:`"Q9#P 4~wSΈq)4vae f 2掏lcr܀@#ѮA -9[ʪUqkXes0x[Gq3"Qy|Zj(V!T6mUnZ:AZ|p)iCy i6w1j뤰E@gn^JG1,|0mX-6ZEpioPVDnb 3t `p,@? G]W5>H_nAc<u#S{ -csɇ[C R -'EO$Ö0C h߬K$GFJ/m ,X d!ے5^sNDս#%M_uUtfdd0 PhebF97S @@|8]-Eѓ@`.3hr(t-&k!P]/Js{r@+hn3& 1K+"(HfʜzԲ9Z#1FE`*ia_#*\JlxnLEWG.wm5:CLn?#CC$گ[[9ꁃORUjK.Ǽ$ȋ]r}]{cYmCB^PAP]Fp^s9<=V8i2 z,`+|n J%<[fD" -F\1Jȟ!𣴁8V"("~X; c#h:ˈkʺ9jfHA黰ER[l9Zn:6 KoʅkK< @{J궑 "a A9Ԩſ8g ar.(p2Q'>xvuji{]:TdX|KXRZle !%_ȸ()U=0qPt.3;d;J/[| -aŭwB':}BxJ)"E^P?dri8N gX]kN !9'ƸtGt q=VQ^f~jd"wuba -A<-$NA#nXQ)!jå 54PaY'[y*i]D"hxm 80aΙi57&U'FiţeНW91(cH Ԉ@-#9=Ť`âQE;1]XJϰQ#6r'Nl,3c048;Bo#բHN_u^(Ca yn6"\U=(n ǿĠ"(A[ U!DP~TY#lY7 P1}/kK*];3RHDV r9yϩTl&BU<Fd͔ 恥a -D[0c" -adt܂2;Yc}o_XÈ3#9B/oGkkSDNEм`~188[P%ɯ1bN9/E=_B"JL.k(EQIll:c #RtUN{$>3;y9@’M Z:D E -2Qv R]7&: G%U҃yj=v䆗^~ݷ>j>p}/>믾wj?_}KVV z!]<s]Iؙ_EDd E/i 9>!t {h 1UgoSI:8 Գgg3ԇ]  SVU1JDT"UNs]cPD  x߉ +&2@`J Y .a`amFv2@@o$m2-hNrĹJ'N{DGDTzjpmhz"P+ܟoH*N6f:In(.ȁ4; h,L#4O;G! y}Dផ/v{&!y Sz#!Bӫ"Ǝlc҃d.Icqrnۆo|dWvxMY8$f9Cb^vͯ#rAUosFcF/th(#k}07}c`[9^&B {|qRV.}Ήx~bCn*eaXBѺtjR![Yc9E4ߵ^C@:D04PFD^ݷ*{ FdU5 f# Ո5DLh{NÙ&uk'X(͚#chbwϞZ3򈠃^QU/P-A W?l{[0/G;Fy -RoFaƾ \Э_wW:mT*BHWO5AarG:sJCOA)<xCޕ<У`gf HN`gB w!m;3#撶M*V3H2+2@Նɢ{t" -q@d@*5;NHG49 1ԃO)0`RҚut|DWdg'` <(~[HCX)+` -}cHV\NCRǔi}+R it5)A12x]M[쿐hE< Tr KŕYT(8dlw@d8hDa)}lr -z@M3o?/>U[Wí Zߢ>)`^Y„UѳdH0cqv@nۧE;G-ckìOy_4mh;1wQ6, 48)ȎÅYC$wX͊XCG(_ ưSb&/udn*LL[$#aH6g vlDF `WZB08 Y@H82eQʨ/^MGfuC[>k)A)6~`[P}RH\#JhQks>Ml/rFwQ,[9n֑H8wxRTQyu`מ Ye5e @m!钺ԟCi4_25\nOQ}33`tBXSDxwu` ]G '!J7z\DAf^Y?etw`ߙg5EJ&ЏPTH3xxX^en|FDi2yJC]5Ze:CVqo:;eCep2`K`b& ]ZĆ6/?-BU,)w,>w *}nJMY6"}"0>7UU.:g05EQxqAe6OE8V[H3~IֿdZxk ,A$ U-xxt]H?4v 1IlA]1ٟמ%!Zǃq(d# Aϣas P^ DzRD_x(#TaVCz?筑y@͉u ඗#v!.j  -m -^D9ϲЉi<./5wIϐf -!mwS -raAVl#D-A IϰweQ@/gN<`3 L⡹yKKʃy'$Y늊<8RK\$F)-g2NK-E措c?2E>QdsnAy.Vih|Xl2M~α$5N,o\q'uC Hr@U&z}GaJP;,}'yhp=Q$_EQ+ITc-jjgl2ۈ(lgk35ZE`Xhz(]&Ɏ0OZ~_:WĘ*v`ƕ驫M)]|UErU"((`zr#w7|m lTs%t$;' [# oHB{!&&88ڇ?֮V x 1f6@:L PU$АH |#&! /;Q^k}ld#_TM,C5x::VgEI=}b٘4s]3~"3<̄! "0[(zuoD\o?KFc`ju(ەT'סE:&]lm{{)iAFx𔨜jom@FA6*"qlXOCA0+q)78pf>yj&*=Cb}QHɗ0%R)y -oТM٪+QzS*򦆞2\+V0Q36b8b#ip+t! FI B`G6: -J=k>/w#_Bl -wAM(TVXvkjoN=?Mn&_ G!Akwio5t|&Jc#kՠr^~,+í9{>a%zZUW]gL\k8dzۨ7a4đ]2z{uȕyd:*7)9%~ *g#߉ $7I ߪ< Wc$alw|U˭# l]9L%Ne\"lI'4,>jX[{6>-^Jdjg#Ew2lZZ!` Cq߈hqeJ>hm9 Zpz]!I2@*kyͤY9و7J hDTjċ> S&%Ἂ4O 0 -endstream endobj 42 0 obj <>stream -H]Gs WUw   -W8Ȃؑ ywwnH]{LWuשSgj9l5٢:Z/h`Fu8Zi*Xӧ_xE+c>OmEi1m t_J+`/V. &촾яͫ7޾|={~2$8G{ߏg|իK?:~%oC~~~ܲ0Ŏk͏g_'O޽~yw{/x -4/߼}~x߿D~gןpΟZCmVT"pY xһ``e`B*D0-%@F"^@Ɔ^JX堵Ha:}p|0؉yd"tԬ ΂ 5t&fe$N9 lQxE[8mҰ \hei\ы}RCҺx!ކ@ڠdagJsHq,GS͡*M>vI/sz_# -4<"V_(xTKyCX࢙K#˕e0f uԞ1S*lM6Rn64hEO0 $]$6t۸#0LZ 6B&H/SaRy=` XYPF [35LQn4Ԑb!42&}+z*QqA|Fh[^ꏽ`b*^"{[ّ;4˨^U %'O#'M5~(}ӡ5C>%ËOnDss샅88V`1,D]t1 =G.98A'[S'ͱaO!XaO}l%sAJ!{"uZPb;إ3nV@PuP"bFGxT5 6xyH6r'DB=M푩n DnX:P! cdtCYécYԎK^䈺J=Q$q 0'I|[Dx)RLv -Y.U S\}*k)oǩ1mV-WQOLę zo2Bt6Bݠh"B1:d$L-#8r5ils Q&=ߑJ0cy r*K`LHFdH䴺 8[h99CZz3W|z[8MLJVU^TѺ`sWjHF2 m$e& Ud .+ɑ# -ԋ}9(ԁȜ.saW~?PA+r]_m~ lahp+ߑP]V3[BwD'p0cᥦMMݞ8;d:ӟ{Ls!%8Z&`:QFDJZ' u@h[yl0- E\'BQK2Տe:!?_bD!w#>7ۀk(lPyN.{qjckX4״xt=HIK) yPt6akyR*Y5L+pX KVr Dp}d - kwxsL҄1oN#aϣj'&a色 xbP8jO2[::,ԪF,QowC;LD/NSeGT#Ʋ9c:!ftj4c,C TPac!9{_@Յ f͗v"nM m`ǽS\c@#-pJ] smar\e [BpR92nwePk#`1$I>TJe't)r W!HUÅ+ s%0KH+H`0Fh7_hzwr'HAWKC9aAB @ "v`5`ުl&EEǙčmL#2t#3ZAl$t(=pj4'`hzK8NmQA5*"'N|nCӞH~AMF7ʘ?)"Eo.v²u9x -uFEE;%ܭ Xu3EkLQzo5nU[iq6З9?'5W В!>ZM%cB}sN=QBdxDQEŌx˺ r@:JAn9Lyڎ"3~*seY5&Fh}21Qb XGu~D@LoBA픎<_+|3p~ -cP}}k1 5@<-iD1Hn Pڏ.&[G21m5ebk}[ Dr0mN -J2G -Ѹ!p=n1b:BL5$FPb"] R 0;.ړs>Rf1hPW´LÖ^^\DT'yb?;6Jka9 ^K.\F. KMLUyC_!Zf!@S)vz-=ٷ\.`K "-!]&Hz-yЖCB 猢X#&b@N@+1|'BRKD #P: ȍfb"zm8̬ EC[߭u^jNUAz])΂~;1G!.yHA:H͘C I:u}X_k+hշm4-[3_& Q'#f,$sR{/1(+#nK"8Y8[Ӛm7w7>vOi;Gn+4 6vJ~~0+f8"Б ]GԙEf^(m>aQd]x 4\(xs$0?>`|A*ьmՊ{ԏ<` dXv7*)`.e!Xf˭˭YOߐ<ͭ -LiS}=5 -n].qHw'?ɼyхE`f S笱as mY~&CRf/cF߇!daQ0 Rr~#.[P\ׅ5Yl=Pv!c3gdxA,ڭVǡ6oXW>Tfer#vE(q+Lս71Iw?j{ "t D&t{룉Q7.|Ԃ϶!7ӭwvDԞ2p0Q0 ׷K̜f%H>D^PC/^a'Kl*y.a!ðΠT;g4^_oȫnSb7`C;!d 8LJK|(nHh1tD~AJFaB8?LFD'kD -01UtB5ͤ㴿F0Rsv6%,PyŦBt5֙Xx,5g81b1w4 3䆍LB;gyW p`I#8Ԓ,)(]GxUjB7V*iR㯪j|0-{Ntc^v!ƍh;E0#8QUf~oGEJU -APAZ="'t MA\{"YrB4ֽEEQOgw|egcMJ0`nRZz|-Z]X @=-|-=}X [Pxk% - *羁Q@0I2w]0l5S5 숳UgbY oJF -](?\G㈢2A)@&]MPL{Y@rnefEFenTxF7Lՠ4?E!G_ԽK扅;Ujް:X dXQǂ+sAkwj>@Y+7}HA||ѰP0AEI cxL$R9[ hdYx`bjf#X`7UW6mi BȊBJ> AU벘!4G-_'~d兡~jM%,0NL'ם*7W{W\ԎW2@^z5yX>Jԛ7$S S#+\Ok}-恠gA9+ ,ՏruPm;[!A4'GDtb!-\%~I%{]QmD\EAqO y;wǐ -:m'"" AA F0Xh￰uEb BIs44&21>TDX[K1o":\41k׃8cgco:GUSi>Da u%hBX`*T̾NIKɉ(I?~)/Qǀǖ4n]>C:p Q(Oz+n@D'm0dYRż~E#ӄ$$RKs/4c!%/±pg7H3Flؤ6|G0]N(Xz9Յa-(E4ԅ  듎h?8I<,Ū»0`NLX̀CѳAļ:AD\/[[=$[[,"p^#tpdÄrX!{t ,T+phTDm\85-; KVj{};~v<P+I9,tЧj@q %8B$Z ?((jA -HwB<8`z7U1MGS.L@1k =?yG,@7IYC吞.Sv*Γ{++t2+^fr[t -ch$=Zߝ^ (/;wE>;g!G㉍5tĴs_ۏz F8Gjʚ:r=w` 9jLy:5LѾbcW3%n쐎 b=W7LsoPm,YۍAHB8RʹK'|#<#2JNs.Kh+U熞j`B㙧' NHp;g±/1͗`y!ҫۍmI3')%lC`uƭe }rɴ|AScC,4S6~B5mT|U۴cz ӡͺB+ϟm#8vADx' _uED ~Q54QGĊ bx.;WG#gg 'r b`RMmy" -fJjReROLȬOƃEָj87 D\jCya!&i^(+'$A/ҕ~HasZ6p]fu (1Paa‚ru|b9"/m;lDhްw+E+b=`hl |اbJ'ew|o{I)cZ.(!vF00F}Z+,!2wC/;{?뱏MHt!ā7J2`~]\h=7TﴑplT;OBǽ 슱l/' J_0L4Ys ,7 `enl(xO8%,/h.fnӻL_3+ΈLrPFv{g?7,+ji>DSxL35A6t ~!Җ|StU-0`0jn2L84]++I`$Gؑǡ[Z0<65g"[ӕAtXmHC:[)胦~mD򗝪v r{$9 acg[y[\ F3%0թ;_u~"`<s3Է t)Z}/Č\ c/Mz'lV^Fa^;bfi.ss溘so^Rn` -ֈ tCm$9m\2BJlљ8{I5Aѿ_EʬH-9 -R%*?dJIWU-j*D*q'._Qo -`ɑ -\w2v -8s>]B} ]@  -g\72^X 1w5KJXO1}Ku9Xq(#oVzχ1H9:3j}ZqPٳS^neCdv-=cEm>!|Ji&L^7vXnLt_E=AxY>숳4:R2.2vVEF5GRӧP)VA3+'$RWlUF)K wC(u E&x#aX]!W) 1hy6+.TKWz NpsoPBѠ]'qN|YjZE/*TLQ -%}a?^߲;W\R&'=9nEѸTV5{Y -{O?}/?{?LZ\}_|o>'K;+fJ<Ƅ)oOOo$= >L t"d0KhB>uE/o/9 -fa MAY. -T;N[%yg#y3?,kxI.|=Զ7/ a&t%I1d4)[N8",EM;| bUlMN}G<K5s`bԮ~yh,Q#}\_/ fPQ}]`SS?S}m -iW,VQ@ =Cp2[o^{^piYہ#F!,U{wB& -A4 -}8TZv(AK=1LYdϺH + ]: zxƣ@UMq)JYRG {s,B*VD,5g9V? B.yV7ܟGE> YY1e[Md{FH.G!`x"Dg%\@ӵ3h/[<<}u3Z'!z{a%'6h 0\w0BZZ:ǀ-49͓8 -^Gazҫcs Д4Ea³ ڔϻ,\GYRz -ڳl 񼯦Gj߳o3m|,!a6qL^.G/U^{|@D+I䏭㈈'y> S>&rQ\:94b-ZWn1\>,_^ P 9P8D`C qt֐V-+`gͯ)Q\N#d8p1p*:XYGL$Bz [{o HFFl`q6zhZ|2 -˲lEi޸h2h1GXvD -bxVv -+253jf}X˸߿ f̐Ϲϕ[%~"Xyw2|Ց[**ϩS/Z h[Vek镊(LVW4\ -<ðI_NGus'܉Ӈ]}=[Ł&É.]< }G*|ƾg?\5}c(3QYZIjya|(>` 9V)5wY߭0ǵ,tI/N9]kˠvW@ϩ<[I I7ev ,B7F/}+`<Pw -08E)_@us(s40Ǹd,8pN?0jrUBIKmL؋Xx nR9);Pq -Q} lRNL8VqHc>o礼#Ltl8l8VtPyЋu u<=BQbLI< *M"/Q>dnX}b)dg|%'j.!-{b@8ƕ<{ j!/5MKv -Ì:߇^R,H~_A%U)XhkfAQ4?Ϭ1UmgYSMj^>d)A[Deu0W -\YωvCMgɞ\l@UNf]=D`?3oڐj)E22p'UsH0.=a=k׸1L>:'7%ܻވ ܆MeO|vX`cMɍȦ[Ntn7=H&40Q@|W8?`H5\ 5f~ry :ow&xCG0ٟʃgt%.BRKP?c:?5+LFiUh$*دScdrEZZ]^H3por%h82~hVbk85T skb.%ΐ#nz+I!]Qw?5I9A۟3 -,.N9'؄>γ "ODm;If\5S]#ԡpF3vb4)rl'm奙p:RD؀zɃ`3PĽ`,Ûh4j ?]"fuRqWfk@c Z%=do^(XK 4a*#%7Fp#)ddDZz?wc ^ d?thaR2iT RNdi]a4 1D3LB4 G4oD^nIf{a[uADe/؏e6B=Z>vmoAh|-bcJOk 'QΑЇV$b |]5j"Fx[;KE ܫ $nU\hn9bO3aK4VrڸWݶHp -b>[Kar߯[s7">Q:{[?G@Yxta7jOŠ :_hRy&}De.R$0U2!1ŶA+8E^2*-a'l -HR,qۉ(ՀGJehGwj2c@RaEd,ynW ME>\kz9*EZn$*=P?K7ƂK3Oo㜯-ݧ=Y*]D,pc ϞG]wˍ+$l 9RV2bG& !IW&-s]VN=޳U|YaWv-=Mڧ\d3"j6nkrPc%x^ 8Mj8r=Hq]+4L$5 @_?[=qWaR'Ç%m^ @ -EV),C[|odN\(>#}K*ė\Wg --* G{] f#rf&0)ʓop -UEW/0xEvJ]`mNa uO)Thrbϟ&*ڋyqS e$KR%{ LDW -lzǍWMDži{h}Y๤z胧*`SCVŦ $5`OmPV Cg 8aJ|-:Z  `x#|Dže yJCK'J p죕þBZ@},iؼ6* 4?p7gf]! =#YL26JA;Odcވp6I:p58VD :OH -Ws8?PaLPNLE|i| z^)#QЊҞ:^WT SZM 0ÿhK:4u@̯*k\tc{ro#.pV?Lv,"T$'aĊnӣTwVG9y<>Ym<*bxFu 6U~,eBї _cBc]#gc4M-N6I$)Z0U_xݗԂtO7q^rVSAG(%B/!^`(NЫd2 Cׄ&t0Bt6+99!WQ#7@pP*xk6˿[T&JtÁ䋀2kGFC0x_RE]OL ϣ&vZZKp_wtQ0#sJb8N^sU*DGtXa%WK1;V-dوNTǿ΅T9SM#phc2Lxh t麮&ҡBcG,D8nE:R"vZU@:qXBPxf8˭U -l|ЫJ m!B3Z${CJHӽq`X6T+Y=gK,2!vWH9,bl{)Kѷ°&ˉZ9~YIGrx~ڀ.&I_901QN!k v!")?aؚz`->=VKogf>o/!."K61;OHn6-WA,ds٤^jRlg-IypjֵbOZXH\ԁԭbc@3ɤƦ8b#NQ!$]8A;xwbk,`Ёe5xjZK4SXΏv.d= v"Wre%|a6E1? -w/_?7߼}?~rxC~/8Eš~Q^Ʈ+V6vxXY`NvT}h nvɢ(H` ~"њ,x vo3{s3FԀ`QS[Dֽo p̑H;"# 0 5rLDGG=̥qN5P!AhEOL,8Hm874n!*>,T6t ͟afzپq'oϻ-ԬʮEm C$R{П;J5V:.S,EBJHqJ%HJfZ7 -˝8Y+xjك ペ8s-Ilp R -_"`;&TYa a{A .:s8+0-7z&r-bH 6 t8h%z^!#XĕPAQ0|RC -r!:,F+\fW1*K 9 ~mfkZ¾W6$ C}[ZRU{B 1略Q)5Fe} K@Fܰ<ID1pvkf |{B6F9 -1nY^& -6ڙZ - \g"\$;@SW9j`,1;TfO"edq;6ZK/q`Zg#ۋ>Z= VD{4pmi5[#&4ڣm_"R.J^;Beqo03ZmsUa`fS2i7mT!P$>Y16#i:=~ըQ?e>䖠zP$@#2?^Dd:O2 JZѴ}=~qBvOB) fp7tuzD`mEMX@$/?+3R< -R>X^a^A9̻ @fw$Kw8.1-\RJ|0z\n&g"Φ N'A4rsaP&r*j~ȧ4i\((L$.O *l?s&N(իƵۣe=!0GYv#XRO4@l:tk lf+ >Bdܖe-DA`dQP x?V"Jh10h1xl=Z|wmVb5dX%EWyf 8>ʉ@^A3j|"" zhKD;aY)YDP6@d)$m']VTIe3n3Mb /Y%cy*N1HO+qQuqKTMM$[,HTJggxE -Z4/gaP4}YP@ˊ%S%}fJʮ`AYm$V_ڙyŸaa|62O+PAb*<>&Tf~VuQ . 5kN n^){/L/PGR'e<^e2:F"൉~x|63{ gtp$Li^8ə>obs,Qe%|BKHɤ鹿Ui -}blu53D@7"4spM"P6!ٚϥVxl *,:'<\9qWuп/O7^a]!ץ\jvD#`-8 ޽s_$G]Ǣ[y+o=5sḢpjRQ]I"yajz i*)!q)"}Jx Z~N6¤pC] w|yAuuDy|D{٫r` NcL^v/SFP뀑Ô5S_`_A7=+7WuDY?KW=d;ڶ%ݒ u*FTސrWNhjBcxm)`MɃX<%چ4/`QЛXs4:n"o}Uh֙ˬ}8xoQf݅.DwD >Td!y+`|A=!78# #D ~#n)65SB}"?LHaST"ìbDhSOΏ#qp״Aضd9>/vzYf!<0%`Djkݺn_5a -ӊ-׀P/i"Yf":TS;[83e̅[L{qzMTA!bkH{Gxn066tCQ~n39 v t2W_K3GGLOC.)˅80Tyz~R}5`:#]})0@"賝}xv,5=X86SB9SgPo?'4 -dl[4:VKʦI8pMg|ZhK CjMíZ9UN8Qp*xl߈<@_7&^oX #+۵0qqӽ8ADlfIOq=u?q"z=5 @(pm$UTyP*j|Rj/FU@}AА!)ojWHo9=oqTmӤ>cpΗeꁔ(*Ѱ#zf8IvR/mVJ LxYX\7yԌR4"`\&C\竽hE( pm`x  Ya*͸OU -EdSُyb30O 8=[G#(1 ^c._^@x -C!YʇH<2Fgǜ/#\Bs'?C0h_YtcFI;+Vgm=XKGw*rxmkp 0^f"c aSʅz?ώbG`G 2PZ)вUDg|rPl"w(4juqzj]_AtKz--D981"AnfOEQ;L -,l[U~( (w-젧(->"i> Kb ,˫""u) ?}MVT@efb*WkOUZV&bڥ/sW' JQ8s ch:]{'T'RP ;ٹkP^d -%R]߿ma,Й@!*YKĜm(:y 9r}'(6[^j(ro&R~ODa[&۱>ZN85.u02S]-rP[F5Jtڢ!Μܸ03І5ʻZOG t>Lbp!QAMӪ^ -Gi0~]} -L^3¢XV^)SʊFM -1/T+.k'eDH6A㿟>I`3c"u#G I|ޡI/}f v=_X&~ -39/D,j t"SVn v@_dD5ՎMEߥ,-WX7&,P=o9!ƒ@j <w!Zs{RX፜L7Ag1t п:® - -}{1 Sq!`[ K¡ѯNE,s!͎ Ҿfᑷ > #uLnS3X4b~R Cl+4NE0_oh]6Vn2kSrv}\IV*T8IU`ֶ_%V8Qe+B\n:.Xr 4NDQOy -[F`ԅ3m8w؁QL"&CL~2/[L90˅zN6EW' -H[=@D$`a|`3`*QpIJ |˵A f;;72rîL(.Fo~Q4]E ]'Ic?a"--ӀPaPU -Q;iƅ  m -t8[mz' -"> 3= A}16Zu Ҿ zMٮiq a$fL/:?kYdb^fu O9<`l*}Bsp̫`JmrB26 q/' LX3,:mؔ~*0][ごXebm^W>P`YO˚ҹS:<찈:CpHP| ;2QT/=T]-]$DBR/0m-`}.J Usmsƚk0RKK0b"i# X $}5CJIAAg\&iU H{F$Z' ي۶3Ӷ- -Lq32L*ck8I1P ÏG.g: r&{)Wzp;u_ׇ?uQP rFƋmlC?xFs .B.B#r="x#ԹbJC8o̶ K_gȨXo3MfxM'10aXd -~ , + Qp^* -w[Kir -][Lği< l/h5ˡ'8^z5+urؼM֝(fIZR`DyiUA~8!0Aqn@=@ 8;U - p!P{5r`H -ٮ,tc( f#&1GQz8{t$%Fj:nTlj2Mls $vɻjj[9߻q\Ɗh.ToGkA\x9G\%fS)w7sAB{CB̄.n`3eצ6c+K8]q9ۜIP]ﭯ-+B5FCw+J[[]㾥FINBF +&3{1Fb f1ʣyجŶֱ(x[g.Sx\Yl0p -ǚEًNݸ+_8*n摁Y*E60TC;Nf]2@]-$,R0z~?^W4{nWa1̪׵~)V͵g@AJ3 1H6YGKIrkW]ݬ>K0M9-;M =QU61ײCWt!US»F? )Ucj:I. j"׼z&DL]],jLsH -\"d%{H) -Ӆ BYIEsFO =*P z}&!2nbok <2;9wP/;[(Q\Zl8ܪI!%T=5 -ُɋ߼7s.E'\^N/^y}{ӇW>{}+8|?/n~~oǯW9._=~ns&\66nN,&Zi#G5as/W&"hG3%m1h͚ܞlp# 0}Ɂ/ Vd}e]"u#@t+9ѥt<@P"3f l-cJ(:h{<:MI3"z?md8>fh(͎ -  "H hFCK~?A)- Ʃ8JoAvhFx k[_7 6aQOsv:h䴤Aspݎ$ >nf1cX&_3ޒ>>W})Kj.1W_ -n!fg*4~~f%gs?78T`Ў̖Uo3B\ȔR¦̓͜Vu((xh40RC OhgڸJQBQITA8K|\ ,!H{]kQc:零kLAC?-v%eeehO + Swr=נ/!҄NdRlgSEvEagն3bME,@AJ ڸS8@-|FKҰ-@R}D8;q%e;.YNI\Tn/LK-ۿy &"Fm[0z'`ߗdeU$f!į SLS9יMhr4rtľMtt qs (50 T6ĵntO>&*&7WD^&IH?AarO$֪]0(" Bn_>tEWT#:Uȿmق8cw%Gq/BjZ$Qw>6O?7ڄ6鹾U,>.v2}&Nf[ &qbl/Aݜd(>Gm06puxԆڻGM JTzB1Afk"]Fe\?iA2uT]J Xc);U,!!+ MnNG(D8Oȸz@$eX`,OquRZO,y)aX~>M0sq(`5$丵7,F=(=[ >WA2tכf"HqB!XbkQja2xF'@d^U8ӊ| gYWXK0XX(,>V?|1(څЃIW~ 0[J}>(A,͛6@ C v75W>P`Kj dCI0WSNqM yT(!!ul <ed#?Ed:2Մ" hV>G#<6h`Ӓ! *ؖH N?ZcDB ^/>?&jH0TrgLv^`%]!ϩ:4L,]IB iRF>/pX޸JS5!T|Oru _CS EL̳h۩wU0 H -8asp_4Z҄mkuew"j1/Bj9ۿ/iBdb -ڙn`~/K1X |UrԨVk@Q]H&M u:SbX{ p9XU+06*Y,a@`D y[ *!т= -(t+ exBK(F@fnaH#>`4d-(LI ]6TTz tR#.s!'5eLdqu.CfU*A`՚3V}49Vy&ht:jmD#t\hL\HiDv̱1[ .ߡ;UmEqoՂF;֛ p<q|.{Mx0Y~yө , یaEɆxD;[9Ʒ6= m3̍ΌRGQ5e7"Ԫ%mIoZH$ioG#VʓaԉSd3CKFl3.".9;Cݿ/ݒl @0;E \[,<Ɖ2["ʧ6ګ6OBFӅ L`E'@jdW ;U,|iF&{/ v֕QPAf8."I*#᧤O+pEu -Um -D\K˹ ub 9ܱQT>GGv:,B #W@S"0R<7Մ@0K}WV~B @ԲQ\}z$FDa6e3nǂ0=U]h ⼊Gxx_6") KI?f Qtİٿ@u!af҇ѩ4p]ep]H>rJ#Q!EEe#UjCQoJ,]s; Ä7Ol!2a%r[JUnaqZuWVr n9^PGLv}G>OIO{Ed\At"ER֪c>"v7P5NH -@,i3+ =[,u`0%F9)Ԫ57E]ItR haRx;;r; ==c$T:$w۪FO.3F>jhtY~O%GLd h7#Sj$x-.fAqO ͪ#! ?7q,-EΝ[4uh-Qo0\@F<ǝwD9l>x5k𰔘 1=0Pxs"h ٿ?z*bay}_)fNrO>7jԤDH '_ɶ kC7Jn&6yU<խfؾ>{uo@@԰SLl1ѩ<W=b#Q >a(u{lCK ^!CM1i*1 -'3 hot]Bf;~`&qI6+$V6B#Qk O sЭ4%CHAjǷ|`;Ac:#,:/x Da萟UJɣ('u JnCَl > Qq ?3jq?TSn:"bLW:hp.6Sn>=[t#!Owh8tSc0&Br|Ѻ0ch~ؠEiPiV3܅v6əljU2%U&.xx9x;n/rs`7wfʭLDaDX Ɲfh8V[:dnoqh*A"%*aߋޱmB|@'Q.7"gV"o..0ӌ-lh˖ɍq*g]4dV{+2m46x749gFΜf伊:X[h\pQzQx =1VNqelki]>3ݎb;srӽ:*p{|(Ė56!NV#fz?Wc}lAT}!'? V!Tg*$7AnJG>NSA\[! -ߑj,N=(-+ByL[j3qmوߞۚ* Gߏ=$jǾCUly #3P6VYqU&"O^ -;W!wFXd87*L͢ ,D>gFqi(`GQuz!te ZuRZhO :SS䜳,qD.ỳ݄fy3i 04qLB`YdHsC.F)l-Fp;S%j\50<KNsmjQ-yc9//jXC-[y鳜‘,.%?5;*ɒ-6ܟ~eN`G5r hϬtچh|b̽]A13{~q?WH2\>k1_>ؾƊ -81yl4=ZgSymˊ~/@`ӌ6zZun}O8|/Q 'd/daBqSw\ .*Cz_G]{m{!XZ0zfZx%cO͟V$]Ո;ɴ^ LQGS{6-WGm??+݈h_n=o(gZ$VRbd uI)OWT>?Ѻ,bHjw#i}(uC/UPoP`NVcFDlڃ_};T]h{9'=-?WBLE0-1knڭPP1 oM]늈@dpy4^'buZU -k<aƘ0%GP.=b뤭#uVZv.Md;,-)ר'rHw#Aos#dD4 ꋄT%G%oLw^L8V)WLx}"V=W2ò:Ɲ2I0&iOEU oE'mxKkϏé8C[c}vY_Ӑw?#| 2#/Y5p>F =Sh'&_L3` qy^ Qrq[GAl^:t@8Lc,r+F#o\/ę1BRIFRFQʣrWq[GoyĝZYܶQ|L0>ch*e:Aq|E?<=oesC*='5 bn=x[D9N5j;|4MB^|xGEB,m@41q;cG8, +}EʓZ2|k|q1ĈM -?ĨQwkBZF!09XY0w_Q\FS [ Dq+y"k+)/\G'y|x4,CdcK.7 ubwԌjI.|>f.?z UeL ? e> endobj xref -0 44 -0000000000 65535 f -0000000016 00000 n -0000000144 00000 n -0000041305 00000 n -0000000000 00000 f -0000042728 00000 n -0000785366 00000 n -0000041356 00000 n -0000041670 00000 n -0000043027 00000 n -0000042914 00000 n -0000042007 00000 n -0000042166 00000 n -0000042214 00000 n -0000042798 00000 n -0000042829 00000 n -0000043100 00000 n -0000043850 00000 n -0000044979 00000 n -0000047256 00000 n -0000076652 00000 n -0000107961 00000 n -0000171552 00000 n -0000213382 00000 n -0000259039 00000 n -0000318851 00000 n -0000360528 00000 n -0000423383 00000 n -0000466067 00000 n -0000506443 00000 n -0000525584 00000 n -0000529321 00000 n -0000535961 00000 n -0000555763 00000 n -0000571456 00000 n -0000584869 00000 n -0000590453 00000 n -0000615200 00000 n -0000640887 00000 n -0000670215 00000 n -0000698999 00000 n -0000727959 00000 n -0000756786 00000 n -0000785389 00000 n -trailer -<<477B29092DE61B4394E02E09C2040298>]>> -startxref -785584 -%%EOF -- GitLab From c6faa2ff0cb842b18250f1e5fea502f90d2e5e20 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 13 Aug 2018 12:21:38 +0300 Subject: [PATCH 29/32] [NY-2581] Implemented interactive image preview for group avatar. (#1094) --- .../ChatSettingCell/AvatarCell.swift | 2 +- .../SettingViewModelProtocol.swift | 2 +- .../Presenter/SettingsGroupPresenter.swift | 15 +++++++++------ .../Modules/SettingsGroup/SettingsProtocols.swift | 4 ++-- .../SettingsGroup/View/SettingsGroupVC.swift | 4 ++-- .../WireFrame/SettingsGroupWireFrame.swift | 10 ++++------ 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Nynja/Library/UI/UITableViewCells/ChatSettingCell/AvatarCell.swift b/Nynja/Library/UI/UITableViewCells/ChatSettingCell/AvatarCell.swift index 62739e65f..dc89bfc4e 100644 --- a/Nynja/Library/UI/UITableViewCells/ChatSettingCell/AvatarCell.swift +++ b/Nynja/Library/UI/UITableViewCells/ChatSettingCell/AvatarCell.swift @@ -114,7 +114,7 @@ class AvatarCell: BaseSettingCell, ConfigurableCell { // MARK: - Actions @objc private func avatarTapped(_ recognizer: UITapGestureRecognizer) { - delegate?.didTapSetting(.Avatar) + delegate?.didTapSetting(.Avatar(avatar)) } @objc private func nameGroupTapped(_ recognizer: UITapGestureRecognizer) { diff --git a/Nynja/Library/UI/UITableViewCells/ChatSettingCell/SettingViewModelProtocol.swift b/Nynja/Library/UI/UITableViewCells/ChatSettingCell/SettingViewModelProtocol.swift index d45ffc9bf..4e45469c8 100644 --- a/Nynja/Library/UI/UITableViewCells/ChatSettingCell/SettingViewModelProtocol.swift +++ b/Nynja/Library/UI/UITableViewCells/ChatSettingCell/SettingViewModelProtocol.swift @@ -8,7 +8,7 @@ enum SettingViewModelType { - case Avatar + case Avatar(UIImageView) case Name case Alias case Rules diff --git a/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift b/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift index 33ebbc763..346e13c9e 100644 --- a/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift +++ b/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift @@ -54,15 +54,18 @@ class SettingsGroupPresenter: BasePresenter, SettingsGroupPresenterProtocol, Cre } } - func changeAvatar() { + func openAvatar(from imageView: UIImageView) { if isAdmin { wireFrame.changeAvatar() - } else { - if let urlStr = room?.data?.first?.payload, !urlStr.isEmpty { - if let url = URL(string: urlStr) { - wireFrame.showAvatar(url) - } + } else if let urlString = room?.data?.first?.payload, let url = URL(string: urlString) { + guard let view = view as? UIViewController else { + return } + let transitionInfo = ImagePreviewTransitionInfo(interactiveDismissalEnabled: true, + startingView: imageView, + endingView: imageView) + + wireFrame.presentAvatarModally(imageURL: url, on: view, with: transitionInfo) } } diff --git a/Nynja/Modules/SettingsGroup/SettingsProtocols.swift b/Nynja/Modules/SettingsGroup/SettingsProtocols.swift index 2617cf66f..bb0ca5f62 100644 --- a/Nynja/Modules/SettingsGroup/SettingsProtocols.swift +++ b/Nynja/Modules/SettingsGroup/SettingsProtocols.swift @@ -16,7 +16,7 @@ protocol SettingsGroupWireframeProtocol: class { func present(navigation: UINavigationController, main: MainWireFrame?) func hide() - func showAvatar(_ url: URL) + func presentAvatarModally(imageURL: URL, on view: UIViewController, with transitionInfo: ImagePreviewTransitionInfo) func changeAvatar() func changeGroupName(name: String) func changeAlias(alias: String, nicks: [String]) @@ -43,7 +43,7 @@ protocol SettingsGroupPresenterProtocol: BasePresenterProtocol { */ func hide() - func changeAvatar() + func openAvatar(from imageView: UIImageView) func changeGroupName() func changeAlias() func showRules() diff --git a/Nynja/Modules/SettingsGroup/View/SettingsGroupVC.swift b/Nynja/Modules/SettingsGroup/View/SettingsGroupVC.swift index fe8fe8e8d..8ceaf243e 100644 --- a/Nynja/Modules/SettingsGroup/View/SettingsGroupVC.swift +++ b/Nynja/Modules/SettingsGroup/View/SettingsGroupVC.swift @@ -123,8 +123,8 @@ class SettingsGroupViewController: BaseVC, SettingsGroupViewProtocol, SettingCel func didTapSetting(_ type: SettingViewModelType) { switch type { - case .Avatar: - presenter.changeAvatar() + case let .Avatar(imageView): + presenter.openAvatar(from: imageView) case .Name: presenter.changeGroupName() case .Alias: diff --git a/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift b/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift index c6a85a0a6..887652606 100644 --- a/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift +++ b/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift @@ -55,12 +55,10 @@ class SettingsGroupWireFrame: SettingsGroupWireframeProtocol { navigation?.popViewController(animated: true) } - - func showAvatar(_ url: URL) { - if let mainNavigation = main?.navigation { - ImagePreviewWireFrame().presentImagePreview(navigation: mainNavigation, imageURL: url) - mainNavigation.view.layoutIfNeeded() - } + func presentAvatarModally(imageURL: URL, on view: UIViewController, with transitionInfo: ImagePreviewTransitionInfo) { + ImagePreviewWireFrame().presentImagePreviewModally(parentVC: view, + imageURL: imageURL, + transitionInfo: transitionInfo) } func changeAvatar() { -- GitLab From 35032ff186fc6a113dacd43400e2d9957c448e98 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 13 Aug 2018 12:23:10 +0300 Subject: [PATCH 30/32] [NY-1408] Implemented case insensitive search on time zone selector screen (#1088) --- .../SwiftLibrary/String/String+Search.swift | 15 +++++---------- .../Interactor/TimeZoneSelectorInteractor.swift | 7 ++----- .../View/TableView/TimeZoneCellModel.swift | 2 +- Nynja/TimeZoneLocal.swift | 4 ++++ 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Nynja/Extensions/SwiftLibrary/String/String+Search.swift b/Nynja/Extensions/SwiftLibrary/String/String+Search.swift index 7e7afc530..b17ec6e7b 100644 --- a/Nynja/Extensions/SwiftLibrary/String/String+Search.swift +++ b/Nynja/Extensions/SwiftLibrary/String/String+Search.swift @@ -7,26 +7,21 @@ // extension String { - func isIn(string: String, options: String.CompareOptions) -> Bool { - return isIn(strings: [string], options: options) + return string.contains(substring: self, options: options) } func isIn(strings: [String], options: String.CompareOptions) -> Bool { return strings.contains(substring: self, options: options) } + func contains(substring: String, options: String.CompareOptions) -> Bool { + return range(of: substring, options: options) != nil + } } - extension Array where Element == String { - func contains(substring: String, options: String.CompareOptions) -> Bool { - guard let _ = self.first(where: { $0.range(of: substring, options: options) != nil }) else { - return false - } - - return true + return contains { $0.contains(substring: substring, options: options) } } - } diff --git a/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift b/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift index dd0223add..b61eef2eb 100644 --- a/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift +++ b/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift @@ -11,10 +11,7 @@ class TimeZoneSelectorInteractor: TimeZoneSelectorInteractorInputProtocol { weak var presenter: TimeZoneSelectorInteractorOutputProtocol! func filterTimeZones(text: String) -> [TimeZoneLocal] { - let filtered = TimeZoneManager.shared.timezones.filter { - text.isEmpty ? true : $0.utc.first!.replacingOccurrences(of: "_", with: " ").contains(text) - } - return filtered + let timezones = TimeZoneManager.shared.timezones + return text.isEmpty ? timezones : timezones.filter { text.isIn(string: $0.name, options: .caseInsensitive) } } - } diff --git a/Nynja/Modules/TimeZoneSelector/View/TableView/TimeZoneCellModel.swift b/Nynja/Modules/TimeZoneSelector/View/TableView/TimeZoneCellModel.swift index f83b38c8b..fd28944b5 100644 --- a/Nynja/Modules/TimeZoneSelector/View/TableView/TimeZoneCellModel.swift +++ b/Nynja/Modules/TimeZoneSelector/View/TableView/TimeZoneCellModel.swift @@ -18,7 +18,7 @@ final class TimeZoneCellModel: CellViewModel { } func setup(cell: TimeZoneCell) { - cell.titleLabel.text = timeZone.utc.first!.replacingOccurrences(of: "_", with: " ") + cell.titleLabel.text = timeZone.name cell.descriptionLabel.text = timeZone.text } } diff --git a/Nynja/TimeZoneLocal.swift b/Nynja/TimeZoneLocal.swift index 37f8b4a10..4a455861e 100644 --- a/Nynja/TimeZoneLocal.swift +++ b/Nynja/TimeZoneLocal.swift @@ -17,6 +17,10 @@ struct TimeZoneLocal : Codable { var text: String var utc: [String] + var name: String { + return utc.first!.replacingOccurrences(of: "_", with: " ") + } + var timeZone: TimeZone? { if utc.count > 1 { return TimeZone(identifier: utc[1]) -- GitLab From 82102a9f0b7ac7002d9a6b56a2968d0ab26b93b4 Mon Sep 17 00:00:00 2001 From: Volodymyr Hryhoriev Date: Mon, 13 Aug 2018 13:00:32 +0300 Subject: [PATCH 31/32] [NY-2548] Implement 'application has to be in English only'. --- Nynja.xcodeproj/project.pbxproj | 8 - Nynja/Resources/ru.lproj/InfoPlist.strings | 12 - Nynja/Resources/ru.lproj/Localizable.strings | 797 ------------------- Nynja/Resources/ru.lproj/StatusCodes.strings | 18 - 4 files changed, 835 deletions(-) delete mode 100644 Nynja/Resources/ru.lproj/InfoPlist.strings delete mode 100644 Nynja/Resources/ru.lproj/Localizable.strings delete mode 100644 Nynja/Resources/ru.lproj/StatusCodes.strings diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index ffc746cb8..12ed5d24f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -2151,11 +2151,9 @@ 00D7B5C620285BA7004B0E2B /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = ""; }; 00E4A65E201A287100CEC61F /* MapSearchDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSearchDS.swift; sourceTree = ""; }; 00E8513A2021E96E007DC792 /* GApiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GApiResponse.swift; sourceTree = ""; }; - 00E864662049840A00844FF1 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 00E86468204D519600844FF1 /* LanguageSettingsItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSettingsItemsFactory.swift; sourceTree = ""; }; 00E8646C204D788100844FF1 /* LanguageSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSettingsCell.swift; sourceTree = ""; }; 00E864712052B1BE00844FF1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 00E864732052B1C400844FF1 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; 00E9824B205C1E19008BF03D /* SecurityItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityItemsFactory.swift; sourceTree = ""; }; 00E9824D205C2604008BF03D /* SessionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionItemView.swift; sourceTree = ""; }; 00E9824F205C2668008BF03D /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = ""; }; @@ -3374,7 +3372,6 @@ A4B544E920EFB1A800EB7B0F /* errors_Spec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = errors_Spec.swift; sourceTree = ""; }; A4B544EE20EFB4DF00EB7B0F /* BertTupleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BertTupleExtension.swift; sourceTree = ""; }; A4B544F920EFC0AD00EB7B0F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/StatusCodes.strings; sourceTree = ""; }; - A4B544FB20EFC0BC00EB7B0F /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/StatusCodes.strings; sourceTree = ""; }; A4B544FE20EFC1BA00EB7B0F /* StatusCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCode.swift; sourceTree = ""; }; A4BCEC6B20DBF2A40078B076 /* Link+DB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Link+DB.swift"; sourceTree = ""; }; A4BE4AB42068E98C00C041D1 /* ALTextInputBar+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTextInputBar+Trim.swift"; sourceTree = ""; }; @@ -12675,8 +12672,6 @@ knownRegions = ( en, Base, - uk, - ru, ); mainGroup = 3ABCE8E41EC9330D00A80B15; productRefGroup = 3ABCE8EE1EC9330D00A80B15 /* Products */; @@ -15073,7 +15068,6 @@ isa = PBXVariantGroup; children = ( 00E864712052B1BE00844FF1 /* en */, - 00E864732052B1C400844FF1 /* ru */, ); name = InfoPlist.strings; sourceTree = ""; @@ -15090,7 +15084,6 @@ isa = PBXVariantGroup; children = ( 6D36F8E31F0ADBD300FA1AC8 /* en */, - 00E864662049840A00844FF1 /* ru */, ); name = Localizable.strings; sourceTree = ""; @@ -15099,7 +15092,6 @@ isa = PBXVariantGroup; children = ( A4B544F920EFC0AD00EB7B0F /* en */, - A4B544FB20EFC0BC00EB7B0F /* ru */, ); name = StatusCodes.strings; sourceTree = ""; diff --git a/Nynja/Resources/ru.lproj/InfoPlist.strings b/Nynja/Resources/ru.lproj/InfoPlist.strings deleted file mode 100644 index 0a2c93b65..000000000 --- a/Nynja/Resources/ru.lproj/InfoPlist.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* InfoPlist.strings - Nynja - - Created by Michael Katkov on 3/9/18. - Copyright © 2018 TecSynt Solutions. All rights reserved. */ -"NSCameraUsageDescription"="NYNJA нужно это для того чтобы позволить вам делать фото и видео."; -"NSContactsUsageDescription"="NYNJA нужно это для того чтобы позволить вам добавлять новые контакты."; -"NSLocationAlwaysUsageDescription"="NYNJA нужно это для того чтобы знать вашу локацию чтоб вы могли поделиться ей."; -"NSLocationWhenInUseUsageDescription"="NYNJA нужно это для того чтобы знать вашу локацию чтоб вы могли поделиться ей."; -"NSMicrophoneUsageDescription"="NYNJA нужно это чтоб отсылать голосовые сообщения."; -"NSPhotoLibraryUsageDescription"="NYNJA нужно это чтобы вы могли пользоваться локальными картинками."; -"NSPhotoLibraryAddUsageDescription"="NYNJA нужно это для того чтобы позволить вам сохранять фото и видео на устрйостве."; diff --git a/Nynja/Resources/ru.lproj/Localizable.strings b/Nynja/Resources/ru.lproj/Localizable.strings deleted file mode 100644 index cc25b1f21..000000000 --- a/Nynja/Resources/ru.lproj/Localizable.strings +++ /dev/null @@ -1,797 +0,0 @@ -/* Localizable.strings - Nynja - - Created by Bohdan Paliychuk on 7/3/17. - Copyright © 2017 TecSynt Solutions. All rights reserved. */ - -// MARK: App name -"nynja"="NYNJA"; - -// MARK: Common -"delete"="Удалить"; -"done"="Готово"; -"call"="Вызов"; -"Search"="Поиск"; -"remove"="Удалить"; -"deleted account name"="Удаленный аккаунт"; -"create" = "Создать"; -"skip" = "Пропустить"; -"mute"="Отключить звук"; -"unmute"="Включить звук"; -"join"="Присоедениться"; - -// MARK: alert -"number_or_region_empty"="Номер или регион пуст"; -"invalid_nomber"="Неправильный номер"; -"ok"="Хорошо"; -"no"="Нет"; -"yes"="Да"; -"cancel"="Отменить"; -"Deny"="Отклонить"; -"Go_to_settings"="В Настройки"; -"your_device_is_rooted" = "Кажется на вашем устройстве есть root права. Безопасность вашего устройства может быть нарушена."; - -// MARK: login vc -"confirm_contry_long"="Пожалуйста, подтвердите код вашей страны и введите номер телефона. \nПодписывая, Вы соглашаетесь в Условиями Службы"; -"terms_of_service"="Условия службы"; -"confirm_contry_short"="Пожалуйста, подтвердите код вашей страны и введите номер телефона"; -"code_send"="Мы отправили код на ваш номер телефона"; -"code_send_isReceived"="Мы отправили код на ваш номер телефона.\nВы еще не получили код?"; -"have_u_receive"="Вы еще не получили код?"; -"wrong_sms"="Неправильный СМС код"; -"alert_sms"="СМС"; -"alert_voice"="Голосовой звонок"; -"alert_get_code"="Как вы хотите получить код?"; -"geolocation_message"="Вы можете изменить конфигурацию службы геолокации NYNJA, в любое время в настройках вашего телефона"; -"You_should_receive_it_within"="Вы получите это в течении"; -"seconds"="секунд"; -"Please_confirm_number"="Пожалуйста подтвердите, что номер который Вы ввели верен:"; -"please_select_country"="Пожалуйста, выберите страну"; -"modify"="Изменить"; -"confirm"="Подтвердить"; -"wrongVersion"="У вас устаревшая версия приложения. Пожалуйста, проверьте что вы загрузили последнюю версию с Testflight. Если ваша версия последняя доступная на Testflight в данный момент и вы продолжаете получать это сообщение, пожалуйста, подождите не много, наши разработчики выпустят новую версию для вас."; -"You_are_not_allowed_to_login"="Вам не позволен вход"; - -// MARK: QR code -"scan_qr_code"="Сканировать QR Код"; -"my_qr_code"="Мой QR Код"; - -// MARK: tutorial -"get_started"="Начат"; - - -// MARK: contactcs -"contact_added"="Добавлен"; -"contact_add"="Добавить"; -"contact_requested"="Запрошен"; -"contact_accept"="Согласиться"; -"contact_block"="Заблокировать"; -"contact_blocked"="Заблокирован"; -"contact_ignore"="Игнорировать"; - -// MARK: add contact -"scan_qr"="Отсканировать QR код"; - -"choose_action"="Выберите опцию"; -"dowload_image_from_gallery"="Выбрать из Галереи"; -"dowload_image_from_camera"="Снять Камерой"; -"no_camera"="Нет Камеры"; -"sorry_this_device_has_no_camera"="Извините, у устройства нет камеры"; - - - -// MARK: edit contact -"editContact_firstName"="Имя *"; -"editContact_lastName"="Фамилия "; -"EditName_first_name"="Имя*:"; -"EditName_last_name"="Фамилия:"; -"nameField_empty"="Имя необходимо, чтобы люди знали с кем они общаются"; -"First_Name_must_be_at_least"="Имя должно быть не меньше двух символов в длину"; -"Last_Name_must_be_at_more"="Фамилия должна быть не более 32 символов в длину"; -"First_Name_must_be_at_more"="Имя должно быть не более 32 символов в длину"; -"Edit_Name"="Изменить Имя"; -"Keep_your_existing_name."="Оставить существующее имя"; - -// MARK: edit username -"Edit_Username"="Изменить имя пользователя"; -"EditName_username"="Имя пользователя:"; -"Keep_your_existing_username"="Оставить существующее имя пользователя."; -"Don't_add_username"="Не добавлять имя пользователя"; -"UsernameField_empty"="Извините, это имя пользователя неправильное"; -"Username_must_be_at_least"="Имя пользователя должно быть по крайней мере 2 символа в длину"; -"Username_max_size"="Имя пользователя не должно превышать 32 символа в длину"; -"Invalid_username"="Извините, это имя пользователя неправильное"; -"Username_is_busy"="Извините, это имя пользователя уже зарегестрировано"; -"Username_description"="Вы можете выбрать имя пользователя в NYNJA. Если Вы выбрали имя пользователя, то другие люди смогут вас найти по этому имени, не зная вашего номера телефона"; - -"Edit_photo"="Изменить фото"; - -// MARK: my group alias -"My_Group_Alias"="Мой псевдоним группы"; -"Alias_Field_Title"="Мой псевдоним в группе:"; -"Cancel_Group_Alias"="Отменить псевдоним группы."; -"Save_Alias"="Сохранить"; -"Invalid_alias"="Извините, этот псевдоним неправильный"; - -"Alias_Empty_Message"="Извините, мы не позволяем пустых псевдонимов"; -"Alias_Max_Size_Message"="Псевдоним должен быть не более 65 символа в длину"; -"Alias_Busy_Message"="Извините, это имя пользователя уже занято"; - -// MARK: edit group name -"Group_Name"="Имя группы"; -"Group_Name_Field_Title"="Имя группы*:"; -"Cancel_Group_Name"="Отменить имя группы."; -"Save_Group_Name"="Сохранить"; -"Group_Name_Empty_Message"="Пожалуйста, введите по крайней мере один символ "; - -// MARK: add contact via phone -"addContactPhone_enterPhone"="Введите номер телефона"; -"Profile_Not_Found"="Профиль не найден"; - - -// MARK: add contacts from phone book -"Contacts_Not_found"="Контакты не найдены"; - -// MARK: add contact by user name -"By_User_Name"="По имени пользователя"; -"Username_Field_Title"="Имя пользователя:"; -"Search_Button_Title"="Поиск"; - -"Add_Contact_By_Username_Incorrect_Username"="Неправильный формат имени пользователя"; -"Add_Contact_By_Username_May_Contain_English_Letters"="Имя пользователя может содержать английские буквы, числа и знак подчеркивания"; -"Add_Contact_By_Username_No_Results_Found"="Нет результатов"; - - -// MARK: call view -"call_accept"="Принять"; -"call_decline"="Отклонить"; -"call_incoming_audio_conference"="Аудиоконференция"; -"call_incoming"="Входящий голосовой звонок..."; -"call_incoming_video"="Входящий видео звонок..."; -"call_ringing"="Звонок..."; -"call_failed"="Звонок не удался"; -"call_connecting"="Соединение..."; -"accept_audio"="Принять Аудио"; -"switch_to_audio"="Переключиться на Аудио"; -"hang_up"="Принять звонок"; -"switch_camera"="Переключить камеру"; -"port_out_to_phone"="Импортировать в телефон"; -"more"="Больше"; -"turn_on_video"="Включить видео"; -"mute_call"="Отключить звук"; -"unmute_call"="Включить звук"; -"speakerphone"="Громкая связь"; -"return_to_chat_from_call"="Вернуться в чат"; - - -// MARK: chat list -"chats_list_title"="Список чатов"; -"last_message_emoji"="Смайлики"; -"last_message_videoCall"="Видео звонок"; -"last_message_photo"="Фото"; -"last_message_location"="Локация"; -"last_message_video"="Видео"; -"write_message"="Напишите ваше сообщение"; -"message"="Сообщение..."; -"last_message_file"="Файл"; -"last_message_voice"="Голос"; -"last_message_contact"="Контакт"; -"voice_message" = "Голосовое сообщение"; -"audio_call" = "Аудиозвонок"; - -// MARK: MessageVC -"save_to_gallery"="Сохранить в галерею"; -"save_to_downloads"="Сохранить в загрузки"; -"delete_message"="Удалить сообщение?"; -"delete_for_all"="Удалить для всех"; -"delete_for_both"="Удалить для обоих"; -"delete_for_me"="Удалить для меня"; - -"accepted_your_contact_request"=" принял запрос в контакты! Отправить сообщение!"; -"accepted_contact_request"=" приняли запрос в контакты"; -"you" = "вы"; -"invited" = "пригласили"; -"removed" = "удалили"; - -"edit_message" = "Редактируемое сообщение"; - -// MARK: Call -"Return_to_call"="Вернуться к звонку"; -"You_can't_call_to_yourself"="Вы можете позвонить себе"; -"You_can't_call_when_you_calling"="Вы не можете звонить, когда вы говорите"; - -// MARK: record -"message_delay"="Задержка сообщения"; -"microphone_is_busy" = "Пожалуйста завершите звонок прежде чем записывать звуковое сообщение"; - -"Unread"="Не прочитанный"; - -// Mark: Permissions -"camera"="камера"; -"gallery"="галерея"; -"location"="локация"; -"microphone"="микрофон"; - -"please_allow_permission"="Пожалуйста позвольте NYNJA доступ к вашим %@."; - -// MARK: notification: -"notif_accept_requst"="примите запрос в контакты! Отправить сообщение!"; -"notif_wants_toAdd"="Хочет добавить вас в NYNJA!"; - -// MARK: mapView -"accurate_to"="Приблизительно"; -"meters"="метров"; -"target_location"="Целевая локация"; - -"map_mode_map"="Карта"; -"map_mode_satellite"="Спутник"; -"map_mode_hybrid"="Гибрид"; - -"map_permission_request"="Пожалуйста позвольте NYNJA знать вашу локацию"; -"map_permission_deny"="Хорошо"; -"map_permission_settings"="Настройки"; - -// MARK: Map Search -"map_search_screen_title"="Поиск или найди друга"; -"map_search_no_results"="Нет результатов"; - - -// MARK: Top Up Account -"top_up_account"="Поднять Счет"; - -// MARK: Paticipants -"participants"="Участники группы"; -"admins"="Админы группы"; -"admin"="админ"; - -// MARK: Add Participants -"delete_participants"="Удалить участников"; -"add_participants"="Добавить участников"; -"add_members_to_call"="Добавить участников для вызова"; -"please_choose_at_least_one_member"="Пожалуйста, выберите хотя бы одного участника"; -"no_participant_selected"="не выбрано участников"; -"add_participants_history_count_popup_title"="Добавить пользователей в группу"; -"add_participants_history_count_popup_message"="Номер последнего сообщения для того чтоб переслать"; -"you_cannot_remove_all"="Вы не можете удалить всех членов."; - -// MARK: share extension -"login_to_Nynja"="Войти в Nynja"; -"open_and_login_to_share"="Откройте Nynja и войдите чтоб поделиться"; -"next"="Следующий"; - -// MARK: settings group -"group_options"="Опции группы"; -"return_to_chat"="Вернуться в групповой чат"; -"delete_leave"="Удалить и покинуть чат?"; -"delete_chat_history"="Удалить историю чата?"; -"conf_remove"="Удалите этих пользователей?"; -"please_add_admins"="Пожалуйста добавьте другого админа перед тем как покинуть группу"; - -// MARK: Create Group -"New_Group"="Новая группа"; -"Cancel_new_group"="Отменить создание группы"; -"Create_group"="Создать"; - -// MARK: groups list -"groups_list_title"="Список групп"; - -// MARK: Contacts -"contacts_title"="Контакты"; - -// MARK: By Contacts -"by_contacts_title"="По контактам"; - -// MARK: By QR Code -"by_qr_code_title"="По QR коду"; - -// MARK: By Number -"by_number_title"="По номеру"; - -// MARK: By Username -"by_username_title"="По имени пользователя"; - -// MARK: History -"history_title"="История"; - -// MARK: Select Country -"select_country_title"="Выбрать страну"; - -// MARK: Group rules -"groups_rules_title"="Правила группы"; -"updated_rules_message"="По-видимому правила изменились недавно, пожалуйста проверьте изменения сначала."; -"save_group_rules"="Сохранить"; -"cancel_group_rules"="Отменить групповые правила."; - -// MARK: Group storage -"files_filter"="файлы"; -"photos_filter"="фотографии"; -"videos_filter"="видео"; -"links_filter"="ссылки"; -"audios_filter"="аудио"; - -// MARK: Context menu -"star"="В избранное"; -"reply"="Ответить"; -"edit"="Изменить"; -"forward"="Вперед"; -"translate"="Перевести"; -"transcribe"="Переписать"; -"choose_language" = "Другой язык"; -"copy"="Скопировать"; -"share"="Поделиться"; -"show_in_chat"="Показать в чате"; -"unstar"="Убрать звезду"; -"open"="Открыть"; - -// MARK: Forward -"chat_list"="СПИСОК ЧАТОВ"; -"group_list"="СПИСОК ГРУПП"; -"no_forward_target_selected"="Чат не выбран"; -"forward_screen_logo"="ВЫБРАН"; -"please_choose_at_least_one_forward_chat"="Пожалуйста выберете по крайней мере один чат"; - -// MARK: date formatting -"now"="Сейчас"; -"minutes_ago"="минуты назад"; -"one_minute_ago"="одну минуту назад"; -"hours_ago"="ч назад"; -"yesterday"="Вчера"; - -// MARK: drop member from group -"delete_group"="УДАЛИТЬ ЭТУ ГРУППУ"; -"you_was_removed"="Вы были удалены из этой группы"; -"are_u_sure"="Вы уверены, вы хотите удалить этот чат?"; - -// MARK: Starred VC -"starred_filter_all"="все"; -"starred_filter_text"="текст"; -"starred_filter_voice"="голос"; -"starred_filter_images"="картинка"; -"starred_filter_files"="файлы"; -"starred_filter_video"="видео"; -"starred_filter_contacts"="контакты"; -"starred_filter_locations"="локации"; -"starred_filter_links"="ссылки"; - -// MARK: Other User VC -"return_to_home"="Вернуться домой"; -"request_sent"="Ваш запрос был отправлен"; -"block"="Заблокировать"; -"unblock"="Разблокировать"; -"are_u_sure_block"="Вы уверены, что хотите заблокировать этого пользователя?"; -"are_u_sure_unblock"="Вы уверены что хотите разблокировать этого пользователя?"; -"are_u_sure_join_the_call"="Вы хотите присоединиться к вызову?"; -"send_message"="Отправить сообщение"; -"add_contact"="Добавить в контакты"; -"u_are_blocked"="Вы заблокированы"; - -// MARK: Schedule message -"schedule_message"="Запланировать сообщение"; -"schedule_message_to"="Сообщение на:"; -"schedule_voice_message"="Голосовое сообщение:"; -"schedule_text_message"="Текстовое сообщение:"; -"schedule_message_last"="сообщение:"; -"schedule_time_zone"="Для того чтобы был доставлен в следующую Тайм зону:"; -"schedule_date_time"="Для того чтобы был доставлен в следующую дату и время:"; -"schedule_save"="Сохранить"; -"schedule_success"="Сообщение было отложено с успехом."; -"schedule_error"="Выбранная дата и/или время некорректны. Пожалуйста, убедитесь что вы выбрали правильную дату и время. "; -"schedule_delete"="Удалить отложенное сообщение?"; -"schedule_cancel"="Отменить создание отложенного сообщения?"; -"voice_message"="Голосовое сообщение"; -"forwarded_message"="Пересланное сообщение"; -"local_time"="Локальное время"; - -// MARK: TimeZone selecter -"time_zone_selector"="Выбор Тайм зоны"; - -// MARK: Calendar -"mo"="По"; -"tu"="Вт"; -"we"="Ср"; -"th"="Чт"; -"fr"="Пт"; -"sa"="Сб"; -"su"="Вс"; - -// MARK: Time Date Picker -"date"="ДАТА"; -"time"="ВРЕМЯ"; - -"hour"="Час"; -"colon"=":"; -"min"="Мин"; - -"am"="AM"; -"pm"="PM"; - -// MARK: Errors -"no_internet_connection"="Нет интернет соединения"; - -// MARK: Custom Image Picker -"custom_image_picker_photo"="Фото"; -"custom_image_picker_video"="Видео"; -"custom_image_picker_retake"="Взять опять"; -"custom_image_picker_use_photo"="Использовать фото"; -"custom_image_picker_use_video"="Использовать видео"; - -// MARK: Other User -"other_user_header_name_label"="Первый Последний"; -"other_user_header_nickname_label"="ник пользователя"; -"other_user_table_view_ds_notifications"="Уведомления"; -"other_user_table_view_ds_unmute"="Включить звук"; -"other_user_table_view_ds_mute"="Отключить звук"; -"other_user_table_view_ds_storage"="Хранилище"; -"other_user_table_view_ds_language"="Языковые настройки"; -"other_user_table_view_ds_clear_chat_history"="Почистить историю чатов"; -"other_user_table_view_ds_banned"="забанен"; -"other_user_table_view_ds_unblock_user"="Разблокировать пользователя"; -"other_user_table_view_ds_block_user"="Заблокировать пользователя"; - -// MARK: Replies -"replies_header_replies"="Ответы"; - -// MARK: Message -"message_status_typing"="...печать"; -"message_new_messages"="Новые сообщения"; -"message_sending"="...отправка"; - -// MARK: Sending Status -"file"="файл"; -"voice message"="голосовое сообщение"; -"photo"="фото"; - -// MARK: Recording Status -"video"="видео"; -"recording"="...запись"; - -// MARK: Presence status -"active"="активный"; -"inactive"="неактивный"; -"members"="пользователи"; - -// MARK: Internet status -"waiting for network..."="ожидание сети..."; -"connecting..."="соединение..."; -"connected"="подключенный"; - -// MARK: Auth -"login_wrong_country_code"="Неправильный код страны"; -"login_choose_country"="Выбрать страну"; -"auth_something_went_wrong"="Что-то пошло не так"; -"auth_sms_code_is_wrong"="СМС код не верен"; -"auth_attempts_expired"="У вас нет больше попыток"; -"auth_attempts_removed"="Удалено"; - -// MARK: Tutorial -"tutorial_weclome_to"="Приглашаем в"; -"tutorial_mobile_communicator"="Мобильный коммуникатор"; -"tutorial_never_go_back"="никогда не возвращаться"; - -// MARK: Wheel items -"wheel_item_myself"="Я"; -"wheel_item_new"="Новый"; -"wheel_item_calls"="Звонки"; -"wheel_item_channels"="Каналы"; -"wheel_item_my_channels"="Мои каналы"; -"wheel_item_new_channel"="Новый канал"; -"wheel_item_contacts"="Контакты"; -"wheel_item_home"="Дом"; -"wheel_item_search"="Поиск"; -"wheel_item_actions"="Действия"; -"wheel_item_location"="Локация"; -"wheel_item_camera"="Камера"; -"wheel_item_media"="Медиа"; -"wheel_item_gallery"="Галерея"; -"wheel_item_go_to_gallery"="Перейти в галерию"; -"wheel_item_call"="Звонок"; -"wheel_item_group_call"="Групповой Звонок"; -"wheel_item_voiceCall"="Голосовой звонок"; -"wheel_item_voiceGroupCall"="Голосовой групповой"; -"wheel_item_videoCall"="Видео звонок"; -"wheel_item_videoGroupCall"="Видео групповой"; -"wheel_item_contact"="Контакт"; -"wheel_item_event"="Событие"; -"wheel_item_editProfile"="Изменить профиль"; -"wheel_item_my_qr_code"="Мой QR код"; -"wheel_item_photo"="Мое фото"; -"wheel_item_name"="Мое имя"; -"wheel_item_username"="Mое имя пользователя"; -"wheel_item_changeNumber"="Изменить номер"; -"wheel_item_starred"="Со звездочкой"; -"wheel_item_recents"="Недавние"; -"wheel_item_work"="Работа"; -"wheel_item_family"="Семья"; -"wheel_item_friends"="Друзья"; -"wheel_item_all"="Все"; -"wheel_item_chats"="Чаты"; -"wheel_item_newChat"="Новый час"; -"wheel_item_groups"="Группы"; -"wheel_item_newGroup"="Новая группа"; -"wheel_item_groupOptions"="Опции группы"; -"wheel_item_notifications"="Уведомления"; -"wheel_item_groupInfo"="Информация о группе"; -"wheel_item_groupPhoto"="Групповое фото"; -"wheel_item_groupName"="Имя группы"; -"wheel_item_done"="СДЕЛАНО"; -"wheel_item_myAlias"="Мой псевдоним"; -"wheel_item_group"="Группа"; -"wheel_item_newContact"="Новый контакт"; -"wheel_item_history"="История"; -"wheel_item_byUsername"="По имени пользователя"; -"wheel_item_byNumber"="По номеру"; -"wheel_item_byQRCode"="По QR коду"; -"wheel_item_byPassword"="По паролю"; -"wheel_item_byContacts"="По контакту"; -"wheel_item_options"="Настройки"; -"wheel_item_logOut"="Выйти"; -"wheel_item_about"="О"; -"wheel_item_deleteAccount"="Удалить счет"; -"wheel_item_send"="Отправить локацию"; -"wheel_item_language"="Язык"; -"wheel_position"="Позиция колеса"; -"wheel_build_number"="Номер сборки"; -"wheel_support"="Поддержка"; -"wheel_security" = "Безопасность"; -"wheel_theme"="Тема"; -"wheel_privacy"="Приватность"; -"wheel_invite_friends"="Пригласить Друзей"; -"wheel_item_transfer" = "Трансфер"; -"wheel_data_and_storage" = "Хранилище"; - -// MARK: Main -"main_undefined"="Неопределенный"; - -// MARK: Profile -"profile_balance"="Баланс"; -"star_added"="Добавлен:"; -"profile_contact_cell_added"="Добавлен"; -"action_cell_profile_starred_messages"="Сообщение со звездочкой"; -"profile_go_to_chats"="Идти в чаты"; -"profile_go_to_groups"="Идти в группы"; -"profile_go_to_history"="Идти в историю"; -"profile_go_to_starred_messages"="Идти в сообщения со звездочкой"; - -// MARK: Edit Profile -"edit_profile_hint"="Хотите чтоб вам позвонили?"; - -// MARK: Call -"voice_call_status"="Телефонный звонок"; -"video_call_status"="Видео звонок"; -"voice_call_the_feature_currently_unavailable"="Эта функция в данный момент недоступна"; - -// MARK: Settings group -"settings_group_no_info_about_me"="Нет информации о мне"; -"settings_group_no_phone_number"="Нет номера телефона"; -"settings_group_group_name"="Имя группы*:"; -"settings_group_my_alias_in_group"="мой псевдоним группы:"; -"settings_group_group_rules"="Правила группы"; -"settings_group_notifications"="Сообщения"; -"settings_group_storage"="Хранилище"; -"settings_group_language"="Языковые настройки"; -"settings_group_add_participants"="Добавить Участника"; -"settings_group_group_participants"="Участники группы"; -"settings_group_delete_participants"="Удалить участника"; -"settings_group_admins"="Админы группы"; -"settings_group_clear_chat_history"="Почистить историю чата"; -"settings_group_delete_and_leave"="Удалить и покинуть"; - -// MARK: Share extensions -"share_extensions_sharing"="делимся..."; - -// MARK: Language Settings -"language_settings_settings"="НАСТРОЙКИ"; -"no_translation_error"="Извините, у нас нет перевода для"; - -// MARK: Profile Section -"Unread Messages"="Непрочитанные сообщения"; -"Unread Group Messages"="Непрочитанные групповые сообщения"; -"Contact Requests"="Запросы в контакты"; -"Scheduled Messages"="Отложенные сообщения"; -"Actions"="Действия"; -"Starred Messages"="Сообщения со звездочкой"; - -// MARK: Wheel Position Picker -"wheel position title"="Позиция колеса"; -"choose wheel position"="Выбрать позицию колеса"; -"wheel current position"="текущая позиция"; -"wheel left hand"="Левая рука"; -"wheel right hand"="Правая рука"; -"wheel position picker alert title"="Использовать эту позицию колеса"; - - -// MARK: Support -"support screen title"="ПОДДЕРЖКА"; -"support privacy policy cell"="Полис приватности"; -"support faq cell"="FAQ"; -"support get help title"="ПОЛУЧИТЬ ПОМОЩЬ"; - -"support privacy policy title"="ПОЛИС ПРИВАТНОСТИ"; -"support faq title"="FAQ"; - -// MARK: Theme Picker -"theme picker title"="ТЕМА"; -"choose theme title"="Выбери тему для себя"; -"theme current name"="Текущая тема"; -"theme dark name"="Черный в черном"; -"theme light name"="Дорога в Рай"; -"theme picker alert title"="Использовать эту тему"; -"theme cell subtitle text"="МОБИЛЬНЫЙ КОММУНИКАТОР"; - -"theme picker alert demo message"="Эта тема будет применена только к этому экрану и поменяется когда вы покинете экран"; - -// MARK: Build Number -"build number title"="НОМЕР СБОРКИ"; -"build number version"="NYNJA для iOS v%@"; - -//MARK: Change Number -"change_number_title" = "ИЗМЕНИТЬ НОМЕР"; -"change_number_number" = "Текущий номер:"; -"change_number_text1" = "Вы можете изменить свой NYNJA номер здесь. Ваш счет и все ваши облачные данные, сообщения, медиа, контакты и т.д. будут перемещены на новый адрес."; -"change_number_text2" = "Все ваши NYNJA контакты остануться связанными с вашим счетом. Единственная вещь которая измениться это ваш телефонный номер NYNJA."; -"change_number_button_title" = "ИЗМЕНИТЬ НОМЕР"; -"change_number_send_sms" = "Мы отправим СМС с кодом подтверждения на ваш номый номер."; -"change_number_keep_number" = "Оставить номер."; -"change_number_country" = "Страна:"; -"change_number_phone_number" = "Телефонный номер:"; -"change_number_code" = "Код"; -"change_number_sent_a_code" = "Мы отправили код на ваш телефон."; -"change_number_should_receive" = "Вы должны получить его в течении %d секунд."; -"change_number_havent_received" = "Не получили еще код?"; -"change_number_next" = "СЛЕДУЮЩИЙ"; -"change_number_invalid_phone" = "Неправильный номер. Пожалуйста попытайтесь еще."; -"change_number_invalid_code" = "Вы ввели неправильный код. Пожалуйста попытайтесь еще."; -"change_number_new_number" = "Новый номер:"; - -//MARK: Security -"security_title" = "БЕЗОПАСНОСТЬ"; -"security_current_session" = "Текущая сессия"; -"security_active_sessions" = "Активные сессии"; -"security_terminate_all_other_sessions" = "Остановить остальные сессии"; -"security_logs_out_all_devices" = "Выйти из всех устройств за исключением этого."; -"security_tap_on_a_session" = "Нажмите на сессию чтобы остановить."; -"security_terminate" = "Остановить эту сессию?"; -"security_you_sure" = "Вы уверены что хотите остановить остальные сессиии?"; -"security_waiting_for_network" = "Ожидание соединения..."; -"security_no_internet_connection" = "Пожалуйста проверьте интернет соединение и попробуйте еще раз."; -"security_nynja" = "NYNJA"; -"security_undefined" = "Не определен"; -"security_no" = "Нет"; -"security_yes" = "Да"; -"security_active" = "активный"; -"security_no_data" = "Нет данных"; - - -// MARK: - New Channel -"channel_new" = "Новый канал"; -"channel_name" = "Имя канала*"; -"channel_link" = "Ссылка на канал*"; -"channel_description" = "Описание"; -"channel_max_length_warning" = "Максимум %d символа"; -"channel_min_length_warning" = "Как минимум %d символа"; -"channel_link_is_already_in_use" = "Эта ссылка занята"; -"channel_link_is_available" = "Эта ссылка свободна"; -"channel_cancel_creation" = "Cancel channel creation?"; -"channel_symbols_are_not_allowed" = "Недоступные символы"; - - -// MARK: - Subscribers -"channel_subscribers_title" = "Подписчики"; -"channel_no_subscribers_selected" = "подписчики не выбраны"; -"channel_single_subscriber" = "1 подписчик"; -"channel_plural_subscribers" = "%d подписчиков"; -"channel_all_subscribers" = "Все"; -"channel_no_subscribers_to_select" = "Извините, нет выбраных подписчиков"; -"channel_no_search_result" = "Извините, не удалось найти"; - - -// MARK: - Channel List & My Channels -"channel_list" = "Список каналов"; -"channel_my_channels" = "Мои каналы"; -"channel_list_is_empty" = "Список каналов пуст. Вы можете создать собственный канал."; -"channel_list_create_channel" = "Создать канал"; -"channel_list_no_search_result" = "Извините, не удалось найти"; - -//MARK: Stickers -"sticker" = "Стикер"; -"stickers_recently_used" = "Недавно используемые"; -"stickers_recently_used_empty" = "Нет отправленных\nстикеров"; - -//MARK: Input Bar -"input_bar_placeholder_sticker_search" = "Поиск стикеров"; -"input_bar_placeholder_message" = "Сообщение..."; - -//MARK: Sticker Search Result -"sticker_search_result_not_found" = "Ничего не найдено...\nПопробуйте еще раз"; - -//MARK: Calling -"remove_participant_from_call"="Удалить из вызова"; -"question_end_call"="Кого вы хотите, чтобы завершить вызов для?"; -"end_call_for_me"="Меня"; -"end_call_for_all"="Все"; -"call_info_banner_members" = "%d участников"; -"Create_conference_call_failed"="Не удалось выполнить конференцию"; -"Add_conference_member_failed" = "Невозможно добавить участника конференции"; -"outgoing_call" = "Исходящий звонок"; -"incoming_call" = "Входящий звонок"; - -//MARK: Wallet -"wallet_recepient" = "Получатель"; -"wallet_balance" = "Баланс"; -"wallet_amount" = "Сумма"; -"wallet_alert" = "На балансе недостаточно средств для осуществления транзакции"; -"wallet_send" = "Отправить средства"; -"wallet_header" = "Кошелек"; -"wallet_created" = "Кошелек успешно создан"; -"wallet_add_title" = "Создать\nКошелек"; -"wallet_transfer_deletion_info" = "Сообщение с информацей о трансфере будет удалено только для вас в истории чата, но это сообщение будет по-прежнему доступно в истории передачи"; - -// MARK: Data and Storage -"data_and_storage" = "Хранилище"; -"automatic_media_download" = "Автоматическая загрузка данных"; -"when_using_mobile_data" = "Мобильный интернет"; -"when_using_wifi" = "WI-FI"; -"when_on_roaming" = "Роуминг"; -"automatic_media_download_Hint" = "NYNJA сохраняет все ваши данные в облаке и вы можете скачать заново их если вам понадобится"; -"local_storage_usage" = "Сохраненных данных"; -"local_storage_usage_hint" = "NYNJA сохраняет загруженные данные в локальном хранилище, что бы вы могли получить к ним быстрый доступ"; -"clear_data_storage" = "Очистить локальное хранилище?"; -"no_media" = "Нет данных"; -"save" = "Сохранить "; - -//MARK: DataAndStorageOptions -"photos" = "Photos"; -"voice_messages" = "Voice Messages"; -"video_messages" = "Video Messages"; -"videos" = "Videos"; -"files" = "Files"; -"music" = "Music"; -"gifs" = "GIFs"; - -//MARK: Marketplace - -//**Submenu titles < CircleMenuItemType case naming are constrained to this values > -"marketplace" = "Marketplace"; -"freelance" = "Freelance"; -"accessMarketplace" = "Access marketplace"; -"virtualGoods" = "Virtual goods"; -"stickers" = "Stickers"; -"mediaContent" = "Media Content"; -"groupsAndChannels" = "Groups and Channels"; -"bots" = "Bots"; -"apps" = "Apps"; -"interpretation" = "Interpretation"; -"nynjaSupport" = "NYNJA Support"; -"design" = "Design"; -//Submenu titles** - -"interpretation" = "Interpretation"; - -"interpretation_type_general" = "General"; -"interpretation_type_technology" = "Technology"; -"interpretation_type_legal" = "Legal"; -"interpretation_type_medical" = "Medical"; - -"interpretation_type_description_general" = "NYNJA users who speak multiple languages will help you right now to interprate your call in real time. Use this for general language that most people would know."; -"interpretation_type_description_technology" = "Choose this for interpretation that require technology specific terminology and we will find someone with this capability."; -"interpretation_type_description_legal" = "Perfect for legal topics and other interpretation work typically done by a lawyer or paralegal."; -"interpretation_type_description_medical" = "Specialists with a background in medical interpretation will only be assigned these interpretations."; - -"interpretation_time" = "Approximate interpretation time"; -"lang_for_interpretation" = "Language for interpretation"; -"from" = "From:"; -"to" = "To:"; -"interpretation_type" = "Interpretation type"; -"total_price" = "Total price"; -"search_interpreter" = "Search Interpreter"; -"price_per_minute" = "Price per minute"; - -"asigning_interpreter_description" = "NYNJA is finding the perfect interpreter for you. Go ahead and continue using the app and we will let you know when they’re ready to start the call."; -"coming_soon" = "Coming soon"; -"assigning_interpreter" = "Assigning Interpreter"; - -"enter_interpretation_time" = "Enter interpretation time (1-300)"; -"interpretation_time_out_of_range" = "Time is ranged from 1 to 300"; - -"interpretation_language_not_selected" = "Please, choose the language you want to interpret from."; -"interpretation_languages_are_equal" = "Please, choose different languages."; diff --git a/Nynja/Resources/ru.lproj/StatusCodes.strings b/Nynja/Resources/ru.lproj/StatusCodes.strings deleted file mode 100644 index 269e78deb..000000000 --- a/Nynja/Resources/ru.lproj/StatusCodes.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* - StatusCodes.strings - Nynja - - Created by Volodymyr Hryhoriev on 7/6/18. - Copyright © 2018 TecSynt Solutions. All rights reserved. -*/ - - -// MARK: - Room - -"RCHIE1" = "Канал уже существует"; - - -// MARK: - Link - -"LNKIE1" = "Недоступные символы"; -"LNKIE2" = "Эта ссылка занята"; -- GitLab From 46506078cca77ab7201731311b17539b5d774990 Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Mon, 13 Aug 2018 13:53:41 +0300 Subject: [PATCH 32/32] merge conflicts --- .../CallInProgress/WireFrame/CallInProgressWireframe.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift index 3232cb52f..d1303d33b 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift @@ -40,7 +40,6 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: true) - interactor.setupDelegate() } func presentDialInCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, contact: Contact?, call: NYNCall? = nil, main: MainWireFrame?) { @@ -67,7 +66,6 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: true) - interactor.setupDelegate() } func presentCreateGroupCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, main: MainWireFrame?, call: NYNCall) { @@ -97,7 +95,6 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: true) - interactor.setupDelegate() } func messageActionWith(room: Room, isVideo: Bool) { -- GitLab