Skip to content

Commit

Permalink
Opens message links popup menu
Browse files Browse the repository at this point in the history
Pressing enter on one of the buttons in links popup menu
does one of the following, depending on the link:
- narrow to stream
- narrow to user
- open image in system's default viewer
- open link in system's default browser

Narrows to stream/user and opens images

Narrows to stream/user successfuly. Opens selected image in default viewer.
Gives 401 error while trying to open gifs or videos.

Opens links in default browser

Opens links in default browser (code from zulip#397).
Tries to download and open user uploaded media in default system viewer
(code from zulip#359), otherwise opens link in default browser.

Opens images/gifs/links in default system viewer

To fix: Issue with videos
  • Loading branch information
shreyamalviya committed Jul 17, 2019
1 parent 4de3733 commit 51515e1
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 5 deletions.
4 changes: 4 additions & 0 deletions zulipterminal/config/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@
'keys': {'ctrl l'},
'help_text': 'Clear message',
}),
('MSG_LINKS', {
'keys': {'v'},
'help_text': 'View all links in the current message',
}),
]) # type: OrderedDict[str, KeyBinding]


Expand Down
18 changes: 16 additions & 2 deletions zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
import time
import signal
from functools import partial
import webbrowser

import urwid
import zulip

from zulipterminal.version import ZT_VERSION
from zulipterminal.helper import asynch
from zulipterminal.helper import asynch, suppress_output
from zulipterminal.model import Model, GetMessagesArgs, ServerConnectionFailure
from zulipterminal.ui import View, Screen
from zulipterminal.ui_tools.utils import create_msg_box_list
from zulipterminal.ui_tools.views import HelpView, MsgInfoView
from zulipterminal.ui_tools.views import HelpView, MsgInfoView, MsgLinksView
from zulipterminal.config.themes import ThemeSpec
from zulipterminal.ui_tools.views import PopUpConfirmationView

Expand Down Expand Up @@ -115,6 +116,10 @@ def show_msg_info(self, msg: Any) -> None:
self.show_pop_up(msg_info_view,
"Message Information (up/down scrolls)")

def show_msg_links(self, msg: Any):
msg_links_view = MsgLinksView(self, msg)
self.show_pop_up(msg_links_view, "Message Links (up/down scrolls)")

def search_messages(self, text: str) -> None:
# Search for a text in messages
self.model.set_narrow(search=text)
Expand Down Expand Up @@ -245,6 +250,15 @@ def show_all_messages(self, button: Any) -> None:

self._finalize_show(w_list)

def view_in_browser(self, url) -> None:
if (sys.platform != 'darwin' and sys.platform[:3] != 'win' and
not os.environ.get('DISPLAY') and os.environ.get('TERM')):
# Don't try to open web browser if running without a GUI
return
with suppress_output():
# Suppress anything on stdout or stderr when opening the browser
webbrowser.open_new(url)

def show_all_pm(self, button: Any) -> None:
already_narrowed = self.model.set_narrow(pms=True)
if already_narrowed:
Expand Down
62 changes: 61 additions & 1 deletion zulipterminal/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
from threading import Thread
from typing import (
Any, Dict, List, Set, Tuple, Optional, DefaultDict, FrozenSet, Union,
Iterable, Callable
Iterator, Iterable, Callable
)
from mypy_extensions import TypedDict
from contextlib import contextmanager

import os
import sys
import requests
import subprocess
import tempfile

Message = Dict[str, Any]

