Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove user refresh and replace with sync icon #732

Merged
merged 5 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion securedrop_client/api_jobs/downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ class MetadataSyncJob(ApiJob):
Update source metadata such that new download jobs can be added to the queue.
'''

NUMBER_OF_TIMES_TO_RETRY_AN_API_CALL = 15

def __init__(self, data_dir: str, gpg: GpgHelper) -> None:
super().__init__()
super().__init__(remaining_attempts=self.NUMBER_OF_TIMES_TO_RETRY_AN_API_CALL)
self.data_dir = data_dir
self.gpg = gpg

Expand Down
87 changes: 33 additions & 54 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,8 @@ def __init__(self):
layout.setContentsMargins(10, 0, 0, 0)
layout.setSpacing(0)

# Refresh button
self.refresh = RefreshButton()
self.refresh.disable()
# Sync icon
self.sync_icon = SyncIcon()

# Activity status bar
self.activity_status_bar = ActivityStatusBar()
Expand All @@ -89,35 +88,35 @@ def __init__(self):
# Create space the size of the status bar to keep the error status bar centered
spacer = QWidget()

# Create space ths size of the refresh button to keep the error status bar centered
# Create space ths size of the sync icon to keep the error status bar centered
spacer2 = QWidget()
spacer2.setFixedWidth(42)

# Set height of top pane to 42 pixels
self.setFixedHeight(42)
self.refresh.setFixedHeight(42)
self.sync_icon.setFixedHeight(42)
self.activity_status_bar.setFixedHeight(42)
self.error_status_bar.setFixedHeight(42)
spacer.setFixedHeight(42)
spacer2.setFixedHeight(42)

# Add widgets to layout
layout.addWidget(self.refresh, 1)
layout.addWidget(self.sync_icon, 1)
layout.addWidget(self.activity_status_bar, 1)
layout.addWidget(self.error_status_bar, 1)
layout.addWidget(spacer, 1)
layout.addWidget(spacer2, 1)

def setup(self, controller):
self.refresh.setup(controller)
self.sync_icon.setup(controller)
self.error_status_bar.setup(controller)

def set_logged_in(self):
self.refresh.enable()
self.sync_icon.enable()
self.setPalette(self.online_palette)

def set_logged_out(self):
self.refresh.disable()
self.sync_icon.disable()
self.setPalette(self.offline_palette)

def update_activity_status(self, message: str, duration: int):
Expand Down Expand Up @@ -192,74 +191,54 @@ def set_logged_out(self):
self.logo.setPalette(self.offline_palette)


class RefreshButton(SvgPushButton):
class SyncIcon(QLabel):
"""
A button that shows an icon for different refresh states.
An icon that shows sync state.
"""

CSS = '''
#refresh_button {
#sync_icon {
border: none;
color: #fff;
}
'''

def __init__(self):
# Add svg images to button
super().__init__(
normal='refresh.svg',
disabled='refresh_offline.svg',
active='refresh_active.svg',
selected='refresh.svg',
svg_size=QSize(16, 16))

self.active = False

# Set css id
self.setObjectName('refresh_button')

# Set styles
super().__init__()
self.setObjectName('sync_icon')
self.setStyleSheet(self.CSS)
self.setFixedSize(QSize(20, 20))

# Click event handler
self.clicked.connect(self._on_clicked)
self.setFixedSize(QSize(24, 20))
self.sync_animation = load_movie("sync_disabled.gif")
self.sync_animation.setScaledSize(QSize(24, 20))
self.setMovie(self.sync_animation)
self.sync_animation.start()

def setup(self, controller):
"""
Assign a controller object (containing the application logic).
"""
self.controller = controller
self.controller.sync_events.connect(self._on_refresh_complete)

def _on_clicked(self):
if self.active:
return
self.controller.sync_events.connect(self._on_sync)

self.controller.sync_api(manual_refresh=True)

# This is a temporary solution for showing the icon as active for the entire duration of a
# refresh, rather than for just the duration of a click. The icon image will be replaced
# when the controller tells us the refresh has finished. A cleaner solution would be to
# store and update our own icon mode so we don't have to reload any images.
self.setIcon(load_icon(normal='refresh_active.svg', disabled='refresh_offline.svg'))
self.active = True

def _on_refresh_complete(self, data):
if (data == 'synced'):
self.setIcon(load_icon(
normal='refresh.svg',
disabled='refresh_offline.svg',
active='refresh_active.svg',
selected='refresh.svg'))
self.active = False
def _on_sync(self, data):
if data == 'syncing':
self.sync_animation = load_movie("sync_active.gif")
self.sync_animation.setScaledSize(QSize(24, 20))
self.setMovie(self.sync_animation)
self.sync_animation.start()

def enable(self):
self.setEnabled(True)
self.active is False
self.sync_animation = load_movie("sync.gif")
self.sync_animation.setScaledSize(QSize(24, 20))
self.setMovie(self.sync_animation)
self.sync_animation.start()

def disable(self):
self.setEnabled(False)
self.sync_animation = load_movie("sync_disabled.gif")
self.sync_animation.setScaledSize(QSize(24, 20))
self.setMovie(self.sync_animation)
self.sync_animation.start()


class ActivityStatusBar(QStatusBar):
Expand Down
28 changes: 3 additions & 25 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,30 +369,18 @@ def authenticated(self):
"""
return bool(self.api and self.api.token is not None)

def sync_api(self, manual_refresh: bool = False):
def sync_api(self):
"""
Grab data from the remote SecureDrop API in a non-blocking manner.
"""
logger.debug("In sync_api on thread {}".format(self.thread().currentThreadId()))
self.sync_events.emit('syncing')

if self.authenticated():
self.sync_events.emit('syncing')
logger.debug("You are authenticated, going to make your call")

job = MetadataSyncJob(self.data_dir, self.gpg)
job.success_signal.connect(self.on_sync_success, type=Qt.QueuedConnection)

# If the sync did not originate from a manual refrsh, increase the number of
# retry attempts (remaining_attempts) to 15, otherwise use the default so that a user
# finds out quicker whether or not their refresh-attempt failed.
#
# Set up failure-handling depending on whether or not the sync originated from a manual
# refresh.
if manual_refresh:
job.failure_signal.connect(self.on_refresh_failure, type=Qt.QueuedConnection)
else:
job.remaining_attempts = 15
job.failure_signal.connect(self.on_sync_failure, type=Qt.QueuedConnection)
job.failure_signal.connect(self.on_sync_failure, type=Qt.QueuedConnection)

self.api_job_queue.enqueue(job)

Expand Down Expand Up @@ -441,16 +429,6 @@ def on_sync_failure(self, result: Exception) -> None:
duration=0,
retry=True)

def on_refresh_failure(self, result: Exception) -> None:
"""
Called when syncronisation of data via the API fails after a user manual clicks refresh.
"""
logger.debug('The SecureDrop server cannot be reached due to Error: {}'.format(result))
self.gui.update_error_status(
_('The SecureDrop server cannot be reached.'),
duration=0,
retry=True)

def update_sync(self):
"""
Updates the UI to show human time of last sync.
Expand Down
Binary file added securedrop_client/resources/images/sync.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions securedrop_client/resources/images/sync.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading