Skip to content

Commit 9eaec46

Browse files
author
Allie Crevier
committed
keep user auth and reply badges up-to-date
1 parent 7318a35 commit 9eaec46

16 files changed

+785
-243
lines changed

securedrop_client/db.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
import os
33
from enum import Enum
4-
from typing import Any, List, Union # noqa: F401
4+
from typing import Any, List, Optional, Union # noqa: F401
55

66
from sqlalchemy import (
77
Boolean,
@@ -280,7 +280,7 @@ class Reply(Base):
280280
)
281281

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

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

464464
@property
465-
def fullname(self) -> str:
465+
def deleted(self) -> bool:
466+
return True if self.uuid == "deleted" else False
467+
468+
@property
469+
def fullname(self) -> Optional[str]:
470+
if self.deleted:
471+
return None
466472
if self.firstname and self.lastname:
467473
return self.firstname + " " + self.lastname
468474
elif self.firstname:
@@ -473,7 +479,9 @@ def fullname(self) -> str:
473479
return self.username
474480

475481
@property
476-
def initials(self) -> str:
482+
def initials(self) -> Optional[str]:
483+
if self.deleted:
484+
return None
477485
if self.firstname and self.lastname:
478486
return self.firstname[0].lower() + self.lastname[0].lower()
479487
elif self.firstname and len(self.firstname) >= 2:

securedrop_client/gui/widgets.py

Lines changed: 147 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -427,11 +427,17 @@ def __init__(self):
427427
layout.addWidget(self.user_button, alignment=Qt.AlignTop)
428428

429429
def setup(self, window, controller):
430+
self.controller = controller
431+
self.controller.update_authenticated_user.connect(self._on_update_authenticated_user)
430432
self.user_button.setup(controller)
431433
self.login_button.setup(window)
432434

435+
@pyqtSlot(User)
436+
def _on_update_authenticated_user(self, db_user: User) -> None:
437+
self.set_user(db_user)
438+
433439
def set_user(self, db_user: User):
434-
self.user_icon.setText(_(db_user.initials))
440+
self.user_icon.setText(db_user.initials)
435441
self.user_button.set_username(db_user.fullname)
436442

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

16411647