Expand Down Expand Up @@ -393,3 +398,58 @@ def powerset(iterable: Iterable[Any],
s = list(iterable)
powerset = chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
return list(map(map_func, list(powerset)))


@contextmanager
def suppress_output() -> Iterator[Any]:
"""
Context manager to redirect stdout and stderr to /dev/null.
Adapted from https://stackoverflow.com/a/2323563
"""
out = os.dup(1)
err = os.dup(2)
os.close(1)
os.close(2)
os.open(os.devnull, os.O_RDWR)
try:
yield
finally:
os.dup2(out, 1)
os.dup2(err, 2)


def open_media(controller, url: str):
# Uploaded media
if 'user_uploads' in url:
# Tries to download media and open in default viewer
# If can't, opens in browser
img_name = url.split("/")[-1]
img_dir_path = os.path.join(tempfile.gettempdir(),
"ZulipTerminal_media")
img_path = os.path.join(img_dir_path, img_name)
try:
os.mkdir(img_dir_path)
except FileExistsError:
pass
with requests.get(url, stream=True,
auth=requests.auth.HTTPBasicAuth(
controller.client.email,
controller.client.api_key)) as r:
if r.status_code == 200:
with open(img_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
r.close()

if os.path.exists(img_path):
imageViewerFromCommandLine = {'linux': 'xdg-open',
'win32': 'explorer',
'darwin': 'open'}[sys.platform]
subprocess.run([imageViewerFromCommandLine, img_path])
else:
controller.view_in_browser(url)

# Other urls
else:
controller.view_in_browser(url)
9 changes: 8 additions & 1 deletion zulipterminal/ui_tools/boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def __init__(self, message: Dict[str, Any], model: Any,
self.model = model
self.message = message
self.stream_name = ''
self.message['links'] = dict() # type: Dict
self.stream_id = None # type: Union[int, None]
self.topic_name = ''
self.email = ''
Expand Down Expand Up @@ -370,6 +371,7 @@ def soup2markup(self, soup: Any) -> List[Any]:
'user-group-mention' in element.attrs.get('class', [])):
# USER MENTIONS & USER-GROUP MENTIONS
markup.append(('span', element.text))
self.message['links'][element.text] = element.text
elif element.name == 'a':
# LINKS
link = element.attrs['href']
Expand All @@ -380,12 +382,15 @@ def soup2markup(self, soup: Any) -> List[Any]:
# a link then just display the link
markup.append(text)
else:
if link.startswith('user_uploads/'):
link = self.model.server_url + link
if link.startswith('/user_uploads/'):
# Append org url to before user_uploads to convert it
# into a link.
link = self.model.server_url + link
link = self.model.server_url + link[1:]
markup.append(
('link', '[' + text + ']' + '(' + link + ')'))
self.message['links'][text] = link
elif element.name == 'blockquote':
# BLOCKQUOTE TEXT
markup.append((
Expand Down Expand Up @@ -681,6 +686,8 @@ def keypress(self, size: Tuple[int, int], key: str) -> str:
self.model.controller.view.middle_column.set_focus('footer')
elif is_command_key('MSG_INFO', key):
self.model.controller.show_msg_info(self.message)
elif is_command_key('MSG_LINKS', key):
self.model.controller.show_msg_links(self.message)
return key


Expand Down
8 changes: 8 additions & 0 deletions zulipterminal/ui_tools/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ def keypress(self, size: Tuple[int, int], key: str) -> str:
return super().keypress(size, key)


class MsgLinkButton(TopButton):
def __init__(self, controller: Any, width: int, count: int,
caption: str, function: Callable) -> None:
super().__init__(controller, caption,
function, count=count,
width=width)


class HomeButton(TopButton):
def __init__(self, controller: Any, width: int, count: int=0) -> None:
button_text = ("All messages [" +
Expand Down
74 changes: 73 additions & 1 deletion zulipterminal/ui_tools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import time

from zulipterminal.config.keys import KEY_BINDINGS, is_command_key
from zulipterminal.helper import asynch, match_user
from zulipterminal.helper import asynch, match_user, open_media
from zulipterminal.ui_tools.buttons import (
TopicButton,
UnreadPMButton,
Expand All @@ -18,6 +18,7 @@
)
from zulipterminal.ui_tools.utils import create_msg_box_list
from zulipterminal.ui_tools.boxes import UserSearchBox, StreamSearchBox
from zulipterminal.ui_tools.buttons import MsgLinkButton


class ModListWalker(urwid.SimpleFocusListWalker):
Expand Down Expand Up @@ -799,3 +800,74 @@ def keypress(self, size: Tuple[int, int], key: str) -> str:
if is_command_key('GO_BACK', key) or is_command_key('MSG_INFO', key):
self.controller.exit_popup()
return super(MsgInfoView, self).keypress(size, key)


class MsgLinksView(urwid.ListBox):
def __init__(self, controller: Any, msg: Any):
self.controller = controller
self.msg = msg

self.width = 70
self.height = 10

self.log = []
for url in msg['links']:
if not url.startswith('/user_uploads'):
self.log.append(MsgLinkButton(self.controller,
self.width,
0,
url,
self.perform_action))

super(MsgLinksView, self).__init__(self.log)

def perform_action(self, button):
# If stream
if self.msg['links'][button._caption].startswith('/#narrow'):
found_stream = False
for stream_details in self.controller.model.stream_dict.values():
# Check mentioned stream with all subscribed streams
if stream_details['name'] == button._caption[1:]:
# Stream button to call function to narrow to stream
req_details = ['name', 'stream_id', 'color', 'invite_only']
stream = [stream_details[key] for key in req_details]
btn = StreamButton(stream,
controller=self.controller,
view=self.controller.view,
width=self.width,
count=0)
self.controller.narrow_to_stream(btn)
found_stream = True
break
if not found_stream:
self.controller.view.set_footer_text(
"You are not subscribed to this stream.", 3)

# If user
elif self.msg['links'][button._caption].startswith('@'):
found_user = False
for user in self.controller.model.users:
if user['full_name'] == button._caption[1:]:
btn = UserButton(user,
controller=self.controller,
view=self.controller.view,
width=self.width,
color=user['status'],
count=0)
self.controller.narrow_to_user(btn)
btn._narrow_with_compose(btn)
found_user = True
break
if not found_user:
self.controller.view.set_footer_text(
"User not found in realm.\
Their account may be deactivated.", 3)

# If link (uploaded media or other webpage)
else:
open_media(self.controller, self.msg['links'][button._caption])

def keypress(self, size: Tuple[int, int], key: str) -> str:
if is_command_key('GO_BACK', key) or is_command_key('MSG_LINKS', key):
self.controller.exit_popup()
return super(MsgLinksView, self).keypress(size, key)

0 comments on commit 51515e1

Please sign in to comment.