Migrate to new peer authorization levels, add new peer approval
the build was successful Details

workflow, add blocked peers to contact list in seperate section
This commit is contained in:
Dan Ballard 2020-06-23 16:42:51 -07:00
parent b8ffa85fac
commit 315c66009e
23 changed files with 385 additions and 209 deletions

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#BCB6BC;}
</style>
<path class="st0" d="M0,0h24v24H0V0z"/>
<g>
<path class="st0" d="M12,5c-1.7,0-3,1.3-3,3s1.3,3,3,3c0.1,0,0.1,0,0.1,0l2.7-4C14.4,5.8,13.3,5,12,5z"/>
<path class="st0" d="M12,19.2c2.5,0,4.7-1.3,6-3.2c0-1.7-3.1-2.8-5.2-3l-3.8,5.6C10,19,11,19.2,12,19.2z"/>
<path class="st0" d="M6,16c0.4,0.7,1,1.2,1.6,1.7l3.2-4.7C8.7,13.3,6,14.3,6,16z"/>
<path class="st1" d="M6,16c0-1.6,2.7-2.7,4.8-3l1.4-2c-0.1,0-0.1,0-0.1,0c-1.7,0-3-1.3-3-3s1.3-3,3-3c1.3,0,2.4,0.8,2.8,2l2.4-3.5
C15.7,2.5,13.9,2,12,2C6.5,2,2,6.5,2,12c0,3.3,1.6,6.2,4.1,8l1.6-2.3C7,17.2,6.4,16.6,6,16z"/>
<path class="st1" d="M18.5,4.4L12.8,13c2.1,0.2,5.1,1.3,5.2,3c-1.3,1.9-3.5,3.2-6,3.2c-1,0-2-0.2-2.9-0.6l-1.6,2.4
C8.8,21.6,10.4,22,12,22c5.5,0,10-4.5,10-10C22,9,20.7,6.3,18.5,4.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

7
go.mod
View File

@ -3,12 +3,13 @@ module cwtch.im/ui
go 1.12
require (
cwtch.im/cwtch v0.3.11
cwtch.im/cwtch v0.3.15
git.openprivacy.ca/openprivacy/connectivity v1.1.4
git.openprivacy.ca/openprivacy/log v1.0.0
git.openprivacy.ca/openprivacy/log v1.0.1
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df
golang.org/x/crypto v0.0.0-20200420104511-884d27f42877 // indirect
)

6
go.sum
View File

@ -2,6 +2,10 @@ cwtch.im/cwtch v0.3.10 h1:akrIwsc1KnLbT3K6ZIFkhmA7kI62L03EWna7Ul1vszU=
cwtch.im/cwtch v0.3.10/go.mod h1:tmYeI2v0IEeBMbqzhcndXWZ2oyflhK4Afcf27+49rKU=
cwtch.im/cwtch v0.3.11 h1:2+W2w9HDQowKwEGx4oRLywmn0NzQ0Sg9JEyBdR/V1mA=
cwtch.im/cwtch v0.3.11/go.mod h1:PnMJb9CyzdrdbYjmL99pl6Nu34s6+lmeENVnGaY0hzk=
cwtch.im/cwtch v0.3.14 h1:XL8UbCUyIosdFTD5nSlpvhbQQGFLjvFmd81/SmfBSP8=
cwtch.im/cwtch v0.3.14/go.mod h1:wDmgxWBWak/xvZ5GurdYNOJ8b8eha1MwVdiWsCS/pwI=
cwtch.im/cwtch v0.3.15 h1:Z7fFREwXY728q2YmmwgHL357zAobrsWJ2oPkkGwzvo0=
cwtch.im/cwtch v0.3.15/go.mod h1:iI9q4C3njHFBYQkNEbzMdK6QWPS0Vbkc0FigRHZNTvM=
cwtch.im/tapir v0.1.15 h1:XSCWOvjmNkzMT2IceFgTBXWGKtYfr3a8o+La1s10OhE=
cwtch.im/tapir v0.1.15/go.mod h1:HzezugpEx+nZ3LdyDsl0w6n45IJYnOt8uqldkLWmaqs=
cwtch.im/tapir v0.1.17 h1:2jVZUe1a88tMI4aJPvRTO4Id3NN3PsM62cT5lntEChk=
@ -23,6 +27,8 @@ git.openprivacy.ca/openprivacy/libricochet-go v1.0.13 h1:Z86uL9K47onznY1wP1P/wWf
git.openprivacy.ca/openprivacy/libricochet-go v1.0.13/go.mod h1:ZUuX1SOrgV4K18IEcp0hQJNPKszRr2oGb3UeK2iYe5U=
git.openprivacy.ca/openprivacy/log v1.0.0 h1:Rvqm1weUdR4AOnJ79b1upHCc9vC/QF1rhSD2Um7sr1Y=
git.openprivacy.ca/openprivacy/log v1.0.0/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
git.openprivacy.ca/openprivacy/log v1.0.1 h1:NWV5oBTatvlSzUE6wtB+UQCulgyMOtm4BXGd34evMys=
git.openprivacy.ca/openprivacy/log v1.0.1/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=

