Skip to content

Commit

Permalink
keep user auth and reply badges up-to-date
Browse files Browse the repository at this point in the history
  • Loading branch information
Allie Crevier committed Sep 30, 2020
1 parent 3efc9e2 commit fe703fe
Show file tree
Hide file tree
Showing 13 changed files with 803 additions and 104 deletions.
16 changes: 12 additions & 4 deletions securedrop_client/db.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import datetime
import os
from enum import Enum
from typing import Any, List, Union # noqa: F401
from typing import Any, List, Optional, Union # noqa: F401

from sqlalchemy import (
Boolean,
Expand Down Expand Up @@ -280,7 +280,7 @@ class Reply(Base):
)

journalist_id = Column(Integer, ForeignKey("users.id"))
journalist = relationship("User", backref=backref("replies", order_by=id))
journalist = relationship("User", backref=backref("replies", order_by=id), lazy="joined")

filename = Column(String(255), nullable=False)
file_counter = Column(Integer, nullable=False)
Expand Down Expand Up @@ -462,7 +462,13 @@ def __repr__(self) -> str:
return "<Journalist {}: {}>".format(self.uuid, self.username)

@property
def fullname(self) -> str:
def deleted(self) -> bool:
return True if self.uuid == "deleted" else False

@property
def fullname(self) -> Optional[str]:
if self.deleted:
return None
if self.firstname and self.lastname:
return self.firstname + " " + self.lastname
elif self.firstname:
Expand All @@ -473,7 +479,9 @@ def fullname(self) -> str:
return self.username

@property
def initials(self) -> str:
def initials(self) -> Optional[str]:
if self.deleted:
return None
if self.firstname and self.lastname:
return self.firstname[0].lower() + self.lastname[0].lower()
elif self.firstname and len(self.firstname) >= 2:
Expand Down
174 changes: 148 additions & 26 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,11 +427,17 @@ def __init__(self):
layout.addWidget(self.user_button, alignment=Qt.AlignTop)

def setup(self, window, controller):
self.controller = controller
self.controller.update_authenticated_user.connect(self._on_update_authenticated_user)
self.user_button.setup(controller)
self.login_button.setup(window)

@pyqtSlot(User)
def _on_update_authenticated_user(self, db_user: User) -> None:
self.set_user(db_user)

def set_user(self, db_user: User):
self.user_icon.setText(_(db_user.initials))
self.user_icon.setText(db_user.initials)
self.user_button.set_username(db_user.fullname)

def show(self):
Expand Down Expand Up @@ -1639,6 +1645,59 @@ def validate(self):
self.error(_("Please enter a username, passphrase and " "two-factor code."))


class SenderIcon(QWidget):
"""
Represents a reply to a source.
"""

SENDER_ICON_CSS = load_css("sender_icon.css")

def __init__(self) -> None:
super().__init__()
self.sender_is_current_user = False
self.setObjectName("SenderIcon")
self.setStyleSheet(self.SENDER_ICON_CSS)
self.setFixedSize(QSize(48, 48))
font = QFont()
font.setLetterSpacing(QFont.AbsoluteSpacing, 0.58)
self.label = QLabel()
self.label.setAlignment(Qt.AlignCenter)
self.label.setFont(font)
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.label)
self.setLayout(layout)

def set_sender(self, sender: User, sender_is_current_user: bool) -> None:
self.sender_is_current_user = sender_is_current_user
if not sender or not sender.initials:
self.label.setPixmap(load_image("deleted-user.png"))
else:
self.label.setText(sender.initials)

def set_normal_styles(self):
self.setStyleSheet("")
if self.sender_is_current_user:
self.setObjectName("SenderIcon_current_user")
else:
self.setObjectName("SenderIcon")
self.setStyleSheet(self.SENDER_ICON_CSS)

def set_failed_styles(self):
self.setStyleSheet("")
self.setObjectName("SenderIcon_failed")
self.setStyleSheet(self.SENDER_ICON_CSS)

def set_pending_styles(self):
self.setStyleSheet("")
if self.sender_is_current_user:
self.setObjectName("SenderIcon_current_user_pending")
else:
self.setObjectName("SenderIcon_pending")
self.setStyleSheet(self.SENDER_ICON_CSS)


