Skip to content
Closed
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: 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
63 changes: 62 additions & 1 deletion zulipterminal/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,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 @@ -410,3 +415,59 @@ def canonicalize_color(color: str) -> str:
return color.lower()
else:
raise ValueError('Unknown format for color "{}"'.format(color))


@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()

try:
imageViewerFromCommandLine = {'linux': 'xdg-open',
'win32': 'explorer',
'darwin': 'open'}[sys.platform]
with suppress_output():
subprocess.run([imageViewerFromCommandLine, img_path])
except(FileNotFoundError):
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
80 changes: 79 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 @@ -823,3 +824,80 @@ 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 self.msg['links']:
if not url.startswith('/user_uploads'):
if len(url) > 0:
self.log.append(MsgLinkButton(self.controller,
self.width,
0,
url,
self.perform_action))
else:
self.log.append(MsgLinkButton(self.controller,
self.width,
0,
self.msg['links'][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)