From 93d420758ff753a0e453bc379d7f812ceb2910e8 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 3 Jul 2020 20:42:44 -0400 Subject: [PATCH] feat: device pairing --- src/app/profile/core.nim | 3 + src/app/profile/view.nim | 29 ++++++-- src/app/profile/views/device_list.nim | 69 +++++++++++++++++++ src/signals/core.nim | 2 +- src/signals/messages.nim | 6 +- src/signals/types.nim | 3 +- src/status/devices.nim | 22 +++++- src/status/libstatus/installations.nim | 16 ++++- src/status/profile/devices.nim | 14 ++++ .../Profile/Sections/DevicesContainer.qml | 66 +++++++++++++++++- 10 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 src/app/profile/views/device_list.nim create mode 100644 src/status/profile/devices.nim diff --git a/src/app/profile/core.nim b/src/app/profile/core.nim index 8ff6724f0c..0e4412cbf6 100644 --- a/src/app/profile/core.nim +++ b/src/app/profile/core.nim @@ -40,6 +40,7 @@ proc init*(self: ProfileController, account: Account) = profile.appearance = appearance profile.id = pubKey + self.view.addDevices(devices.getAllDevices()) self.view.setDeviceSetup(devices.isDeviceSetup()) self.view.setNewProfile(profile) self.view.setMnemonic(mnemonic) @@ -67,3 +68,5 @@ method onSignal(self: ProfileController, data: Signal) = # TODO: view should react to model changes self.status.chat.updateContacts(msgData.contacts) self.view.updateContactList(msgData.contacts) + if msgData.installations.len > 0: + self.view.addDevices(msgData.installations) diff --git a/src/app/profile/view.nim b/src/app/profile/view.nim index 3127be015a..db2f350e60 100644 --- a/src/app/profile/view.nim +++ b/src/app/profile/view.nim @@ -1,11 +1,11 @@ import NimQml, sequtils -import views/[mailservers_list, contact_list, profile_info] -import ../../status/profile/[mailserver, profile] +import views/[mailservers_list, contact_list, profile_info, device_list] +import ../../status/profile/[mailserver, profile, devices] import ../../status/profile as status_profile import ../../status/contacts as status_contacts import ../../status/accounts as status_accounts import ../../status/status -import ../../status/devices +import ../../status/devices as status_devices import ../../status/chat/chat import qrcode/qrcode @@ -14,6 +14,7 @@ QtObject: profile*: ProfileInfoView mailserversList*: MailServersList contactList*: ContactList + deviceList*: DeviceList mnemonic: string status*: Status isDeviceSetup: bool @@ -24,6 +25,7 @@ QtObject: proc delete*(self: ProfileView) = if not self.mailserversList.isNil: self.mailserversList.delete if not self.contactList.isNil: self.contactList.delete + if not self.deviceList.isNil: self.deviceList.delete if not self.profile.isNil: self.profile.delete self.QObject.delete @@ -33,6 +35,7 @@ QtObject: result.profile = newProfileInfoView() result.mailserversList = newMailServersList() result.contactList = newContactList() + result.deviceList = newDeviceList() result.mnemonic = "" result.status = status result.isDeviceSetup = false @@ -123,7 +126,23 @@ QtObject: self.setDeviceSetup(true) proc syncAllDevices*(self: ProfileView) {.slot.} = - devices.syncAllDevices() + status_devices.syncAllDevices() proc advertiseDevice*(self: ProfileView) {.slot.} = - devices.advertise() + status_devices.advertise() + + proc addDevices*(self: ProfileView, devices: seq[Installation]) = + for dev in devices: + self.deviceList.addDeviceToList(dev) + + proc getDeviceList(self: ProfileView): QVariant {.slot.} = + return newQVariant(self.deviceList) + + QtProperty[QVariant] deviceList: + read = getDeviceList + + proc enableInstallation*(self: ProfileView, installationId: string, enable: bool) {.slot.} = + if enable: + status_devices.enable(installationId) + else: + status_devices.disable(installationId) \ No newline at end of file diff --git a/src/app/profile/views/device_list.nim b/src/app/profile/views/device_list.nim new file mode 100644 index 0000000000..7c94691ee3 --- /dev/null +++ b/src/app/profile/views/device_list.nim @@ -0,0 +1,69 @@ +import NimQml +import Tables +import ../../../status/devices as status_devices +import ../../../status/profile/devices + +type + DeviceRoles {.pure.} = enum + Name = UserRole + 1, + InstallationId = UserRole + 2 + IsUserDevice = UserRole + 3 + IsEnabled = UserRole + 4 + +QtObject: + type DeviceList* = ref object of QAbstractListModel + devices*: seq[Installation] + + proc setup(self: DeviceList) = self.QAbstractListModel.setup + + proc delete(self: DeviceList) = + self.devices = @[] + self.QAbstractListModel.delete + + proc newDeviceList*(): DeviceList = + new(result, delete) + result.devices = @[] + result.setup + + method rowCount(self: DeviceList, index: QModelIndex = nil): int = + return self.devices.len + + method data(self: DeviceList, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.devices.len: + return + let installation = self.devices[index.row] + case role.DeviceRoles: + of DeviceRoles.Name: result = newQVariant(installation.name) + of DeviceRoles.InstallationId: result = newQVariant(installation.installationId) + of DeviceRoles.IsUserDevice: result = newQVariant(installation.isUserDevice) + of DeviceRoles.IsEnabled: result = newQVariant(installation.enabled) + + method roleNames(self: DeviceList): Table[int, string] = + { + DeviceRoles.Name.int:"name", + DeviceRoles.InstallationId.int:"installationId", + DeviceRoles.IsUserDevice.int:"isUserDevice", + DeviceRoles.IsEnabled.int:"isEnabled" + }.toTable + + proc addDeviceToList*(self: DeviceList, installation: Installation) = + var i = 0; + var found = false + for dev in self.devices: + if dev.installationId == installation.installationId: + found = true + break + i = i + 1 + + if found: + let topLeft = self.createIndex(i, 0, nil) + let bottomRight = self.createIndex(i, 0, nil) + self.devices[i].name = installation.name + self.devices[i].enabled = installation.enabled + self.dataChanged(topLeft, bottomRight, @[DeviceRoles.Name.int, DeviceRoles.IsEnabled.int]) + else: + self.beginInsertRows(newQModelIndex(), self.devices.len, self.devices.len) + self.devices.add(installation) + self.endInsertRows() diff --git a/src/signals/core.nim b/src/signals/core.nim index 6408cf3403..a0a12706da 100644 --- a/src/signals/core.nim +++ b/src/signals/core.nim @@ -53,7 +53,7 @@ QtObject: return var signal: Signal = Signal(signalType: signalType) - + case signalType: of SignalType.Message: signal = messages.fromEvent(jsonSignal) diff --git a/src/signals/messages.nim b/src/signals/messages.nim index 162976569f..e9ec98bcfe 100644 --- a/src/signals/messages.nim +++ b/src/signals/messages.nim @@ -1,7 +1,7 @@ import json, random import ../status/libstatus/accounts as status_accounts import ../status/chat/[chat, message] -import ../status/profile/profile +import ../status/profile/[profile, devices] import types proc toMessage*(jsonMsg: JsonNode): Message @@ -25,6 +25,10 @@ proc fromEvent*(event: JsonNode): Signal = for jsonChat in event["event"]["chats"]: signal.chats.add(jsonChat.toChat) + if event["event"]{"installations"} != nil: + for jsonInstallation in event["event"]["installations"]: + signal.installations.add(jsonInstallation.toInstallation) + result = signal proc toChatMember*(jsonMember: JsonNode): ChatMember = diff --git a/src/signals/types.nim b/src/signals/types.nim index d22d41f60f..680835ff03 100644 --- a/src/signals/types.nim +++ b/src/signals/types.nim @@ -1,7 +1,7 @@ import json, chronicles, json_serialization, tables import ../status/libstatus/types import ../status/chat/[chat, message] -import ../status/profile/profile +import ../status/profile/[profile, devices] type SignalSubscriber* = ref object of RootObj @@ -28,6 +28,7 @@ type MessageSignal* = ref object of Signal messages*: seq[Message] chats*: seq[Chat] contacts*: seq[Profile] + installations*: seq[Installation] type Filter* = object chatId*: string diff --git a/src/status/devices.nim b/src/status/devices.nim index 2a55487625..f2bb0dd7bd 100644 --- a/src/status/devices.nim +++ b/src/status/devices.nim @@ -1,6 +1,8 @@ import system import libstatus/settings +import libstatus/types import libstatus/installations +import profile/devices import json proc setDeviceName*(name: string) = @@ -8,7 +10,7 @@ proc setDeviceName*(name: string) = proc isDeviceSetup*():bool = let installationId = getSetting[string]("installation-id", "", true) - let responseResult = parseJSON($getOurInstallations())["result"] + let responseResult = getOurInstallations() if responseResult.kind == JNull: return false for installation in responseResult: @@ -21,3 +23,21 @@ proc syncAllDevices*() = proc advertise*() = discard sendPairInstallation() + +proc getAllDevices*():seq[Installation] = + let responseResult = getOurInstallations() + let installationId = getSetting[string]("installation-id", "", true) + result = @[] + if responseResult.kind != JNull: + for inst in responseResult: + var device = inst.toInstallation + if device.installationId == installationId: + device.isUserDevice = true + result.add(device) + +proc enable*(installationId: string) = + # TODO handle errors + discard enableInstallation(installationId) + +proc disable*(installationId: string) = + discard disableInstallation(installationId) diff --git a/src/status/libstatus/installations.nim b/src/status/libstatus/installations.nim index bef31821bf..e82b64b369 100644 --- a/src/status/libstatus/installations.nim +++ b/src/status/libstatus/installations.nim @@ -1,11 +1,18 @@ import json, core, utils, system +var installations: JsonNode = %*{} +var dirty: bool = true + proc setInstallationMetadata*(installationId: string, deviceName: string, deviceType: string): string = result = callPrivateRPC("setInstallationMetadata".prefix, %* [installationId, {"name": deviceName, "deviceType": deviceType}]) # TODO: handle errors -proc getOurInstallations*(): string = - result = callPrivateRPC("getOurInstallations".prefix, %* []) +proc getOurInstallations*(useCached: bool = true): JsonNode = + if useCached and not dirty: + return installations + installations = callPrivateRPC("getOurInstallations".prefix, %* []).parseJSON()["result"] + dirty = false + result = installations proc syncDevices*(): string = # These are not being used at the moment @@ -16,3 +23,8 @@ proc syncDevices*(): string = proc sendPairInstallation*(): string = result = callPrivateRPC("sendPairInstallation".prefix) +proc enableInstallation*(installationId: string): string = + result = callPrivateRPC("enableInstallation".prefix, %* [installationId]) + +proc disableInstallation*(installationId: string): string = + result = callPrivateRPC("disableInstallation".prefix, %* [installationId]) diff --git a/src/status/profile/devices.nim b/src/status/profile/devices.nim new file mode 100644 index 0000000000..ec2b56c670 --- /dev/null +++ b/src/status/profile/devices.nim @@ -0,0 +1,14 @@ +import json, hashes + +type Installation* = ref object + installationId*: string + name*: string + deviceType*: string + enabled*: bool + isUserDevice*: bool + +proc toInstallation*(jsonInstallation: JsonNode): Installation = + result = Installation(installationid: jsonInstallation{"id"}.getStr, enabled: jsonInstallation{"enabled"}.getBool, name: "", deviceType: "", isUserDevice: false) + if jsonInstallation["metadata"].kind != JNull: + result.name = jsonInstallation["metadata"]["name"].getStr + result.deviceType = jsonInstallation["metadata"]["deviceType"].getStr diff --git a/ui/app/AppLayouts/Profile/Sections/DevicesContainer.qml b/ui/app/AppLayouts/Profile/Sections/DevicesContainer.qml index 08ced695fa..679032006b 100644 --- a/ui/app/AppLayouts/Profile/Sections/DevicesContainer.qml +++ b/ui/app/AppLayouts/Profile/Sections/DevicesContainer.qml @@ -52,7 +52,6 @@ Item { } StyledButton { - visible: !selectChatMembers anchors.top: deviceNameTxt.bottom anchors.topMargin: 10 anchors.right: deviceNameTxt.right @@ -64,6 +63,7 @@ Item { } Item { + id: advertiseDeviceItem anchors.left: syncContainer.left anchors.leftMargin: Style.current.padding anchors.top: sectionTitle.bottom @@ -71,6 +71,7 @@ Item { anchors.right: syncContainer.right anchors.rightMargin: Style.current.padding visible: profileModel.deviceSetup + height: childrenRect.height Rectangle { id: advertiseDevice @@ -133,6 +134,69 @@ Item { } } + + Item { + id: deviceListItem + anchors.left: syncContainer.left + anchors.leftMargin: Style.current.padding + anchors.top: advertiseDeviceItem.bottom + anchors.topMargin: Style.current.padding * 2 + anchors.bottom: syncAllBtn.top + anchors.bottomMargin: Style.current.padding + anchors.right: syncContainer.right + anchors.rightMargin: Style.current.padding + visible: profileModel.deviceSetup + + + StyledText { + id: deviceListLbl + text: qsTr("Paired devices") + font.pixelSize: 16 + font.weight: Font.Bold + } + + ListView { + id: listView + anchors.bottom: parent.bottom + anchors.top: deviceListLbl.bottom + anchors.topMargin: Style.current.padding + spacing: 5 + anchors.right: syncContainer.right + anchors.left: syncContainer.left + delegate: Item { + height: childrenRect.height + SVGImage { + id: enabledIcon + source: "/app/img/" + (devicePairedSwitch.checked ? "messageActive.svg" : "message.svg") + height: 24 + width: 24 + } + StyledText { + id: deviceItemLbl + text: { + let deviceId = model.installationId.split("-")[0].substr(0, 5) + let labelText = `${model.name || qsTr("No info")} (${model.isUserDevice ? qsTr("you") + ", ": ""}${deviceId})`; + return labelText; + } + elide: Text.ElideRight + font.pixelSize: 14 + anchors.left: enabledIcon.right + anchors.leftMargin: Style.current.padding + } + Switch { + id: devicePairedSwitch + visible: !model.isUserDevice + checked: model.isEnabled + anchors.left: deviceItemLbl.right + anchors.leftMargin: Style.current.padding + anchors.top: deviceItemLbl.top + onClicked: profileModel.enableInstallation(model.installationId, devicePairedSwitch) + } + } + model: profileModel.deviceList + } + } + StyledButton { id: syncAllBtn anchors.bottom: syncContainer.bottom