class SpeechBubble(QWidget):
"""
Represents a speech bubble that's part of a conversation between a source
Expand Down Expand Up @@ -1682,6 +1741,10 @@ def __init__(
self.color_bar.setObjectName("SpeechBubble_status_bar")
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)

# User icon
self.sender_icon = SenderIcon()
self.sender_icon.hide()

# Speech bubble
self.speech_bubble = QWidget()
self.speech_bubble.setObjectName("SpeechBubble_container")
Expand All @@ -1698,6 +1761,7 @@ def __init__(
self.bubble_area_layout = QHBoxLayout()
self.bubble_area_layout.setContentsMargins(0, self.TOP_MARGIN, 0, self.BOTTOM_MARGIN)
bubble_area.setLayout(self.bubble_area_layout)
self.bubble_area_layout.addWidget(self.sender_icon, alignment=Qt.AlignBottom)
self.bubble_area_layout.addWidget(self.speech_bubble)

# Add widget to layout
Expand Down Expand Up @@ -1779,6 +1843,7 @@ class ReplyWidget(SpeechBubble):

def __init__(
self,
controller: Controller,
message_uuid: str,
message: str,
reply_status: str,
Expand All @@ -1787,11 +1852,16 @@ def __init__(
message_succeeded_signal,
message_failed_signal,
index: int,
sender: User,
sender_is_current_user: bool,
error: bool = False,
) -> None:
super().__init__(message_uuid, message, update_signal, download_error_signal, index, error)
self.controller = controller
self.status = reply_status
self.uuid = message_uuid

self.sender = sender
self.sender_is_current_user = sender_is_current_user
self.error = QWidget()
error_layout = QHBoxLayout()
error_layout.setContentsMargins(0, 0, 0, 0)
Expand All @@ -1806,23 +1876,21 @@ def __init__(
self.error.hide()

self.bubble_area_layout.addWidget(self.error)
self.sender_icon.set_sender(sender, self.sender_is_current_user)
self.sender_icon.show()

message_succeeded_signal.connect(self._on_reply_success)
message_failed_signal.connect(self._on_reply_failure)
self.controller.update_authenticated_user.connect(self._on_update_authenticated_user)

self._set_reply_state(reply_status)
self._set_reply_state()

def _set_reply_state(self, status: str) -> None:
logger.debug(f"Setting ReplyWidget state: {status}")

if status == "SUCCEEDED":
self.set_normal_styles()
self.error.hide()
elif status == "FAILED":
self.set_failed_styles()
self.error.show()
elif status == "PENDING":
self.set_pending_styles()
@pyqtSlot(User)
def _on_update_authenticated_user(self, db_user: User) -> None:
sender_is_current_user = True if db_user.uuid == self.sender.uuid else False
self.sender_is_current_user = sender_is_current_user
self.sender_icon.sender_is_current_user = sender_is_current_user
self._set_reply_state()

@pyqtSlot(str, str, str)
def _on_reply_success(self, source_id: str, message_uuid: str, content: str) -> None:
Expand All @@ -1831,7 +1899,8 @@ def _on_reply_success(self, source_id: str, message_uuid: str, content: str) ->
signal matches the uuid of this widget.
"""
if message_uuid == self.uuid:
self._set_reply_state("SUCCEEDED")
self.status = "SUCCEEDED"
self._set_reply_state()