View File

@ -5,6 +5,7 @@ const SchemaVersion = "schemaVersion"
const Name = "name"
const LastRead = "last-read"
const Picture = "picture"
const ShowBlocked = "show-blocked"
const ProfileTypeV1DefaultPassword = "v1-defaultPassword"
const ProfileTypeV1Password = "v1-userPassword"

View File

@ -3,6 +3,7 @@ package handlers
import (
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
peerC "cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
@ -77,13 +78,12 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) {
case event.PeerStateChange:
cxnState := connections.ConnectionStateToType[e.Data[event.ConnectionState]]
// if it's not in the.PeerHandler it's new. Only add once Authed
if contact := peer.GetContact(e.Data[event.RemotePeer]); contact == nil {
// Contact does not exist, we will add them but we won't know who they are until they are authenticated
// So if we get any other state from an unknown contact we do nothing
// (the next exists check will fail)
if cxnState == connections.AUTHENTICATED {
peer.AddContact(e.Data[event.RemotePeer], e.Data[event.RemotePeer], false)
// New connection established
if cxnState == connections.AUTHENTICATED {
// if it's not in the peer it's new
if contact := peer.GetContact(e.Data[event.RemotePeer]); contact == nil {
// Contact does not exist, we will add them
peer.AddContact(e.Data[event.RemotePeer], e.Data[event.RemotePeer], model.AuthUnknown)
uiManager.AddContact(e.Data[event.RemotePeer])
}
}

View File

@ -3,6 +3,7 @@ package ui
import (
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/ui/go/constants"
@ -52,14 +53,13 @@ type GrandCentralDispatcher struct {
_ func(failed bool) `signal:"ChangePasswordResponse"`
// contact list stuff
_ func(handle, displayName, image string, badge, status int, blocked bool, loading bool, lastMsgTime int) `signal:"AddContact"`
_ func(handle, displayName string) `signal:"UpdateContactDisplayName"`
_ func(handle, image string) `signal:"UpdateContactPicture"`
_ func(handle string, status int, loading bool) `signal:"UpdateContactStatus"`
_ func(handle string, blocked bool) `signal:"UpdateContactBlocked"`
_ func(handle string) `signal:"IncContactUnreadCount"`
_ func(handle string) `signal:"RemoveContact"`
_ func(handle, key, value string) `signal:"UpdateContactAttribute"`
_ func(handle, displayName, image string, badge, status int, authorization string, loading bool, lastMsgTime int) `signal:"AddContact"`
_ func(handle, displayName string) `signal:"UpdateContactDisplayName"`
_ func(handle, image string) `signal:"UpdateContactPicture"`
_ func(handle string, status int, loading bool) `signal:"UpdateContactStatus"`
_ func(handle string) `signal:"IncContactUnreadCount"`
_ func(handle string) `signal:"RemoveContact"`
_ func(handle, key, value string) `signal:"UpdateContactAttribute"`
// messages pane stuff
_ func(handle, from, displayName, message, image string, mID string, fromMe bool, ts string, ackd bool, error bool) `signal:"AppendMessage"`
@ -72,14 +72,14 @@ type GrandCentralDispatcher struct {
_ func(loading bool) `signal:"SetLoadingState"`
// profile-area stuff
_ func(name, onion, image, tag string) `signal:"UpdateMyProfile"`
_ func(status int) `signal:"TorStatus"`
_ func(name, onion, image, tag, showBlocked string) `signal:"UpdateMyProfile"`
_ func(status int) `signal:"TorStatus"`
// settings helpers
_ func(str string) `signal:"InvokePopup"`
_ func(locale string, zoom float32, theme string) `signal:"SupplySettings"`
_ func(groupID, name, server, invitation string, accepted bool, addrbooknames, addrbookaddrs []string) `signal:"SupplyGroupSettings"`
_ func(onion, nick string, blocked bool) `signal:"SupplyPeerSettings"`
_ func(onion, nick string, authorization string) `signal:"SupplyPeerSettings"`
// signals emitted from the ui (written in go, below)
// ui
@ -92,10 +92,10 @@ type GrandCentralDispatcher struct {
_ func() `signal:"reloadProfileList,auto"`
_ func(onion string) `signal:"deleteProfile,auto"`
_ func(onion, currentPassword, newPassword string, defaultPass bool) `signal:"changePassword,auto""`
_ func(key, val string) `signal:"storeSetting,auto"`
// operating a profile
_ func(message string, mid string) `signal:"sendMessage,auto"`
_ func(onion string) `signal:"blockPeer,auto"`
_ func(onion string) `signal:"unblockPeer,auto"`
_ func(onion string, auth string) `signal:"setPeerAuthorization,auto"`
_ func(onion string) `signal:"loadMessagesPane,auto"`
_ func(signal string) `signal:"broadcast,auto"` // convenience relay signal
_ func(str string) `signal:"importString,auto"`
@ -339,7 +339,7 @@ func (this *GrandCentralDispatcher) requestPeerSettings() {
contact := the.Peer.GetContact(this.SelectedConversation())
if contact == nil {
log.Errorf("error: requested settings for unknown contact %v?", this.SelectedConversation())
this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), false)
this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), string(contact.Authorization))
return
}
@ -348,7 +348,7 @@ func (this *GrandCentralDispatcher) requestPeerSettings() {
// Todo: Move to profile settings
//blockunkownpeers, _ := the.Peer.GetAttribute(attr.GetPeerScope(constants.BlockUnknownPeersSetting))
this.SupplyPeerSettings(contact.Onion, name, contact.Blocked)
this.SupplyPeerSettings(contact.Onion, name, string(contact.Authorization))
}
func (this *GrandCentralDispatcher) savePeerSettings(onion, nick string) {
@ -397,7 +397,7 @@ func (this *GrandCentralDispatcher) createContact(onion string) {
if contact := the.Peer.GetContact(onion); contact != nil {
return
}
the.Peer.AddContact(onion, onion, false)
the.Peer.AddContact(onion, onion, model.AuthApproved)
the.Peer.PeerWithOnion(onion)
}
@ -460,7 +460,7 @@ func (this *GrandCentralDispatcher) importString(str string) {
this.InvokePopup("already have this contact")
return //TODO: bring them to the duplicate
} else {
the.Peer.AddContact(name, onion, false)
the.Peer.AddContact(name, onion, model.AuthApproved)
the.Peer.PeerWithOnion(onion)
}
@ -496,21 +496,18 @@ func (this *GrandCentralDispatcher) createGroup(server, groupName string) {
the.Peer.JoinServer(server)
}
func (this *GrandCentralDispatcher) blockPeer(onion string) {
err := the.Peer.BlockPeer(onion)
func (this *GrandCentralDispatcher) setPeerAuthorization(onion string, authorization string) {
log.Debugf("Setting peer auth level to %v for %v\n", authorization, onion)
err := the.Peer.SetContactAuthorization(onion, model.Authorization(authorization))
if err != nil {
this.InvokePopup("Error Blocking Peer: " + err.Error())
log.Errorf("Could not set peer authorization %v to %v\n", onion, authorization)
return
}
this.UpdateContactBlocked(onion, true)
}
func (this *GrandCentralDispatcher) unblockPeer(onion string) {
err := the.Peer.UnblockPeer(onion)
if err != nil {
this.InvokePopup("Error Unblocking Peer: " + err.Error())
this.RemoveContact(onion)
this.GetUiManager(this.selectedProfile()).AddContact(onion)
if model.Authorization(authorization) == model.AuthApproved {
the.Peer.PeerWithOnion(onion)
}
the.Peer.PeerWithOnion(onion)
this.UpdateContactBlocked(onion, false)
}
func (this *GrandCentralDispatcher) inviteToGroup(onion, groupID string) {
@ -622,7 +619,12 @@ func (this *GrandCentralDispatcher) loadProfile(onion string) {
the.Peer.SetAttribute(attr.GetPublicScope(constants.Picture), ImageToString(pic))
}
tag, _ := the.Peer.GetAttribute(app.AttributeTag)
this.UpdateMyProfile(the.Peer.GetName(), the.Peer.GetOnion(), getPicturePath(pic), tag)
showBlocked, exists := the.Peer.GetAttribute(attr.GetSettingsScope(constants.ShowBlocked))
if !exists {
showBlocked = "false"
the.Peer.SetAttribute(attr.GetSettingsScope(constants.ShowBlocked), showBlocked)
}
this.UpdateMyProfile(the.Peer.GetName(), the.Peer.GetOnion(), getPicturePath(pic), tag, showBlocked)
contacts := the.Peer.GetContacts()
for i := range contacts {
@ -658,6 +660,10 @@ func (this *GrandCentralDispatcher) changePassword(onion, currentPassword, newPa
the.CwtchApp.ChangePeerPassword(onion, currentPassword, newPassword)
}
func (this *GrandCentralDispatcher) storeSetting(key, val string) {
the.Peer.SetAttribute(attr.GetSettingsScope(key), val)
}
func (this *GrandCentralDispatcher) reloadProfileList() {
this.ResetProfileList()

View File

@ -123,7 +123,7 @@ func getProfilePic(id string) string {
return getPicturePath(pic)
}
}
return getPicturePath(NewImage("fontawesome/regular/user.svg", TypeImageDistro))
return RandomProfileImage(id)
}
}
@ -241,7 +241,7 @@ func (this *manager) AddContact(handle string) {
unread := countUnread(group.Timeline.GetMessages(), lastRead)
picture := getProfilePic(handle)
this.gcd.AddContact(handle, getNick(handle), picture, unread, int(connections.ConnectionStateToType[group.State]), false, false, getLastMessageTime(&group.Timeline))
this.gcd.AddContact(handle, getNick(handle), picture, unread, int(connections.ConnectionStateToType[group.State]), string(model.AuthApproved), false, getLastMessageTime(&group.Timeline))
}
return
} else if !isPeer(handle) {
@ -256,7 +256,7 @@ func (this *manager) AddContact(handle string) {
unread := countUnread(contact.Timeline.GetMessages(), lastRead)
picture := getProfilePic(handle)
this.gcd.AddContact(handle, getNick(handle), picture, unread, int(connections.ConnectionStateToType[contact.State]), contact.Blocked, false, getLastMessageTime(&contact.Timeline))
this.gcd.AddContact(handle, getNick(handle), picture, unread, int(connections.ConnectionStateToType[contact.State]), string(contact.Authorization), false, getLastMessageTime(&contact.Timeline))
}
})
}

View File

@ -1,15 +1,11 @@
package ui
import (
"crypto/rand"
"cwtch.im/cwtch/storage/v1"
"cwtch.im/ui/go/the"
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
"github.com/therecipe/qt/core"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"io/ioutil"
"os"
"path"
@ -31,34 +27,12 @@ var DefaultGlobalSettings = GlobalSettings{
Theme: "light",
}
// createKeySalt derives a key from a password: returns key, salt, err
func createKeySalt(password string) ([32]byte, [128]byte, error) {
var salt [128]byte
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
return [32]byte{}, salt, err
}
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr, salt, nil
}
func createKey(password string, salt []byte) [32]byte {
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr
}
func InitGlobalSettingsFile(directory string, password string) error {
var key [32]byte
salt, err := ioutil.ReadFile(path.Join(directory, saltFile))
if err != nil {
var newSalt [128]byte
key, newSalt, err = createKeySalt(password)
key, newSalt, err = v1.CreateKeySalt(password)
if err != nil {
return err
}
@ -68,7 +42,7 @@ func InitGlobalSettingsFile(directory string, password string) error {
return err
}
} else {
key = createKey(password, salt)
key = v1.CreateKey(password, salt)
}
the.GlobalSettingsFile = v1.NewFileStore(directory, GlobalSettingsFilename, key)

View File

@ -55,11 +55,16 @@
<context>
<name>ContactList</name>
<message>
<location filename="../qml/widgets/ContactList.qml" line="41"/>
<location filename="../qml/widgets/ContactList.qml" line="53"/>
<source>paste-address-to-add-contact</source>
<extracomment>ex: &quot;... paste an address here to add a contact ...&quot;</extracomment>
<translation type="unfinished">Adresse hier hinzufügen, um einen Kontakt aufzunehmen</translation>
</message>
<message>
<location filename="../qml/widgets/ContactList.qml" line="251"/>
<source>blocked</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>GroupSettingsPane</name>
@ -227,38 +232,38 @@
<context>
<name>PeerSettingsPane</name>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="25"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="26"/>
<source>address-label</source>
<translation>Adresse</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="30"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="31"/>
<source>copy-btn</source>
<translation>Kopieren</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="34"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="35"/>
<source>copied-to-clipboard-notification</source>
<extracomment>notification: copied to clipboard</extracomment>
<translation>in die Zwischenablage kopiert</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="46"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="47"/>
<source>display-name-label</source>
<translation>Angezeigter Name</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="50"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="51"/>
<source>save-btn</source>
<translation>speichern</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="89"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="92"/>
<source>delete-btn</source>
<translation>löschen</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="65"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="64"/>
<source>block-btn</source>
<translation type="unfinished"></translation>
</message>
@ -371,7 +376,7 @@
<context>
<name>ProfileList</name>
<message>
<location filename="../qml/widgets/ProfileList.qml" line="108"/>
<location filename="../qml/widgets/ProfileList.qml" line="107"/>
<source>add-new-profile-btn</source>
<translation type="unfinished"></translation>
</message>
@ -447,34 +452,34 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="84"/>
<location filename="../qml/panes/SettingsPane.qml" line="86"/>
<source>large-text-label</source>
<translation>Groß</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="92"/>
<location filename="../qml/panes/SettingsPane.qml" line="94"/>
<source>setting-theme</source>
<extracomment>Theme</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="101"/>
<location filename="../qml/panes/SettingsPane.qml" line="103"/>
<source>theme-light</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="102"/>
<location filename="../qml/panes/SettingsPane.qml" line="104"/>
<source>theme-dark</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="151"/>
<location filename="../qml/panes/SettingsPane.qml" line="153"/>
<source>version %1</source>
<extracomment>Version %1</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="160"/>
<location filename="../qml/panes/SettingsPane.qml" line="162"/>
<source>builddate %2</source>
<extracomment>Built on: %2</extracomment>
<translation type="unfinished"></translation>

Binary file not shown.

View File

@ -55,11 +55,16 @@
<context>
<name>ContactList</name>
<message>
<location filename="../qml/widgets/ContactList.qml" line="41"/>
<location filename="../qml/widgets/ContactList.qml" line="53"/>
<source>paste-address-to-add-contact</source>
<extracomment>ex: &quot;... paste an address here to add a contact ...&quot;</extracomment>
<translation>... paste an address here to add a contact...</translation>
</message>
<message>
<location filename="../qml/widgets/ContactList.qml" line="251"/>
<source>blocked</source>
<translation>Blocked</translation>
</message>
</context>
<context>
<name>EmojiDrawer</name>
@ -322,33 +327,33 @@ Right-click to reset.</translation>
<context>
<name>PeerSettingsPane</name>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="25"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="26"/>
<source>address-label</source>
<translation>Address</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="30"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="31"/>
<source>copy-btn</source>
<translation>Copy</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="34"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="35"/>
<source>copied-to-clipboard-notification</source>
<extracomment>notification: copied to clipboard</extracomment>
<translation>Copied to Clipboard</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="46"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="47"/>
<source>display-name-label</source>
<translation>Display Name</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="50"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="51"/>
<source>save-btn</source>
<translation>Save</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="65"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="64"/>
<source>block-btn</source>
<translation>Block Peer</translation>
</message>
@ -357,7 +362,7 @@ Right-click to reset.</translation>
<translation type="vanished">Unblock Peer</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="89"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="92"/>
<source>delete-btn</source>
<translation>Delete</translation>
</message>
@ -478,7 +483,7 @@ Right-click to reset.</translation>
<context>
<name>ProfileList</name>
<message>
<location filename="../qml/widgets/ProfileList.qml" line="108"/>
<location filename="../qml/widgets/ProfileList.qml" line="107"/>
<source>add-new-profile-btn</source>
<translation>Add new profile</translation>
</message>
@ -563,34 +568,34 @@ Right-click to reset.</translation>
<translation>Zoom level</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="84"/>
<location filename="../qml/panes/SettingsPane.qml" line="86"/>
<source>large-text-label</source>
<translation>Large</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="92"/>
<location filename="../qml/panes/SettingsPane.qml" line="94"/>
<source>setting-theme</source>
<extracomment>Theme</extracomment>
<translation>Theme</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="101"/>
<location filename="../qml/panes/SettingsPane.qml" line="103"/>
<source>theme-light</source>
<translation>Light</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="102"/>
<location filename="../qml/panes/SettingsPane.qml" line="104"/>
<source>theme-dark</source>
<translation>Dark</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="151"/>
<location filename="../qml/panes/SettingsPane.qml" line="153"/>
<source>version %1</source>
<extracomment>Version %1</extracomment>
<translation>Version %1</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="160"/>
<location filename="../qml/panes/SettingsPane.qml" line="162"/>
<source>builddate %2</source>
<extracomment>Built on: %2</extracomment>
<translation>Built on: %2</translation>

View File

@ -55,11 +55,16 @@
<context>
<name>ContactList</name>
<message>
<location filename="../qml/widgets/ContactList.qml" line="41"/>
<location filename="../qml/widgets/ContactList.qml" line="53"/>
<source>paste-address-to-add-contact</source>
<extracomment>ex: &quot;... paste an address here to add a contact ...&quot;</extracomment>
<translation type="unfinished">... coller une adresse ici pour ajouter un contact...</translation>
</message>
<message>
<location filename="../qml/widgets/ContactList.qml" line="251"/>
<source>blocked</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>GroupSettingsPane</name>
@ -227,38 +232,38 @@
<context>
<name>PeerSettingsPane</name>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="25"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="26"/>
<source>address-label</source>
<translation>Adresse</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="30"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="31"/>
<source>copy-btn</source>
<translation>Copier</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="34"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="35"/>
<source>copied-to-clipboard-notification</source>
<extracomment>notification: copied to clipboard</extracomment>
<translation>Copié dans le presse-papier</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="46"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="47"/>
<source>display-name-label</source>
<translation>Pseudo</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="50"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="51"/>
<source>save-btn</source>
<translation>Sauvegarder</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="89"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="92"/>
<source>delete-btn</source>
<translation>Effacer</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="65"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="64"/>
<source>block-btn</source>
<translation type="unfinished"></translation>
</message>
@ -371,7 +376,7 @@
<context>
<name>ProfileList</name>
<message>
<location filename="../qml/widgets/ProfileList.qml" line="108"/>
<location filename="../qml/widgets/ProfileList.qml" line="107"/>
<source>add-new-profile-btn</source>
<translation type="unfinished"></translation>
</message>
@ -447,34 +452,34 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="84"/>
<location filename="../qml/panes/SettingsPane.qml" line="86"/>
<source>large-text-label</source>
<translation type="unfinished">Large</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="92"/>
<location filename="../qml/panes/SettingsPane.qml" line="94"/>
<source>setting-theme</source>
<extracomment>Theme</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="101"/>
<location filename="../qml/panes/SettingsPane.qml" line="103"/>
<source>theme-light</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="102"/>
<location filename="../qml/panes/SettingsPane.qml" line="104"/>
<source>theme-dark</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="151"/>
<location filename="../qml/panes/SettingsPane.qml" line="153"/>
<source>version %1</source>
<extracomment>Version %1</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="160"/>
<location filename="../qml/panes/SettingsPane.qml" line="162"/>
<source>builddate %2</source>
<extracomment>Built on: %2</extracomment>
<translation type="unfinished"></translation>

View File

@ -55,11 +55,16 @@
<context>
<name>ContactList</name>
<message>
<location filename="../qml/widgets/ContactList.qml" line="41"/>
<location filename="../qml/widgets/ContactList.qml" line="53"/>
<source>paste-address-to-add-contact</source>
<extracomment>ex: &quot;... paste an address here to add a contact ...&quot;</extracomment>
<translation type="unfinished"> cole um endereço aqui para adicionar um contato</translation>
</message>
<message>
<location filename="../qml/widgets/ContactList.qml" line="251"/>
<source>blocked</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>GroupSettingsPane</name>
@ -227,38 +232,38 @@
<context>
<name>PeerSettingsPane</name>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="25"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="26"/>
<source>address-label</source>
<translation>Endereço</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="30"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="31"/>
<source>copy-btn</source>
<translation>Copiar</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="34"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="35"/>
<source>copied-to-clipboard-notification</source>
<extracomment>notification: copied to clipboard</extracomment>
<translation>Copiado</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="46"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="47"/>
<source>display-name-label</source>
<translation>Nome de Exibição</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="50"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="51"/>
<source>save-btn</source>
<translation>Salvar</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="89"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="92"/>
<source>delete-btn</source>
<translation>Deletar</translation>
</message>
<message>
<location filename="../qml/panes/PeerSettingsPane.qml" line="65"/>
<location filename="../qml/panes/PeerSettingsPane.qml" line="64"/>
<source>block-btn</source>
<translation type="unfinished"></translation>
</message>
@ -371,7 +376,7 @@
<context>
<name>ProfileList</name>
<message>
<location filename="../qml/widgets/ProfileList.qml" line="108"/>
<location filename="../qml/widgets/ProfileList.qml" line="107"/>
<source>add-new-profile-btn</source>
<translation type="unfinished"></translation>
</message>
@ -447,34 +452,34 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="84"/>
<location filename="../qml/panes/SettingsPane.qml" line="86"/>
<source>large-text-label</source>
<translation>Grande</translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="92"/>
<location filename="../qml/panes/SettingsPane.qml" line="94"/>
<source>setting-theme</source>
<extracomment>Theme</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="101"/>
<location filename="../qml/panes/SettingsPane.qml" line="103"/>
<source>theme-light</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="102"/>
<location filename="../qml/panes/SettingsPane.qml" line="104"/>
<source>theme-dark</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="151"/>
<location filename="../qml/panes/SettingsPane.qml" line="153"/>
<source>version %1</source>
<extracomment>Version %1</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/panes/SettingsPane.qml" line="160"/>
<location filename="../qml/panes/SettingsPane.qml" line="162"/>
<source>builddate %2</source>
<extracomment>Built on: %2</extracomment>
<translation type="unfinished"></translation>

14
qml/const/Const.qml Normal file
View File

@ -0,0 +1,14 @@
pragma Singleton
import QtQuick 2.0
Item {
// defined in cwtch.im/cwtch/model/profile.go ln24
readonly property string auth_unknown: "unknown"
readonly property string auth_blocked: "blocked"
readonly property string auth_approved: "approved"
// defined in cwtch.im/ui/go/constants/attributes.go
readonly property string show_blocked: "show-blocked"
}

1
qml/const/qmldir Normal file
View File

@ -0,0 +1 @@
singleton Const 1.0 Const.qml

View File

@ -348,8 +348,6 @@ ApplicationWindow {
if (Qt.application.state == 4) {
// Active
gcd.onActivate()
console.log(Fonts.applicationFontRegular.name)
console.log(Fonts.applicationFontBold.name)
}
}
}

@ -1 +1 @@
Subproject commit 0ec6a2df571e1a9c462d902cfc89f2402a9bef97
Subproject commit 0505934172c42fc589f8c0e095e1e8f0efeda245

View File

@ -10,12 +10,13 @@ import QtQuick.Controls.Styles 1.4
import "../opaque" as Opaque
import "../opaque/styles"
import "../opaque/theme"
import "../const"
Opaque.SettingsList { // settingsPane
id: root
anchors.fill: parent
property bool blocked
property string authorization
settings: Column {
anchors.fill: parent
@ -59,8 +60,6 @@ Opaque.SettingsList { // settingsPane
}
Opaque.Setting {
label: qsTr("block-btn")
@ -68,15 +67,19 @@ Opaque.SettingsList { // settingsPane
field: Opaque.ToggleSwitch {
anchors.right: parent.right
isToggled: root.blocked // ? qsTr("unblock-btn") : qsTr("block-btn")
isToggled: root.authorization == Const.auth_blocked
onToggled: function() {
if (root.blocked) {
gcd.unblockPeer(txtOnion.text)
console.log("peer block toddle for " + txtOnion.text + " currently: " + root.authorization)
if (root.authorization == Const.auth_blocked) {
root.authorization = Const.auth_unknown
console.log("setPeerAuthorization to " + Const.auth_unknown + " for " + txtOnion.text)
gcd.setPeerAuthorization(txtOnion.text, Const.auth_unknown)
} else {
gcd.blockPeer(txtOnion.text)
root.authorization = Const.auth_blocked
console.log("setPeerAuthorization to " + Const.auth_blocked + " for " + txtOnion.text)
gcd.setPeerAuthorization(txtOnion.text, Const.auth_blocked)
}
root.blocked = !root.blocked
isToggled = root.blocked
isToggled = root.authorization == Const.auth_blocked
}
}
}
@ -103,10 +106,10 @@ Opaque.SettingsList { // settingsPane
Connections {
target: gcd
onSupplyPeerSettings: function(onion, nick, blocked) {
onSupplyPeerSettings: function(onion, nick, authorization) {
txtOnion.text = onion
txtDisplayName.text = nick
root.blocked = blocked
root.authorization = authorization
}
}

View File

@ -193,43 +193,6 @@ text: qsTr("block-unknown-label")
}
}
}
=======
}
/* Opaque.ScalingLabel {
width: parent.width
wrapMode: TextEdit.Wrap
//: Interface zoom (mostly affects text and button sizes)
text: qsTr("zoom-label") + ":"
}
CheckBox {
id: blockUnknownToggle
checked: true
onClicked: {
if (blockUnknownToggle.checked) {
gcd.blockUnknownPeers()
} else {
gcd.allowUnknownPeers()
}
}
style: CheckBoxStyle {
label: Opaque.ScalingLabel {
text: qsTr("block-unknown-label")
}
}
}
>>>>>>> 007b851f... Add global settings pane; migrate peer settings to new opaque settings widgets; minor fixes; add global settings storage; rework global settings settings
*/

View File

@ -6,11 +6,13 @@ import QtQuick.Layouts 1.3
import "../opaque" as Opaque
import "../opaque/theme"
import "../const"
ColumnLayout {
id: root
property alias dualPane: myprof.dualPane
property real logscale: 4 * Math.log10(gcd.themeScale + 1)
spacing: 10
@ -26,6 +28,16 @@ ColumnLayout {
id: myprof
}
function removeContact(model, handle) {
for(var i = 0; i < model.count; i++){
if(model.get(i)["_handle"] == handle) {
model.remove(i)
return true
}
}
return false
}
Opaque.IconTextField {
id: searchAddText
@ -61,10 +73,9 @@ ColumnLayout {
Layout.fillHeight: true
Layout.minimumWidth: parent.width
Layout.maximumWidth: parent.width
contentWidth: colContacts.width
contentWidth: parent.width
contentHeight: colContacts.height
boundsBehavior: Flickable.StopAtBounds
maximumFlickVelocity: 400
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
@ -80,6 +91,8 @@ ColumnLayout {
}
}
ColumnLayout {
id: colContacts
width: root.width
@ -88,17 +101,29 @@ ColumnLayout {
Connections { // ADD/REMOVE CONTACT ENTRIES
target: gcd
onAddContact: function(handle, displayName, image, badge, status, blocked, loading, lastMsgTs) {
onAddContact: function(handle, displayName, image, badge, status, authorization, loading, lastMsgTs) {
var model = contactsModel
if (authorization == Const.auth_blocked) {
model = blockedContactsModel
}
for (var i = 0; i < contactsModel.count; i++) {
if (contactsModel.get(i)["_handle"] == handle) {
for (var i = 0; i < model.count; i++) {
if (model.get(i)["_handle"] == handle) {
return
}
}
var index = contactsModel.count
for (var i = 0; i < contactsModel.count; i++) {
if (contactsModel.get(i)["_lastMsgTs"] < lastMsgTs) {
// Sort order: [unknown,authed][most recent message]
var index = model.count
for (var i = 0; i < model.count; i++) {
var contact = model.get(i)
if (contact["_authorization"] == authorization) {
if (contact["_lastMsgTs"] < lastMsgTs) {
index = i
break
}
} else if (authorization == Const.auth_unknown) {
index = i
break
}
@ -106,26 +131,23 @@ ColumnLayout {
var newContact = {
"_handle": handle,
"_displayName": displayName + (blocked ? " (blocked)" : "" ),
"_displayName": displayName,
"_image": image,
"_badge": badge,
"_status": status,
"_blocked": blocked,
"_authorization": authorization,
"_loading": loading,
"_loading": loading,
"_lastMsgTs": lastMsgTs
}
contactsModel.insert(index, newContact)
model.insert(index, newContact)
}
onRemoveContact: function(handle) {
for(var i = 0; i < contactsModel.count; i++){
if(contactsModel.get(i)["_handle"] == handle) {
console.log("deleting contact " + contactsModel.get(i)["_handle"])
contactsModel.remove(i)
return
}
var removed = root.removeContact(contactsModel, handle)
if (!removed) {
root.removeContact(blockedContactsModel, handle)
}
}
@ -142,6 +164,7 @@ ColumnLayout {
onResetProfile: function() {
contactsModel.clear()
blockedContactsModel.clear()
}
}
@ -150,6 +173,7 @@ ColumnLayout {
}
Repeater {
id: contactRepeater
model: contactsModel // ... AND DISPLAYED HERE
delegate: ContactRow {
handle: _handle
@ -157,17 +181,124 @@ ColumnLayout {
image: _image
badge: _badge
status: _status
blocked: _blocked
authorization: _authorization
loading: _loading
rowColor: (_authorization == Const.auth_unknown) ? Theme.backgroundHilightElementColor : Theme.backgroundMainColor
Layout.fillWidth: true
}
}
Item {
id: blockItem
height: blockedToggle.height
Layout.fillWidth: true
visible: blockedContactsModel.count > 0
MouseArea {
anchors.fill: blockItem
hoverEnabled: true
onClicked: {
blockedToggle.showing = !blockedToggle.showing
blockedContacts.visible = blockedToggle.showing
if (blockedToggle.showing) {
gcd.storeSetting(Const.show_blocked, "true")
} else {
gcd.storeSetting(Const.show_blocked, "false")
}
}
onEntered: {
blockedBG.color = Theme.backgroundPaneColor
}
onExited: {
blockedBG.color = Theme.backgroundMainColor
}
}
Rectangle {
id: blockedBG
property bool isHover: false
anchors.fill: blockItem
color: Theme.backgroundMainColor
Connections {
target: Theme
onThemeChanged: {
blockedBG.color = Theme.backgroundMainColor
}
}
}
Row {
id: blockedToggle
property bool showing: true
leftPadding: 32 * logscale
topPadding: 16 * logscale
bottomPadding: 8 * logscale
spacing: 5 * logscale
Opaque.ScalingLabel {
id: blockLbl
text: qsTr("blocked")
size: Theme.chatMetaTextSize
color: Theme.portraitBlockedTextColor
}
Opaque.ScalingLabel {
id: blockBtn
text: blockedToggle.showing ? "▲" : "▼"
size: Theme.chatMetaTextSize
color: Theme.portraitBlockedTextColor
}
}
Connections {
target: gcd
onUpdateMyProfile: function(_nick, _onion, _image, _tag, _showBlocked) {
blockedToggle.showing = (_showBlocked == "true")
blockedContacts.visible = (_showBlocked == "true")
}
}
}
ListModel {
id: blockedContactsModel
}
ColumnLayout {
id: blockedContacts
Layout.fillWidth: true
spacing: 0
Repeater {
id: blockedContactsRepeater
model: blockedContactsModel // ... AND DISPLAYED HERE
delegate: ContactRow {
handle: _handle
displayName: _displayName
image: _image
badge: _badge
status: _status
authorization: _authorization
loading: _loading
rowColor: Theme.backgroundHilightElementColor
rowHilightColor: Theme.backgroundMainColor
Layout.fillWidth: true
}
}
}
}
}
}

View File

@ -10,11 +10,16 @@ import QtQuick.Controls.Styles 1.4
import "../opaque" as Opaque
import "../opaque/styles"
import "../opaque/theme"
import "../const"
Opaque.PortraitRow {
property int status: 0
property int badge
property bool loading
property string authorization
// TODO: should be in ContactRow
property bool blocked
badgeColor: Theme.portraitContactBadgeColor
badgeVisible: badge > 0
@ -48,6 +53,35 @@ Opaque.PortraitRow {
}
}
Column {
visible: authorization == Const.auth_unknown
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 1 * gcd.themeScale
anchors.rightMargin: 25 * gcd.themeScale
spacing: 16 * gcd.themeScale
Opaque.Icon {
source: gcd.assetPath + "core/favorite-24px.svg"
iconColor: Theme.toolbarIconColor
backgroundColor: rowColor
height: 18 * gcd.themeScale
width: 18 * gcd.themeScale
onClicked: { gcd.setPeerAuthorization(handle, Const.auth_approved)}
}
Opaque.Icon {
source: gcd.assetPath + "core/delete-24px.svg"
iconColor: Theme.toolbarIconColor
backgroundColor: rowColor
height: 18 * gcd.themeScale
width: 18 * gcd.themeScale
onClicked: { console.log("approve"); gcd.setPeerAuthorization(handle, Const.auth_blocked)}
}
}
onClicked: function(handle) {
gcd.broadcast("ResetMessagePane")
isActive = true
@ -65,7 +99,13 @@ Opaque.PortraitRow {
function setColors(status) {
//-2:WtfCodeError,-1:Error,0:Disconnected,1:Connecting,2:Connected,3:Authenticated,4:Synced,5:Failed,6:Killed
if (status == 4 || status == 3) {
if (authorization == Const.auth_blocked) {
portraitBorderColor = Theme.portraitBlockedBorderColor
portraitColor = Theme.portraitBlockedBackgroundColor
nameColor = Theme.portraitBlockedTextColor
onionColor = Theme.portraitBlockedTextColor
} else if (status == 4 || status == 3) {
portraitBorderColor = Theme.portraitOnlineBorderColor
portraitColor = Theme.portraitOnlineBackgroundColor
nameColor = Theme.portraitOnlineTextColor

View File

@ -139,12 +139,11 @@ Item {
Connections {
target: gcd
onUpdateMyProfile: function(_nick, _onion, _image, _tag) {
onUpdateMyProfile: function(_nick, _onion, _image, _tag, _showBlocked) {
nick = _nick
onion = _onion
image = _image
tag = _tag
//realignProfile()
}
onResetProfile: { realignProfile() }

View File

@ -98,8 +98,8 @@ ColumnLayout {
handle: _handle
displayName: _displayName
image: _image
blocked: false
tag: _tag
Layout.fillWidth: true
}
}