initial commit
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/application"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatChannelListener struct {
|
||||
rai *application.ApplicationInstance
|
||||
ra *application.RicochetApplication
|
||||
}
|
||||
|
||||
|
||||
func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) {
|
||||
this.rai = rai
|
||||
this.ra = ra
|
||||
}
|
||||
|
||||
// We always want bidirectional chat channels
|
||||
func (this *ChatChannelListener) OpenInbound() {
|
||||
outboutChatChannel := this.rai.Connection.Channel("im.ricochet.chat", channels.Outbound)
|
||||
if outboutChatChannel == nil {
|
||||
this.rai.Connection.Do(func() error {
|
||||
this.rai.Connection.RequestOpenChannel("im.ricochet.chat",
|
||||
&channels.ChatChannel{
|
||||
Handler: this,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (this *ChatChannelListener) ChatMessage(messageID uint32, when time.Time, message string) bool {
|
||||
DeliverMessageToUI(this.rai.RemoteHostname, message, uint(messageID), false, when)
|
||||
return true
|
||||
}
|
||||
|
||||
func (this *ChatChannelListener) ChatMessageAck(messageID uint32, accepted bool) {
|
||||
gcd.Acknowledged(acknowledgementIDs[messageID])
|
||||
}
|
80
gcd.go
|
@ -6,22 +6,27 @@ import (
|
|||
"github.com/therecipe/qt/core"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var TIME_FORMAT = "Mon 3:04pm"
|
||||
|
||||
type GrandCentralDispatcher struct {
|
||||
core.QObject
|
||||
|
||||
currentOpenConversation string
|
||||
|
||||
// messages pane stuff
|
||||
_ func(from, message string) `signal:"AppendMessage"`
|
||||
_ func() `signal:"ClearMessages"`
|
||||
_ func() `signal:"ResetMessagePane"`
|
||||
_ func(from, message string, mID uint, ts string) `signal:"AppendMessage"`
|
||||
_ func() `signal:"ClearMessages"`
|
||||
_ func() `signal:"ResetMessagePane"`
|
||||
_ func(uint) `signal:"Acknowledged"`
|
||||
|
||||
// contact list stuff
|
||||
_ func(onion string, num int) `signal:"SetUnread"`
|
||||
_ func(onion string, status int) `signal:"SetConnectionStatus"`
|
||||
_ func(name, onion, image, badge string, trusted bool) `signal:"AddContact"`
|
||||
_ func(name, onion, server, image, badge string, trusted bool) `signal:"AddContact"`
|
||||
_ func(onion string) `signal:"MarkTrusted"`
|
||||
|
||||
// profile-area stuff
|
||||
|
@ -32,15 +37,15 @@ type GrandCentralDispatcher struct {
|
|||
_ func(str string) `signal:"InvokePopup"`
|
||||
|
||||
// exfiltrated signals (written in go, below)
|
||||
_ func(message string) `signal:"sendMessage,auto"`
|
||||
_ func(onion string) `signal:"loadMessagesPane,auto"`
|
||||
_ func(signal string) `signal:"broadcast,auto"` // convenience relay signal
|
||||
_ func(str string) `signal:"importString,auto"`
|
||||
_ func(str string) `signal:"popup,auto"`
|
||||
_ func(nick string) `signal:"updateNick,auto"`
|
||||
_ func(message string, mid uint) `signal:"sendMessage,auto"`
|
||||
_ func(onion string) `signal:"loadMessagesPane,auto"`
|
||||
_ func(signal string) `signal:"broadcast,auto"` // convenience relay signal
|
||||
_ func(str string) `signal:"importString,auto"`
|
||||
_ func(str string) `signal:"popup,auto"`
|
||||
_ func(nick string) `signal:"updateNick,auto"`
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) sendMessage(message string) {
|
||||
func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) {
|
||||
if len(message) > 65530 {
|
||||
gcd.InvokePopup("message is too long")
|
||||
return
|
||||
|
@ -50,6 +55,16 @@ func (this *GrandCentralDispatcher) sendMessage(message string) {
|
|||
return
|
||||
}
|
||||
|
||||
if len(gcd.currentOpenConversation) == 32 { // SEND TO GROUP
|
||||
if !peer.GetGroup(gcd.currentOpenConversation).Accepted {
|
||||
peer.GetGroup(gcd.currentOpenConversation).Accepted = true
|
||||
peer.Save()
|
||||
}
|
||||
|
||||
peer.SendMessageToGroup(gcd.currentOpenConversation, message)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: require explicit invite accept/reject instead of implicitly trusting on send
|
||||
if !peer.GetContact(gcd.currentOpenConversation).Trusted {
|
||||
peer.GetContact(gcd.currentOpenConversation).Trusted = true
|
||||
|
@ -57,13 +72,13 @@ func (this *GrandCentralDispatcher) sendMessage(message string) {
|
|||
gcd.MarkTrusted(gcd.currentOpenConversation)
|
||||
}
|
||||
|
||||
select { // fancy trick to do a non-blocking send. this means the user can only send a limited number of messages
|
||||
// before the channel buffer fills. TODO: stop the user from sending if the buffer is full
|
||||
case outgoingMessages <- Message{gcd.currentOpenConversation, message, true}:
|
||||
select { // 1 weird trick to do a non-blocking send. this means the user can only send a limited number of messages
|
||||
// before the channel buffer fills. TODO: stop the user from sending if the buffer is full
|
||||
case outgoingMessages <- Message{gcd.currentOpenConversation, message, true, mID, time.Now()}:
|
||||
default:
|
||||
}
|
||||
|
||||
DeliverMessageToUI(gcd.currentOpenConversation, message, true)
|
||||
DeliverMessageToUI(gcd.currentOpenConversation, message, mID, true, time.Now())
|
||||
}
|
||||
|
||||
func (this *GrandCentralDispatcher) loadMessagesPane(onion string) {
|
||||
|
@ -71,6 +86,16 @@ func (this *GrandCentralDispatcher) loadMessagesPane(onion string) {
|
|||
gcd.currentOpenConversation = onion
|
||||
gcd.SetUnread(onion, 0)
|
||||
|
||||
if len(onion) == 32 { // eg 48e7dcfc353e6d77da2c31d63654fd19
|
||||
log.Printf("LOADING GROUP %s", onion)
|
||||
tl := peer.GetGroup(onion).GetTimeline()
|
||||
log.Printf("messages: %d", len(tl))
|
||||
for i := range tl {
|
||||
gcd.AppendMessage(tl[i].PeerID, tl[i].Message, 0, tl[i].Timestamp.Format(TIME_FORMAT))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, exists := contactMgr[onion]
|
||||
if exists { // (if not, they haven't been accepted as a contact yet)
|
||||
contactMgr[onion].Unread = 0
|
||||
|
@ -81,7 +106,7 @@ func (this *GrandCentralDispatcher) loadMessagesPane(onion string) {
|
|||
if messages[i].FromMe {
|
||||
from = "me"
|
||||
}
|
||||
gcd.AppendMessage(from, messages[i].Message)
|
||||
gcd.AppendMessage(from, messages[i].Message, messages[i].MessageID, messages[i].Timestamp.Format(TIME_FORMAT))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +114,7 @@ func (this *GrandCentralDispatcher) loadMessagesPane(onion string) {
|
|||
func (this *GrandCentralDispatcher) broadcast(signal string) {
|
||||
switch signal {
|
||||
default:
|
||||
log.Fatalf("unhandled broadcast signal: %v", signal)
|
||||
log.Printf("unhandled broadcast signal: %v", signal)
|
||||
case "ResetMessagePane":
|
||||
gcd.ResetMessagePane()
|
||||
}
|
||||
|
@ -99,10 +124,27 @@ func (this *GrandCentralDispatcher) importString(str string) {
|
|||
log.Printf("importing: %s\n", str)
|
||||
onion := str
|
||||
name := onion
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
if strings.Contains(str, " ") {// usually people prepend spaces and we don't want it going into the name (use ~ for that)
|
||||
//eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA==
|
||||
if str[0:5] == "torv3" { // GROUP INVITE
|
||||
groupID, err := peer.ImportGroup(str)
|
||||
if err != nil {
|
||||
gcd.InvokePopup("not a valid group invite")
|
||||
return
|
||||
}
|
||||
|
||||
group := peer.GetGroup(groupID)
|
||||
peer.JoinServer(group.GroupServer)
|
||||
peer.Save()
|
||||
fmt.Printf("imported groupid=%s server=%s", groupID, group.GroupServer)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(str, " ") { // usually people prepend spaces and we don't want it going into the name (use ~ for that)
|
||||
parts := strings.Split(strings.TrimSpace(str), " ")
|
||||
str = parts[len(parts) - 1]
|
||||
str = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
if strings.Contains(str, "~") {
|
||||
|
|
120
main.go
|
@ -17,7 +17,12 @@ import (
|
|||
"time"
|
||||
"strconv"
|
||||
"git.openprivacy.ca/openprivacy/asaur"
|
||||
)
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/application"
|
||||
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
|
||||
"log"
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
var gcd *GrandCentralDispatcher
|
||||
|
||||
|
@ -27,6 +32,9 @@ var contactMgr ContactManager
|
|||
var peer libpeer.CwtchPeer
|
||||
var outgoingMessages chan Message
|
||||
|
||||
// need this to translate from the message IDs the UI uses to the ones the acknowledgement system uses on the wire
|
||||
var acknowledgementIDs map[uint32]uint
|
||||
|
||||
type Contact struct {
|
||||
Messages []Message
|
||||
Unread int
|
||||
|
@ -40,20 +48,22 @@ func (this *Contact) AddMessage(m Message) {
|
|||
type Message struct {
|
||||
With, Message string
|
||||
FromMe bool
|
||||
MessageID uint
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func DeliverMessageToUI(from, message string, fromMe bool) {
|
||||
func DeliverMessageToUI(from, message string, mID uint, fromMe bool, ts time.Time) {
|
||||
_, found := contactMgr[from]
|
||||
if !found {
|
||||
contactMgr[from] = &Contact{[]Message{}, 0, 0}
|
||||
}
|
||||
|
||||
contactMgr[from].AddMessage(Message{from, message, fromMe})
|
||||
contactMgr[from].AddMessage(Message{from, message, fromMe, mID, ts})
|
||||
if gcd.currentOpenConversation == from {
|
||||
if fromMe {
|
||||
from = "me"
|
||||
}
|
||||
gcd.AppendMessage(from, message)
|
||||
gcd.AppendMessage(from, message, mID, ts.Format(TIME_FORMAT))
|
||||
} else {
|
||||
contactMgr[from].Unread++
|
||||
gcd.SetUnread(from, contactMgr[from].Unread)
|
||||
|
@ -66,6 +76,7 @@ func init() {
|
|||
|
||||
func main() {
|
||||
contactMgr = make(ContactManager)
|
||||
acknowledgementIDs = make(map[uint32]uint)
|
||||
|
||||
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true)
|
||||
widgets.NewQApplication(len(os.Args), os.Args)
|
||||
|
@ -89,22 +100,24 @@ func main() {
|
|||
view.Show()
|
||||
go torStatusPoller()
|
||||
go presencePoller()
|
||||
go groupPoller()
|
||||
go ricochetListener()
|
||||
go alice()
|
||||
widgets.QApplication_Exec()
|
||||
}
|
||||
|
||||
func alice() {
|
||||
i := 0
|
||||
|
||||
func groupPoller() {
|
||||
for {
|
||||
time.Sleep(time.Second * 3)
|
||||
//words, _ := diceware.Generate(3)
|
||||
//DeliverMessageToUI("f76b5vtleqx2puhwgkords34gs6crgbjqud6sebfzwtlrq4ngbqgcsyd", strings.Join(words, " "), i % 3 == 0)
|
||||
i++
|
||||
time.Sleep(time.Second * 4)
|
||||
|
||||
servers := peer.GetServers()
|
||||
groups := peer.GetGroups()
|
||||
for i := range groups {
|
||||
group := peer.GetGroup(groups[i])
|
||||
log.Printf("setting group %s to status %d", groups[i], int(servers[group.GroupServer]))
|
||||
gcd.SetConnectionStatus(groups[i], int(servers[group.GroupServer]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func presencePoller() { // TODO: make this subscribe-able in ricochet
|
||||
time.Sleep(time.Second * 4)
|
||||
for {
|
||||
|
@ -117,7 +130,7 @@ func presencePoller() { // TODO: make this subscribe-able in ricochet
|
|||
peer.GetProfile().SetCustomAttribute(contacts[i]+"_name", c.Name)
|
||||
peer.GetProfile().SetCustomAttribute(c.Name+"_onion", contacts[i])
|
||||
peer.Save()
|
||||
gcd.AddContact(c.Name, contacts[i], randomProfileImage(contacts[i]), "0", c.Trusted)
|
||||
gcd.AddContact(c.Name, contacts[i], "", randomProfileImage(contacts[i]), "0", c.Trusted)
|
||||
}
|
||||
|
||||
c, found := peer.GetPeers()[contacts[i]]
|
||||
|
@ -162,36 +175,18 @@ func torStatusPoller() {
|
|||
}
|
||||
|
||||
gcd.TorStatus(2, status["SUMMARY"])
|
||||
//qCwtchApp.SetTorStatusProgress(progress)
|
||||
//qCwtchApp.SetTorStatusSummary(status["SUMMARY"])
|
||||
//if status["TAG"] == "done" {
|
||||
}
|
||||
}
|
||||
|
||||
func cwtchListener(groupID string, channel chan model.Message) {
|
||||
for {
|
||||
m := <-channel
|
||||
log.Printf("GROUPMSG %s", m.Message)
|
||||
DeliverMessageToUI(groupID, m.Message, 0, m.PeerID == peer.GetProfile().Onion, m.Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func ricochetListener() {
|
||||
processData := func(onion string, data []byte) []byte {
|
||||
/* _, exists := peer.GetProfile().GetCustomAttribute(onion + "_name")
|
||||
if !exists {
|
||||
for peer.GetContact(onion) == nil {
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
}
|
||||
|
||||
name := peer.GetContact(onion).Name
|
||||
|
||||
fmt.Printf("adding new untrusted contact %v <%v>\n", name, onion)
|
||||
peer.GetProfile().SetCustomAttribute(onion+"_name", name)
|
||||
peer.GetProfile().SetCustomAttribute(name+"_onion", onion)
|
||||
peer.Save()
|
||||
|
||||
gcd.AddContact(name, onion, randomProfileImage(onion), "0", false)
|
||||
} */
|
||||
|
||||
DeliverMessageToUI(onion, string(data), false)
|
||||
return nil
|
||||
}
|
||||
|
||||
peer.SetPeerDataHandler(processData)
|
||||
fmt.Fprintf(os.Stderr, "waiting for messages...\n")
|
||||
err := peer.Listen()
|
||||
if err != nil {
|
||||
fmt.Printf("error listening for connections: %v\n", err)
|
||||
|
@ -219,7 +214,13 @@ func andHisBlackAndWhiteCat(incomingMessages chan Message) {
|
|||
for {
|
||||
m := <-incomingMessages
|
||||
connection := peer.PeerWithOnion(m.With)
|
||||
connection.SendPacket([]byte(m.Message))
|
||||
connection.DoOnChannel("im.ricochet.chat", channels.Outbound, func(channel *channels.Channel) {
|
||||
chatchannel, ok := channel.Handler.(*channels.ChatChannel)
|
||||
if ok {
|
||||
log.Printf("Sending packet")
|
||||
acknowledgementIDs[chatchannel.SendMessage(m.Message)] = m.MessageID
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,11 +258,36 @@ func initialize(view *quick.QQuickView) {
|
|||
|
||||
gcd.UpdateMyProfile(peer.GetProfile().Name, peer.GetProfile().Onion, randomProfileImage(peer.GetProfile().Onion))
|
||||
|
||||
aif := application.ApplicationInstanceFactory{}
|
||||
aif.Init()
|
||||
app := new(application.RicochetApplication)
|
||||
aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler {
|
||||
ccl := new(ChatChannelListener)
|
||||
ccl.Init(rai, app)
|
||||
return func() channels.Handler {
|
||||
chat := new(channels.ChatChannel)
|
||||
chat.Handler = ccl
|
||||
return chat
|
||||
}
|
||||
})
|
||||
peer.SetApplicationInstanceFactory(aif)
|
||||
|
||||
contacts := peer.GetContacts()
|
||||
for i := range contacts {
|
||||
attr, _ := peer.GetProfile().GetCustomAttribute(contacts[i] + "_name")
|
||||
contactMgr[contacts[i]] = &Contact{[]Message{}, 0, 0}
|
||||
gcd.AddContact(attr, contacts[i], randomProfileImage(contacts[i]), "0", peer.GetContact(contacts[i]).Trusted)
|
||||
gcd.AddContact(attr, contacts[i], "", randomProfileImage(contacts[i]), "0", peer.GetContact(contacts[i]).Trusted)
|
||||
}
|
||||
|
||||
groups := peer.GetGroups()
|
||||
for i := range groups {
|
||||
group := peer.GetGroup(groups[i])
|
||||
group.NewMessage = make(chan model.Message)
|
||||
go cwtchListener(groups[i], group.NewMessage)
|
||||
peer.JoinServer(group.GroupServer)
|
||||
//TODO: base the profileImage off the groupid, not the server. probably by decoding it and then re-encoding it b32
|
||||
gcd.AddContact(group.GroupID[:12], group.GroupID, group.GroupServer, randomGroupImage(groups[i]), "0", group.Accepted)
|
||||
log.Printf("GROUP %s@%s", group.GroupID, group.GroupServer)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,3 +301,13 @@ func randomProfileImage(onion string) string {
|
|||
}
|
||||
return "qrc:/qml/images/profiles/" + choices[int(barr[33])%len(choices)] + ".png"
|
||||
}
|
||||
|
||||
func randomGroupImage(onion string) string {
|
||||
choices := []string{"001-borobudur","002-opera-house","003-burj-al-arab","004-chrysler","005-acropolis","006-empire-state-building","007-temple","008-indonesia-1","009-new-zealand","010-notre-dame","011-space-needle","012-seoul","013-mosque","014-milan","015-statue","016-pyramid","017-cologne","018-brandenburg-gate","019-berlin-cathedral","020-hungarian-parliament","021-buckingham","022-thailand","023-independence","024-angkor-wat","025-vaticano","026-christ-the-redeemer","027-colosseum","028-golden-gate-bridge","029-sphinx","030-statue-of-liberty","031-cradle-of-humankind","032-istanbul","033-london-eye","034-sagrada-familia","035-tower-bridge","036-burj-khalifa","037-washington","038-big-ben","039-stonehenge","040-white-house","041-ahu-tongariki","042-capitol","043-eiffel-tower","044-church-of-the-savior-on-spilled-blood","045-arc-de-triomphe","046-windmill","047-louvre","048-torii-gate","049-petronas","050-matsumoto-castle","051-fuji","052-temple-of-heaven","053-pagoda","054-chichen-itza","055-forbidden-city","056-merlion","057-great-wall-of-china","058-taj-mahal","059-pisa","060-indonesia"}
|
||||
barr, err := hex.DecodeString(onion)
|
||||
if err != nil || len(barr) == 0 {
|
||||
fmt.Printf("error: %v %v %v\n", onion, err, barr)
|
||||
return "qrc:/qml/images/extra/openprivacy.png"
|
||||
}
|
||||
return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png"
|
||||
}
|
After Width: | Height: | Size: 551 B |
After Width: | Height: | Size: 923 B |
After Width: | Height: | Size: 557 B |
After Width: | Height: | Size: 458 B |
After Width: | Height: | Size: 562 B |
After Width: | Height: | Size: 403 B |
After Width: | Height: | Size: 682 B |
After Width: | Height: | Size: 561 B |
After Width: | Height: | Size: 603 B |
After Width: | Height: | Size: 517 B |
After Width: | Height: | Size: 495 B |
After Width: | Height: | Size: 668 B |
After Width: | Height: | Size: 670 B |
After Width: | Height: | Size: 640 B |
After Width: | Height: | Size: 722 B |
After Width: | Height: | Size: 553 B |
After Width: | Height: | Size: 747 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 697 B |
After Width: | Height: | Size: 287 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 849 B |
After Width: | Height: | Size: 989 B |
After Width: | Height: | Size: 662 B |
After Width: | Height: | Size: 245 B |
After Width: | Height: | Size: 723 B |
After Width: | Height: | Size: 481 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 847 B |
After Width: | Height: | Size: 205 B |
After Width: | Height: | Size: 835 B |
After Width: | Height: | Size: 387 B |
After Width: | Height: | Size: 230 B |
After Width: | Height: | Size: 402 B |
After Width: | Height: | Size: 559 B |
After Width: | Height: | Size: 652 B |
After Width: | Height: | Size: 406 B |
After Width: | Height: | Size: 336 B |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 390 B |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 411 B |
After Width: | Height: | Size: 940 B |
After Width: | Height: | Size: 279 B |
After Width: | Height: | Size: 978 B |
After Width: | Height: | Size: 919 B |
After Width: | Height: | Size: 884 B |
After Width: | Height: | Size: 666 B |
After Width: | Height: | Size: 871 B |
After Width: | Height: | Size: 805 B |
After Width: | Height: | Size: 419 B |
After Width: | Height: | Size: 1000 B |
After Width: | Height: | Size: 270 B |
After Width: | Height: | Size: 214 B |
After Width: | Height: | Size: 669 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 459 B |
After Width: | Height: | Size: 445 B |
After Width: | Height: | Size: 706 B |
After Width: | Height: | Size: 602 B |
After Width: | Height: | Size: 389 B |
After Width: | Height: | Size: 356 B |
After Width: | Height: | Size: 220 B |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 961 B |
After Width: | Height: | Size: 394 B |
After Width: | Height: | Size: 341 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 245 B |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 569 B |
After Width: | Height: | Size: 574 B |
After Width: | Height: | Size: 357 B |
After Width: | Height: | Size: 913 B |
After Width: | Height: | Size: 539 B |
After Width: | Height: | Size: 549 B |
After Width: | Height: | Size: 503 B |
After Width: | Height: | Size: 246 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 643 B |
After Width: | Height: | Size: 251 B |
After Width: | Height: | Size: 696 B |
After Width: | Height: | Size: 584 B |
After Width: | Height: | Size: 524 B |
After Width: | Height: | Size: 454 B |
After Width: | Height: | Size: 362 B |
After Width: | Height: | Size: 688 B |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 461 B |
After Width: | Height: | Size: 586 B |
After Width: | Height: | Size: 809 B |
After Width: | Height: | Size: 362 B |
After Width: | Height: | Size: 833 B |