@pyqtSlot(str)
def _on_reply_failure(self, message_uuid: str) -> None:
Expand All @@ -1840,30 +1909,54 @@ def _on_reply_failure(self, message_uuid: str) -> None:
signal matches the uuid of this widget.
"""
if message_uuid == self.uuid:
self._set_reply_state("FAILED")
self.status = "FAILED"
self._set_reply_state()

def _set_reply_state(self) -> None:
if self.status == "SUCCEEDED":
self.set_normal_styles()
self.error.hide()
elif self.status == "PENDING":
self.set_pending_styles()
elif self.status == "FAILED":
self.set_failed_styles()
self.error.show()

def set_normal_styles(self):
self.message.setStyleSheet("")
self.message.setObjectName("ReplyWidget_message")
self.message.setStyleSheet(self.MESSAGE_CSS)

self.sender_icon.set_normal_styles()

self.color_bar.setStyleSheet("")
self.color_bar.setObjectName("ReplyWidget_status_bar")
if self.sender_is_current_user:
self.color_bar.setObjectName("ReplyWidget_status_bar_current_user")
else:
self.color_bar.setObjectName("ReplyWidget_status_bar")
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)

def set_failed_styles(self):
def set_pending_styles(self):
self.message.setStyleSheet("")
self.message.setObjectName("ReplyWidget_message_failed")
self.message.setObjectName("ReplyWidget_message_pending")
self.message.setStyleSheet(self.MESSAGE_CSS)

self.sender_icon.set_pending_styles()

self.color_bar.setStyleSheet("")
self.color_bar.setObjectName("ReplyWidget_status_bar_failed")
if self.sender_is_current_user:
self.color_bar.setObjectName("ReplyWidget_status_bar_pending_current_user")
else:
self.color_bar.setObjectName("ReplyWidget_status_bar_pending")
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)

def set_pending_styles(self):
def set_failed_styles(self):
self.message.setStyleSheet("")
self.message.setObjectName("ReplyWidget_message_pending")
self.message.setObjectName("ReplyWidget_message_failed")
self.message.setStyleSheet(self.MESSAGE_CSS)
self.sender_icon.set_failed_styles()
self.color_bar.setStyleSheet("")
self.color_bar.setObjectName("ReplyWidget_status_bar_pending")
self.color_bar.setObjectName("ReplyWidget_status_bar_failed")
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)


Expand Down Expand Up @@ -2778,12 +2871,22 @@ def update_conversation(self, collection: list) -> None:
item_widget.message.text() != conversation_item.content
) and conversation_item.content:
item_widget.message.setText(conversation_item.content)

# Keep reply sender information up-to-date
if isinstance(item_widget, ReplyWidget):
self.controller.session.refresh(conversation_item.journalist)
if self.controller.authenticated_user == conversation_item.journalist:
current_user = True
self.controller.update_authenticated_user.emit(conversation_item.journalist)
else:
current_user = False
item_widget.sender_icon.set_sender(conversation_item.journalist, current_user)
else:
# add a new item to be displayed.
if isinstance(conversation_item, Message):
self.add_message(conversation_item, index)
elif isinstance(conversation_item, (DraftReply, Reply)):
self.add_reply(conversation_item, index)
self.add_reply(conversation_item, conversation_item.journalist, index)
else:
self.add_file(conversation_item, index)

Expand Down Expand Up @@ -2839,7 +2942,7 @@ def add_message(self, message: Message, index) -> None:
self.current_messages[message.uuid] = conversation_item
self.conversation_updated.emit()

def add_reply(self, reply: Union[DraftReply, Reply], index) -> None:
def add_reply(self, reply: Union[DraftReply, Reply], sender: User, index: int) -> None:
"""
Add a reply from a journalist to the source.
"""
Expand All @@ -2848,8 +2951,16 @@ def add_reply(self, reply: Union[DraftReply, Reply], index) -> None:
except AttributeError:
send_status = "SUCCEEDED"

logger.debug("adding reply: with status {}".format(send_status))
if (
self.controller.authenticated_user
and self.controller.authenticated_user.id == reply.journalist_id
):
sender_is_current_user = True
else:
sender_is_current_user = False

conversation_item = ReplyWidget(
self.controller,
reply.uuid,
str(reply),
send_status,
Expand All @@ -2858,17 +2969,25 @@ def add_reply(self, reply: Union[DraftReply, Reply], index) -> None:
self.controller.reply_succeeded,
self.controller.reply_failed,
index,
sender,
sender_is_current_user,
getattr(reply, "download_error", None) is not None,
)

self.scroll.add_widget_to_conversation(index, conversation_item, Qt.AlignRight)
self.current_messages[reply.uuid] = conversation_item

def add_reply_from_reply_box(self, uuid: str, content: str) -> None:
"""
Add a reply from the reply box.
"""
if not self.controller.authenticated_user:
logger.error("User is no longer authenticated so cannot send reply.")
return

index = len(self.current_messages)
conversation_item = ReplyWidget(
self.controller,
uuid,
content,
"PENDING",
Expand All @@ -2877,6 +2996,8 @@ def add_reply_from_reply_box(self, uuid: str, content: str) -> None:
self.controller.reply_succeeded,
self.controller.reply_failed,
index,
self.controller.authenticated_user,
True,
)
self.scroll.add_widget_to_conversation(index, conversation_item, Qt.AlignRight)
self.current_messages[uuid] = conversation_item
Expand Down Expand Up @@ -3049,6 +3170,7 @@ def send_reply(self) -> None:
self.controller.send_reply(self.source.uuid, reply_uuid, reply_text)
self.reply_sent.emit(self.source.uuid, reply_uuid, reply_text)

@pyqtSlot(bool)
def _on_authentication_changed(self, authenticated: bool) -> None:
try:
self.update_authentication_state(authenticated)
Expand Down
Loading

0 comments on commit fe703fe

Please sign in to comment.