1648+
class SenderIcon(QWidget):
1649+
"""
1650+
Represents a reply to a source.
1651+
"""
1652+
1653+
SENDER_ICON_CSS = load_css("sender_icon.css")
1654+
1655+
def __init__(self) -> None:
1656+
super().__init__()
1657+
self.sender_is_current_user = False
1658+
self.setObjectName("SenderIcon")
1659+
self.setStyleSheet(self.SENDER_ICON_CSS)
1660+
self.setFixedSize(QSize(48, 48))
1661+
font = QFont()
1662+
font.setLetterSpacing(QFont.AbsoluteSpacing, 0.58)
1663+
self.label = QLabel()
1664+
self.label.setAlignment(Qt.AlignCenter)
1665+
self.label.setFont(font)
1666+
layout = QVBoxLayout()
1667+
layout.setContentsMargins(0, 0, 0, 0)
1668+
layout.setSpacing(0)
1669+
layout.addWidget(self.label)
1670+
self.setLayout(layout)
1671+
1672+
def set_sender(self, sender: User, sender_is_current_user: bool) -> None:
1673+
self.sender_is_current_user = sender_is_current_user
1674+
if not sender or not sender.initials:
1675+
self.label.setPixmap(load_image("deleted-user.png"))
1676+
else:
1677+
self.label.setText(sender.initials)
1678+
1679+
def set_normal_styles(self):
1680+
self.setStyleSheet("")
1681+
if self.sender_is_current_user:
1682+
self.setObjectName("SenderIcon_current_user")
1683+
else:
1684+
self.setObjectName("SenderIcon")
1685+
self.setStyleSheet(self.SENDER_ICON_CSS)
1686+
1687+
def set_failed_styles(self):
1688+
self.setStyleSheet("")
1689+
self.setObjectName("SenderIcon_failed")
1690+
self.setStyleSheet(self.SENDER_ICON_CSS)
1691+
1692+
def set_pending_styles(self):
1693+
self.setStyleSheet("")
1694+
if self.sender_is_current_user:
1695+
self.setObjectName("SenderIcon_current_user_pending")
1696+
else:
1697+
self.setObjectName("SenderIcon_pending")
1698+
self.setStyleSheet(self.SENDER_ICON_CSS)
1699+
1700+
16421701
class SpeechBubble(QWidget):
16431702
"""
16441703
Represents a speech bubble that's part of a conversation between a source
@@ -1682,6 +1741,10 @@ def __init__(
16821741
self.color_bar.setObjectName("SpeechBubble_status_bar")
16831742
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)
16841743

1744+
# User icon
1745+
self.sender_icon = SenderIcon()
1746+
self.sender_icon.hide()
1747+
16851748
# Speech bubble
16861749
self.speech_bubble = QWidget()
16871750
self.speech_bubble.setObjectName("SpeechBubble_container")
@@ -1698,6 +1761,7 @@ def __init__(
16981761
self.bubble_area_layout = QHBoxLayout()
16991762
self.bubble_area_layout.setContentsMargins(0, self.TOP_MARGIN, 0, self.BOTTOM_MARGIN)
17001763
bubble_area.setLayout(self.bubble_area_layout)
1764+
self.bubble_area_layout.addWidget(self.sender_icon, alignment=Qt.AlignBottom)
17011765
self.bubble_area_layout.addWidget(self.speech_bubble)
17021766

17031767
# Add widget to layout
@@ -1779,6 +1843,7 @@ class ReplyWidget(SpeechBubble):
17791843

17801844
def __init__(
17811845
self,
1846+
controller: Controller,
17821847
message_uuid: str,
17831848
message: str,
17841849
reply_status: str,
@@ -1787,11 +1852,16 @@ def __init__(
17871852
message_succeeded_signal,
17881853
message_failed_signal,
17891854
index: int,
1855+
sender: User,
1856+
sender_is_current_user: bool,
17901857
error: bool = False,
17911858
) -> None:
17921859
super().__init__(message_uuid, message, update_signal, download_error_signal, index, error)
1860+
self.controller = controller
1861+
self.status = reply_status
17931862
self.uuid = message_uuid
1794-
1863+
self.sender = sender
1864+
self.sender_is_current_user = sender_is_current_user
17951865
self.error = QWidget()
17961866
error_layout = QHBoxLayout()
17971867
error_layout.setContentsMargins(0, 0, 0, 0)
@@ -1806,18 +1876,21 @@ def __init__(
18061876
self.error.hide()
18071877

18081878
self.bubble_area_layout.addWidget(self.error)
1879+
self.sender_icon.set_sender(sender, self.sender_is_current_user)
1880+
self.sender_icon.show()
18091881

18101882
message_succeeded_signal.connect(self._on_reply_success)
18111883
message_failed_signal.connect(self._on_reply_failure)
1884+
self.controller.update_authenticated_user.connect(self._on_update_authenticated_user)
18121885

1813-
if reply_status == "SUCCEEDED":
1814-
self.set_normal_styles()
1815-
self.error.hide()
1816-
elif reply_status == "FAILED":
1817-
self.set_failed_styles()
1818-
self.error.show()
1819-
elif reply_status == "PENDING":
1820-
self.set_pending_styles()
1886+
self._set_reply_state()
1887+
1888+
@pyqtSlot(User)
1889+
def _on_update_authenticated_user(self, db_user: User) -> None:
1890+
sender_is_current_user = True if db_user.uuid == self.sender.uuid else False
1891+
self.sender_is_current_user = sender_is_current_user
1892+
self.sender_icon.sender_is_current_user = sender_is_current_user
1893+
self._set_reply_state()
18211894

18221895
@pyqtSlot(str, str, str)
18231896
def _on_reply_success(self, source_id: str, message_uuid: str, content: str) -> None:
@@ -1826,8 +1899,8 @@ def _on_reply_success(self, source_id: str, message_uuid: str, content: str) ->
18261899
signal matches the uuid of this widget.
18271900
"""
18281901
if message_uuid == self.uuid:
1829-
self.set_normal_styles()
1830-
self.error.hide()
1902+
self.status = "SUCCEEDED"
1903+
self._set_reply_state()
18311904

18321905
@pyqtSlot(str)
18331906
def _on_reply_failure(self, message_uuid: str) -> None:
@@ -1836,31 +1909,54 @@ def _on_reply_failure(self, message_uuid: str) -> None:
18361909
signal matches the uuid of this widget.
18371910
"""
18381911
if message_uuid == self.uuid:
1912+
self.status = "FAILED"
1913+
self._set_reply_state()
1914+
1915+
def _set_reply_state(self) -> None:
1916+
if self.status == "SUCCEEDED":
1917+
self.set_normal_styles()
1918+
self.error.hide()
1919+
elif self.status == "PENDING":
1920+
self.set_pending_styles()
1921+
elif self.status == "FAILED":
18391922
self.set_failed_styles()
18401923
self.error.show()
18411924

18421925
def set_normal_styles(self):
18431926
self.message.setStyleSheet("")
18441927
self.message.setObjectName("SpeechBubble_message")
18451928
self.message.setStyleSheet(self.MESSAGE_CSS)
1929+
1930+
self.sender_icon.set_normal_styles()
1931+
18461932
self.color_bar.setStyleSheet("")
1847-
self.color_bar.setObjectName("ReplyWidget_status_bar")
1933+
if self.sender_is_current_user:
1934+
self.color_bar.setObjectName("ReplyWidget_status_bar_current_user")
1935+
else:
1936+
self.color_bar.setObjectName("ReplyWidget_status_bar")
18481937
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)
18491938

1850-
def set_failed_styles(self):
1939+
def set_pending_styles(self):
18511940
self.message.setStyleSheet("")
1852-
self.message.setObjectName("ReplyWidget_message_failed")
1941+
self.message.setObjectName("ReplyWidget_message_pending")
18531942
self.message.setStyleSheet(self.MESSAGE_CSS)
1943+
1944+
self.sender_icon.set_pending_styles()
1945+
18541946
self.color_bar.setStyleSheet("")
1855-
self.color_bar.setObjectName("ReplyWidget_status_bar_failed")
1947+
if self.sender_is_current_user:
1948+
self.color_bar.setObjectName("ReplyWidget_status_bar_pending_current_user")
1949+
else:
1950+
self.color_bar.setObjectName("ReplyWidget_status_bar_pending")
18561951
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)
18571952

1858-
def set_pending_styles(self):
1953+
def set_failed_styles(self):
18591954
self.message.setStyleSheet("")
1860-
self.message.setObjectName("ReplyWidget_message_pending")
1955+
self.message.setObjectName("ReplyWidget_message_failed")
18611956
self.message.setStyleSheet(self.MESSAGE_CSS)
1957+
self.sender_icon.set_failed_styles()
18621958
self.color_bar.setStyleSheet("")
1863-
self.color_bar.setObjectName("ReplyWidget_status_bar_pending")
1959+
self.color_bar.setObjectName("ReplyWidget_status_bar_failed")
18641960
self.color_bar.setStyleSheet(self.STATUS_BAR_CSS)
18651961

18661962

@@ -2775,12 +2871,22 @@ def update_conversation(self, collection: list) -> None:
27752871
item_widget.message.text() != conversation_item.content
27762872
) and conversation_item.content:
27772873
item_widget.message.setText(conversation_item.content)
2874+
2875+
# Keep reply sender information up-to-date
2876+
if isinstance(item_widget, ReplyWidget):
2877+
self.controller.session.refresh(conversation_item.journalist)
2878+
if self.controller.authenticated_user == conversation_item.journalist:
2879+
current_user = True
2880+
self.controller.update_authenticated_user.emit(conversation_item.journalist)
2881+
else:
2882+
current_user = False
2883+
item_widget.sender_icon.set_sender(conversation_item.journalist, current_user)
27782884
else:
27792885
# add a new item to be displayed.
27802886
if isinstance(conversation_item, Message):
27812887
self.add_message(conversation_item, index)
27822888
elif isinstance(conversation_item, (DraftReply, Reply)):
2783-
self.add_reply(conversation_item, index)
2889+
self.add_reply(conversation_item, conversation_item.journalist, index)
27842890
else:
27852891
self.add_file(conversation_item, index)
27862892

@@ -2836,7 +2942,7 @@ def add_message(self, message: Message, index) -> None:
28362942
self.current_messages[message.uuid] = conversation_item
28372943
self.conversation_updated.emit()
28382944

2839-
def add_reply(self, reply: Union[DraftReply, Reply], index) -> None:
2945+
def add_reply(self, reply: Union[DraftReply, Reply], sender: User, index: int) -> None:
28402946
"""
28412947
Add a reply from a journalist to the source.
28422948
"""
@@ -2845,8 +2951,16 @@ def add_reply(self, reply: Union[DraftReply, Reply], index) -> None:
28452951
except AttributeError:
28462952
send_status = "SUCCEEDED"
28472953

2848-
logger.debug("adding reply: with status {}".format(send_status))
2954+
if (
2955+
self.controller.authenticated_user
2956+
and self.controller.authenticated_user.id == reply.journalist_id
2957+
):
2958+
sender_is_current_user = True
2959+
else:
2960+
sender_is_current_user = False
2961+
28492962
conversation_item = ReplyWidget(
2963+
self.controller,
28502964
reply.uuid,
28512965
str(reply),
28522966
send_status,
@@ -2855,17 +2969,25 @@ def add_reply(self, reply: Union[DraftReply, Reply], index) -> None:
28552969
self.controller.reply_succeeded,
28562970
self.controller.reply_failed,
28572971
index,
2972+
sender,
2973+
sender_is_current_user,
28582974
getattr(reply, "download_error", None) is not None,
28592975
)
2976+
28602977
self.scroll.add_widget_to_conversation(index, conversation_item, Qt.AlignRight)
28612978
self.current_messages[reply.uuid] = conversation_item
28622979

28632980
def add_reply_from_reply_box(self, uuid: str, content: str) -> None:
28642981
"""
28652982
Add a reply from the reply box.
28662983
"""
2984+
if not self.controller.authenticated_user:
2985+
logger.error("User is no longer authenticated so cannot send reply.")
2986+
return
2987+
28672988
index = len(self.current_messages)
28682989
conversation_item = ReplyWidget(
2990+
self.controller,
28692991
uuid,
28702992
content,
28712993
"PENDING",
@@ -2874,6 +2996,8 @@ def add_reply_from_reply_box(self, uuid: str, content: str) -> None:
28742996
self.controller.reply_succeeded,
28752997
self.controller.reply_failed,
28762998
index,
2999+
self.controller.authenticated_user,
3000+
True,
28773001
)
28783002
self.scroll.add_widget_to_conversation(index, conversation_item, Qt.AlignRight)
28793003
self.current_messages[uuid] = conversation_item
@@ -3046,6 +3170,7 @@ def send_reply(self) -> None:
30463170
self.controller.send_reply(self.source.uuid, reply_uuid, reply_text)
30473171
self.reply_sent.emit(self.source.uuid, reply_uuid, reply_text)
30483172

3173+
@pyqtSlot(bool)
30493174
def _on_authentication_changed(self, authenticated: bool) -> None:
30503175
try:
30513176
self.update_authentication_state(authenticated)

0 commit comments

Comments
 (0)