plchat/plchat.py

1569 lines
59 KiB
Python
Raw Normal View History

#PlChat is a pleroma chat client
# Copyright (C) 2021 Knott Eye
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import platform, keyring
os = platform.system()
if os == "Windows":
import keyring.backends.Windows
keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring())
elif os == "Darwin":
import keyring.backends.OS_X
keyring.set_keyring(keyring.backends.OS_X.Keyring())
else:
# Probably linux or BSD so we'll just go for it
import keyring.backends.SecretService
keyring.set_keyring(keyring.backends.SecretService.Keyring())
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
2021-04-15 15:16:38 -05:00
from notifypy import Notify
import sys, threading, queue, asyncio, urllib, appdirs, os, time, pleroma, re, magic, monkeypatch, requests, misc, json, timeconvert, videowidget, audiowidget
2021-04-15 15:16:38 -05:00
CACHE = appdirs.AppDirs('plchat', 'plchat').user_cache_dir
APPDATA = appdirs.AppDirs('plchat', 'plchat').user_data_dir
THREADS = {}
STATIC_PREF = ''
2021-04-17 11:13:26 -05:00
ICON_PATH = os.path.join(os.path.dirname(__file__), "fedi.svg")
ICON_PATH_COLOR = os.path.join(os.path.dirname(__file__), "fedi_color.svg")
NOTIF_SOUND = os.path.join(os.path.dirname(__file__), 'notif.wav')
2021-04-15 15:16:38 -05:00
class App(QMainWindow):
settings = QSettings(APPDATA+"/settings.ini")
2021-04-15 15:16:38 -05:00
_eventloop = asyncio.new_event_loop()
accts = {}
def __init__(self, processEvents):
super().__init__()
self.title = 'PlChat'
self.processEvents = processEvents
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
if self.settings.value('colorIcon', type=bool):
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
2021-04-15 15:16:38 -05:00
self.setGeometry(self.settings.value('left', type=int) or 10, self.settings.value('top', type=int) or 10, self.settings.value('width', type=int) or 640, self.settings.value('height', type=int) or 480)
self._exit = False
2021-04-17 23:34:34 -05:00
self.totpReady = False
2021-04-15 15:16:38 -05:00
self.Err = QErrorMessage()
exitAction = QAction('&Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setToolTip('Exit application')
exitAction.triggered.connect(self.exitActionTrigger)
newChatAction = QAction('New Chat', self, triggered=self.newChatDialog)
newChatAction.setShortcut('Ctrl+N')
newChatAction.setToolTip('Add a new chat')
closeChatAction = QAction('Close Chat', self, triggered=self.closeTab)
closeChatAction.setShortcut('Ctrl+W')
closeChatAction.setToolTip('Close the current chat tab')
newAcctAction = QAction('New Account', self, triggered=self.newAcctDialog)
newAcctAction.setShortcut('Ctrl+Shift+N')
newAcctAction.setToolTip('Add a new account')
logoutAction = QAction('Logout', self, triggered=self.logout)
logoutAction.setToolTip('Log out of the current account')
reopenAction = QAction('Reopen All Chats', self, triggered=self.reopenAll)
reopenAction.setToolTip('In case something breaks')
contactAction = QAction('Contact', self, triggered=self.contactDialog)
licenseAction = QAction('License', self, triggered=self.licenseDialog)
self.closeToTrayAction = QAction('Close To System Tray', self, checkable=True)
self.closeToTrayAction.setChecked(self.settings.value('closeToTray', type=bool))
self.openInTrayAction = QAction('Open In System Tray', self, checkable=True)
self.openInTrayAction.setChecked(self.settings.value('openInTray', type=bool))
self.openInTrayAction.setToolTip('Does nothing if not also closing to tray.')
self.animatePicturesAction = QAction('Display Animations', self, checkable=True)
self.animatePicturesAction.setChecked(self.settings.value('animatePictures', type=bool))
self.animatePicturesAction.changed.connect(updateAnimationPref)
self.fetchBackgroundsAction = QAction("Fetch Backgrounds", self, checkable=True)
self.fetchBackgroundsAction.setChecked(self.settings.value('fetchBackgrounds', type=bool))
self.fetchBackgroundsAction.setToolTip("Most instances do not set this in a way that PlChat can check for. It will (probably) not work like you expect, but there is nothing I can do about it.")
self.darkModeAction = QAction("Dark Mode", self, checkable=True)
self.darkModeAction.setChecked(self.settings.value('darkMode', type=bool))
self.darkModeAction.setToolTip("Only affects chat bubbles")
self.colorIconAction = QAction("Use Color Icon", self, checkable=True, triggered=self.useColorIcon)
self.colorIconAction.setChecked(self.settings.value('colorIcon', type=bool))
2021-04-15 15:16:38 -05:00
self.sendNotificationsAction = QAction("Send Notifications", self, checkable=True)
self.sendNotificationsAction.setChecked(self.settings.value('sendNotifications', type=bool))
self.silenceNotifsAction = QAction("Silence Notifications", self, checkable=True, triggered=self.silenceNotifs)
self.silenceNotifsAction.setChecked(self.settings.value('silenceNotifications', type=bool))
2021-04-15 15:16:38 -05:00
self.fetchHeadersAction = QAction("Fetch Headers", self, checkable=True)
self.fetchHeadersAction.setChecked(self.settings.value('fetchHeaders', type=bool))
self.twoFourTimeAction = QAction("Display 24-Hour Time", self, checkable=True)
self.twoFourTimeAction.setChecked(self.settings.value('twoFourTime', type=bool))
self.acctComboBox = QComboBox(self)
self.acctComboBox.currentIndexChanged.connect(self.acctSwitch)
acctComboBoxAction = QWidgetAction(self)
acctComboBoxAction.setDefaultWidget(self.acctComboBox)
self.acctComboBox.show()
menubar = self.menuBar()
if self.closeToTrayAction.isChecked():
systraymenu = QMenu()
hideAction = QAction("Hide", self, triggered=self.hide)
showAction = QAction("Show", self, triggered=self.show)
systraymenu.addAction(showAction)
systraymenu.addAction(hideAction)
systraymenu.addAction(self.silenceNotifsAction)
2021-04-15 15:16:38 -05:00
systraymenu.addAction(exitAction)
self.trayIcon = QSystemTrayIcon()
if self.settings.value('colorIcon', type=bool):
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.trayIcon.setIcon(QIcon(ICON_PATH))
2021-04-15 15:16:38 -05:00
self.trayIcon.setVisible(True)
self.trayIcon.setToolTip("PlChat")
self.trayIcon.setContextMenu(systraymenu)
self.trayIcon.activated.connect(self.systrayClicked)
else:
self.trayIcon = None
filemenu = menubar.addMenu("File")
filemenu.addAction(newChatAction)
filemenu.addAction(closeChatAction)
filemenu.addAction(newAcctAction)
filemenu.addAction(exitAction)
editmenu = menubar.addMenu("Edit")
editmenu.addAction(reopenAction)
editmenu.setToolTipsVisible(True)
prefsmenu = editmenu.addMenu("Preferences")
prefsmenu.addAction(self.closeToTrayAction)
prefsmenu.addAction(self.openInTrayAction)
prefsmenu.addAction(self.animatePicturesAction)
prefsmenu.addAction(self.darkModeAction)
prefsmenu.addAction(self.colorIconAction)
2021-04-15 15:16:38 -05:00
prefsmenu.addAction(self.sendNotificationsAction)
prefsmenu.addAction(self.silenceNotifsAction)
2021-04-15 15:16:38 -05:00
prefsmenu.addAction(self.fetchBackgroundsAction)
prefsmenu.addAction(self.fetchHeadersAction)
prefsmenu.addAction(self.twoFourTimeAction)
prefsmenu.setToolTipsVisible(True)
accountmenu = menubar.addMenu('Accounts')
accountmenu.addAction(acctComboBoxAction)
accountmenu.addSeparator()
accountmenu.addAction(logoutAction)
aboutmenu = menubar.addMenu('About')
aboutmenu.addAction(contactAction)
aboutmenu.addAction(licenseAction)
self.tabs = QTabWidget()
self.tabs.setMovable(True)
self.tabs.setTabsClosable(True)
self.tabs.tabCloseRequested.connect(self.closeTab)
self.tabs.currentChanged.connect(self.changeTab)
self.setFocusProxy(self.tabs)
self.setCentralWidget(self.tabs)
if not self.openInTrayAction.isChecked() or not self.closeToTrayAction.isChecked():
self.show()
self.HeaderFont = QFont()
self.HeaderFont.setPointSize(16)
self.headerEmojiFontSize = QFontMetrics(self.HeaderFont).height()
self.defaultFontMetrics = QFontMetrics(QFont())
self.emojiFontSize = self.defaultFontMetrics.height()
self.TimestampFont = QFont()
self.TimestampFont.setPointSize(7)
self.installEventFilter(self)
asyncio.set_event_loop(self._eventloop)
self._eventloop.call_soon(self.eventLoop)
acctList = self.settings.value('acctList', type=list)
if not acctList:
self.newAcctDialog()
return
2021-04-25 15:06:00 -05:00
for ind in range(0,len(acctList)):
CallThread(getAvi, None, acctList[ind]['instance'])
try:
self.initAcct(acctList[ind]['instance'], acctList[ind]['username'])
except:
print("account info corrupted, deleting")
del acctList[ind]
if acctList:
self.settings.setValue('acctList', acctList)
2021-04-15 15:16:38 -05:00
def systrayClicked(self, reason):
if reason == QSystemTrayIcon.Trigger:
if self.isVisible():
self.hide()
else:
self.show()
def silenceNotifs(self, dothing):
if dothing:
# Definitely not supposed to be poking around in the internals like this lul
Notification._notification_audio = None
else:
Notification.audio = NOTIF_SOUND
def useColorIcon(self, dothing):
if dothing:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
if self.trayIcon:
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
if self.trayIcon:
self.trayIcon.setIcon(QIcon(ICON_PATH))
2021-04-15 15:16:38 -05:00
def eventLoop(self):
# Custom event loop to process queue events
self.processEvents()
self._eventloop.call_soon(self.eventLoop)
monkeypatch.sleep(0.01)
def exitActionTrigger(self):
self._exit = True
self.close()
def closeEvent(self, event):
if not self._exit and self.closeToTrayAction.isChecked():
self.hide()
return
self.settings.setValue("left", self.x())
self.settings.setValue("top", self.y())
self.settings.setValue("width", self.width())
self.settings.setValue("height", self.height())
self.settings.setValue("closeToTray", self.closeToTrayAction.isChecked())
self.settings.setValue("openInTray", self.openInTrayAction.isChecked())
self.settings.setValue("animatePictures", self.animatePicturesAction.isChecked())
self.settings.setValue("darkMode", self.darkModeAction.isChecked())
self.settings.setValue("fetchBackgrounds", self.fetchBackgroundsAction.isChecked())
self.settings.setValue("sendNotifications", self.sendNotificationsAction.isChecked())
self.settings.setValue("fetchHeaders", self.fetchHeadersAction.isChecked())
self.settings.setValue("twoFourTime", self.twoFourTimeAction.isChecked())
self.settings.setValue('colorIcon', self.colorIconAction.isChecked())
self.settings.setValue('silenceNotifications', self.silenceNotifsAction.isChecked())
2021-04-15 15:16:38 -05:00
event.accept()
self._eventloop.stop()
def getCurrentAcc(self):
if self.acctComboBox.currentIndex() == -1 or not self.accts:
return (False, False)
# Returns username, instance
return self.acctComboBox.currentText().split('@')[1], self.acctComboBox.currentText().split('@')[2]
2021-04-25 16:03:55 -05:00
def badLogin(self, name):
self.Err.showMessage("Bad login info for: "+name)
self.newAcctDialog()
2021-04-15 15:16:38 -05:00
def newAcctDialog(self):
dialog = LoginDialog(self)
dialog.getInput(self.initAcct)
def newChatDialog(self):
u, i = self.getCurrentAcc()
text, ok = QInputDialog.getText(self, 'Start A New Chat', "Username:", QLineEdit.Normal, "")
if ok:
if len(text) < 1:
self.Err.showMessage("No text provided")
else:
if text[0] == '@':
text = text[1:]
if text.find('@'+i) != -1:
text = text.replace('@'+i, '')
CallThread(self.accts[u+i].getAcctInfo, self.newChatReady, text).start()
def newChatReady(self, result):
if result is None:
self.Err.showMessage("I couldn't find that user!")
elif not result['pleroma']['accepts_chat_messages']:
self.Err.showMessage("This user is on instance that does not support pleroma chats.\nIt may be too old, or it may not be pleroma.")
else:
u, i = self.getCurrentAcc()
closedList = self.settings.value('closed'+u+i, type=list) or []
for ind in range(0,(len(closedList))):
if closedList[ind] == result['acct']:
del closedList[ind]
break
self.settings.setValue('closed'+u+i, closedList)
CallThread(self.accts[u+i].addChat, self.populateChats, result['id']).start()
2021-04-17 23:34:34 -05:00
def getTotp(self, acct, challenge_type):
if type(challenge_type) != list:
challenge_type = [challenge_type, 'recovery']
self._eventloop.call_soon_threadsafe(self.makeTotpCard, '@'+acct.username+'@'+acct.instance, challenge_type)
while not self.totpReady:
monkeypatch.sleep(0.2)
t = self.totpReady
self.totpReady = None
return t
def makeTotpCard(self, acct, challenge_type):
dialog = TotpCard(acct, challenge_type)
dialog.getInput()
2021-04-15 15:16:38 -05:00
def contactDialog(self):
dialog = ContactCard(self)
dialog.show()
def licenseDialog(self):
dialog = LicenseCard(self)
dialog.show()
def reopenAll(self):
u, i = self.getCurrentAcc()
if not u or not i:
return
self.settings.setValue('closed'+u+i, [])
CallThread(self.accts[u+i].listChats, self.populateChats).start()
def initAcct(self, instance, username, password=None):
if password:
2021-04-17 23:34:34 -05:00
acct = pleroma.Account(instance, username, password, totpFunc=self.getTotp)
2021-04-15 15:16:38 -05:00
else:
token = keyring.get_password('plchat', instance+username+'access_token')
refresh_token = keyring.get_password('plchat', instance+username+'refresh_token')
clientID = keyring.get_password('plchat', instance+username+'clientID')
clientSecret = keyring.get_password('plchat', instance+username+'clientSecret')
acct = pleroma.Account(instance, username,
token=token,
refresh_token=refresh_token,
clientID=clientID,
2021-04-17 23:34:34 -05:00
clientSecret=clientSecret,
totpFunc=self.getTotp
2021-04-15 15:16:38 -05:00
)
RegisterThread(acct, self.doneRegister).start()
def doneRegister(self, acct):
self.accts[acct.username+acct.instance] = acct
self.acctComboBox.addItem('@'+acct.username+'@'+acct.instance)
acctList = self.settings.value('acctList', type=list) or []
concat = True
for acc in acctList:
if acc['username'] == acct.username and acc['instance'] == acct.instance:
concat = False
if concat:
acctList.append({"username": acct.username, "instance": acct.instance})
self.settings.setValue('acctList', acctList)
keyring.set_password('plchat', acct.instance+acct.username+'access_token', acct.token)
keyring.set_password('plchat', acct.instance+acct.username+'refresh_token', acct.refresh_token)
keyring.set_password('plchat', acct.instance+acct.username+'clientID', acct.clientID)
keyring.set_password('plchat', acct.instance+acct.username+'clientSecret', acct.clientSecret)
CallThread(getAvi, None, acct.instance).start()
CallThread(acct.getInstanceInfo, None).start()
def logout(self):
u, i = self.getCurrentAcc()
if not u or not i:
print("Couldn't log out")
return
self.acctComboBox.removeItem(self.acctComboBox.currentIndex())
del self.accts[u+i]
acctList = self.settings.value('acctList', type=list)
for j in range(len(acctList)):
if acctList[j]['username'] == u and acctList[j]['instance'] == i:
keyring.delete_password("plchat", i+u+'access_token')
keyring.delete_password("plchat", i+u+'refresh_token')
keyring.delete_password("plchat", i+u+'clientID')
keyring.delete_password("plchat", i+u+'clientSecret')
del acctList[j]
self.settings.setValue('acctList', acctList)
def acctSwitch(self):
u, i = self.getCurrentAcc()
if u+i in self.accts and 'title' in self.accts[u+i].getInstanceInfo():
self.setWindowTitle(self.accts[u+i].getInstanceInfo()['title']+' Chat')
else:
self.setWindowTitle('PlChat')
self.tabs.clear()
2021-04-17 23:34:34 -05:00
if u and i:
CallThread(self.accts[u+i].listChats, self.populateChats).start()
2021-04-15 15:16:38 -05:00
def populateChats(self, chatList):
if type(chatList) == dict:
chatList = [chatList]
u, i = self.getCurrentAcc()
if not u or not i:
return
closedList = self.settings.value('closed'+u+i, type=list) or []
for chat in chatList:
c = False
for ind in range(0, self.tabs.count()):
if self.tabs.widget(ind).acct == chat['account']['acct']:
c = True
for entry in closedList:
if chat['account']['acct'] == entry:
c = True
if c:
continue
ctab = ChatArea(chat, self.tabs)
self.tabs.addTab(ctab, chat['account']['display_name'])
def closeTab(self, index=None):
if not index:
index = self.tabs.currentIndex()
act = self.tabs.widget(index).acct
u, i = self.getCurrentAcc()
closedList = self.settings.value('closed'+u+i, type=list) or []
f = True
for entry in closedList:
if entry == act:
f = False
if f:
closedList.append(act)
self.settings.setValue('closed'+u+i, closedList)
self.tabs.removeTab(index)
def handlePleromaEvent(self, acct, event):
if event['event'] == 'pleroma:chat_update':
payload = json.loads(event['payload'])
tmp = 0
for ind in range(0, self.tabs.count()):
if self.tabs.widget(ind).acct == payload['account']['acct']:
self._eventloop.call_soon_threadsafe(self.tabs.widget(ind).addMessage, payload['last_message'])
tmp = ind
#self.tabs.widget(ind).addMessage(payload['last_message'])
if payload['last_message']['account_id'] != acct.flakeid and (not self.hasFocus() or payload['account']['acct'] != self.tabs.widget(self.tabs.currentIndex()).acct):
if self.sendNotificationsAction.isChecked():
CallThread(self.makeNotification, None, payload['last_message']['content'], payload['account']['acct'], payload['account']['avatar_static']).start()
app.alert(self, 0)
self._eventloop.call_soon_threadsafe(self.setUrgent, tmp)
def makeNotification(self, content, user, url):
path = getPic(url)
sendNotification(content, title=user, icon=path)
def setUrgent(self, ind):
icon = QIcon()
2021-04-17 11:13:26 -05:00
icon.addPixmap(QPixmap(os.path.join(os.path.dirname(__file__), "unread.svg")))
2021-04-15 15:16:38 -05:00
self.tabs.setTabIcon(ind, icon)
self.tabs.widget(ind).unread = True
if self.trayIcon:
self.trayIcon.setIcon(icon)
self.setWindowIcon(icon)
def changeTab(self, ind):
if not self.tabs.widget(ind):
return
if self.tabs.widget(ind).unread:
CallThread(getPic, self.tabs.widget(ind).useAvi, self.tabs.widget(ind).avaURL).start()
self.tabs.widget(ind).unread = False
flip = True
for A in range(0, self.tabs.count()):
if self.tabs.widget(A).unread:
flip = False
if flip:
if self.trayIcon:
if self.settings.value('colorIcon', type=bool):
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.trayIcon.setIcon(QIcon(ICON_PATH))
if self.settings.value('colorIcon', type=bool):
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
2021-04-15 15:16:38 -05:00
self.tabs.widget(ind).markRead()
self.tabs.widget(ind).setFocus(Qt.NoFocusReason)
def eventFilter(self, object, event):
if event.type() == QEvent.WindowActivate:
self.windowActivate()
return False
def windowActivate(self):
ind = self.tabs.currentIndex()
if not self.tabs.widget(ind):
return
if self.tabs.widget(ind).unread:
CallThread(getPic, self.tabs.widget(ind).useAvi, self.tabs.widget(ind).avaURL).start()
self.tabs.widget(ind).unread = False
flip = True
for A in range(0, self.tabs.count()):
if self.tabs.widget(A).unread:
flip = False
if flip:
if self.trayIcon:
if self.settings.value('colorIcon', type=bool):
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.trayIcon.setIcon(QIcon(ICON_PATH))
if self.settings.value('colorIcon', type=bool):
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
2021-04-15 15:16:38 -05:00
self.tabs.widget(ind).markRead()
self.tabs.widget(ind).setFocus(Qt.NoFocusReason)
class BGWidget(QWidget):
def __init__(self, path=None):
super().__init__()
self.path=path
self.pixmap = QPixmap()
def setPath(self, path):
self.path = path
def paintEvent(self, event):
super().paintEvent(event)
if self.pixmap.isNull() and self.path:
self.pixmap = QPixmap(self.path)
painter = QPainter(self)
if not self.pixmap.isNull():
self.pixmap = self.pixmap.scaled(self.width(), self.height(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
painter.drawPixmap(0,0, self.pixmap)
class ChatArea(QWidget):
def __init__(self, chat, parent=None):
super().__init__(parent=parent)
self.acct = chat['account']['acct']
self.parent = parent
self.chatID = chat['id']
self.account = chat['account']
self.unread = False
if ex.animatePicturesAction.isChecked():
avatar_prop = 'avatar'
header_prop = 'header'
else:
avatar_prop = 'avatar_static'
header_prop = 'header_static'
self.avaURL = chat['account'][avatar_prop]
CallThread(getPic, self.useAvi, self.avaURL).start()
if ex.fetchHeadersAction.isChecked():
CallThread(getPic, self.headerReady, chat['account'][header_prop]).start()
self.wrapper = BGWidget()
wrapperlayout = QHBoxLayout()
wrapperlayout.setContentsMargins(0,0,0,0)
wrapperlayout.setSpacing(0)
self.wrapper.setLayout(wrapperlayout)
self.header = QFrame()
headerlayout = QHBoxLayout()
headernest = QVBoxLayout()
headernestwidget = QWidget()
headernestwidget.setContentsMargins(0,0,0,0)
self.header.setContentsMargins(0,0,0,0)
wrapperlayout.addWidget(self.header)
self.header.setLayout(headerlayout)
headernestwidget.setLayout(headernest)
uDN = EmojiText(chat['account']['display_name'], chat['account']['emojis'], header=True)
if self.acct.find('@') == -1:
uACT = LinkLabel(chat['account']['url'], '@'+self.acct+'@'+ex.getCurrentAcc()[1])
else:
uACT = LinkLabel(chat['account']['url'], '@'+self.acct)
uACT.setMargin(0)
#uACT.setStyleSheet("color: white;")
headernest.addWidget(uDN)
headernest.addWidget(uACT)
self.avatar = QLabel()
self.avatar.setMargin(0)
self.avatar.setFixedHeight(round(QDesktopWidget().screenGeometry(-1).height() * 0.06))
headerlayout.addWidget(self.avatar)
headerlayout.addWidget(headernestwidget, 100)
self.messageArea = MessageArea(self.chatID, self.account, self.callMe)
self.msgs = MovingScroll(self.messageArea)
if ex.fetchBackgroundsAction.isChecked():
if self.acct.find('@') == -1:
remoteURL = ex.getCurrentAcc()[1]
else:
remoteURL = self.acct.split('@')[1]
self.msgs.remoteURL = remoteURL
CallThread(requests.get, self.msgs.parseNodeInfo, 'https://'+remoteURL+'/api/v1/instance').start()
del remoteURL
self.msgs.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.msgs.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.msgs.setWidgetResizable(True)
self.messageArea.setFixedWidth(self.msgs.getChildWidth())
self.messageArea.update()
send = SendArea(self.chatID)
layout = QVBoxLayout()
layout.addWidget(self.wrapper, 2)
layout.addWidget(self.msgs, 35)
layout.addWidget(send, 1)
self.setFocusProxy(send.getTextbox())
self.setLayout(layout)
def callMe(self):
self.msgs.setWidget(self.messageArea)
def markRead(self):
self.messageArea.markRead()
def headerReady(self, path):
self.wrapper.setPath(path)
def addMessage(self, message):
self.messageArea.messages.insert(0, message)
self.messageArea._update(self.messageArea.messages)
def useAvi(self, path):
pic = QPixmap(path)
ava = QPixmap(path)
if pic.isNull():
pic = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
ava = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
icon = QIcon()
icon.addPixmap(pic)
self.parent.setTabIcon(self.parent.indexOf(self), icon)
ava = ava.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() * 0.06), mode=Qt.SmoothTransformation)
self.avatar.resize(ava.width(), ava.height())
self.avatar.setPixmap(ava)
self.avatar.setFixedWidth(self.avatar.width())
def paintEvent(self, event):
super().paintEvent(event)
if not self.wrapper.pixmap.isNull():
self.header.setStyleSheet(".QFrame{background-color: rgba(0, 0, 0, 0.3);} QFrame{color: white;}")
self.paintEvent = super().paintEvent
class MovingScroll(QScrollArea):
def __init__(self, child, path=None):
super().__init__()
self.child = child
self.remoteURL = ''
self.verticalScrollBar().rangeChanged.connect(self.scrollToBottom)
self.verticalScrollBar().valueChanged.connect(self.handleValue)
def resizeEvent(self, event):
self.child.setFixedWidth(self.getChildWidth())
def scrollToBottom(self):
self.verticalScrollBar().triggerAction(QAbstractSlider.SliderToMaximum)
def getChildWidth(self):
return self.width()-self.verticalScrollBar().width()-5
def handleValue(self, pos):
if pos < (self.verticalScrollBar().maximum() / 10):
self.child.addPage()
def parseNodeInfo(self, response):
try:
CallThread(getPic, self.child.setPath, 'https://'+self.remoteURL+response.json()['background_image']).start()
except:
print("Couldn't download background image for: "+remoteURL)
class MessageArea(QWidget):
def __init__(self, chatID, account, callback):
super().__init__()
self.chatID = chatID
self.account = account
self.callback = callback
self.path = None
self.pixmap = QPixmap()
self.layout = QVBoxLayout()
self.layout.setContentsMargins(10, 0,-5,0)
self.last_read_id = None
self.messages = []
self.page = 0
self.fetchingPage = False
def update(self):
u, i = ex.getCurrentAcc()
CallThread(ex.accts[u+i].getMessages, self._update, self.chatID).start()
def markRead(self):
u, i = ex.getCurrentAcc()
2021-04-17 23:34:34 -05:00
if not self.last_read_id or not u or not i:
return
2021-04-15 15:16:38 -05:00
acc = ex.accts[u+i]
acc.markChatRead(self.chatID, self.last_read_id)
def addPage(self):
if self.fetchingPage:
return
self.fetchingPage = True
u, i = ex.getCurrentAcc()
self.page += 1
CallThread(ex.accts[u+i].getMessages, self.pageReady, self.chatID, past=self.messages[len(self.messages)-1]['id']).start()
def pageReady(self, messages):
for message in messages:
self.messages.append(message)
self._update(self.messages)
def _update(self, messages):
self.messages = messages
for message in messages:
if message['account_id'] == self.account['id']:
self.last_read_id = message['id']
for i in reversed(range(self.layout.count())):
self.layout.itemAt(i).widget().setParent(None)
for message in reversed(messages):
#for line in re.split(newlineRegex, message['content']):
# if line.strip() == '':
# continue
self.layout.addWidget(SingleMessage(message, self.account, self), 0, Qt.AlignBottom)
self.setLayout(self.layout)
self.callback()
if self.fetchingPage:
self.fetchingPage = False
def getMessages(self):
return self.messages or []
def setPath(self, path):
self.path = path
def paintEvent(self, event):
super().paintEvent(event)
if self.pixmap.isNull() and self.path:
self.pixmap = QPixmap(self.path)
painter = QPainter(self)
if not self.pixmap.isNull():
self.pixmap = self.pixmap.scaled(self.width(), self.height(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
painter.drawPixmap(0,0, self.pixmap)
newlineRegex = re.compile('<\/?br\/?>|\n')
class SingleMessage(QWidget):
def __init__(self, message, account, parent):
super().__init__(parent=parent)
self.parent = parent
self.message = message
self.account = account
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.setSpacing(0)
self.setStyleSheet("MessageAvatar{margin: 0em 0.2em 0em 0.2em;border: 1px solid black;}")
if ex.animatePicturesAction.isChecked():
avatar_prop = 'avatar'
header_prop = 'header'
else:
avatar_prop = 'avatar_static'
header_prop = 'header_static'
self.avaURL = self.account[avatar_prop]
u,i = ex.getCurrentAcc()
self.userPixmap = None
self.convoPixmap = None
CallThread(getPic, self.setUserPixmap, ex.accts[u+i].acct[avatar_prop]).start()
CallThread(getPic, self.setConvoPixmap, self.avaURL).start()
if self.message['account_id'] == self.account['id']:
self.layout.addWidget(MessageAvatar('convo', self), 0, (Qt.AlignLeft | Qt.AlignTop))
self.layout.addWidget(InternalMessage(self.message, self.account, self), 1, Qt.AlignLeft)
#spacer = QWidget()
#spacer.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.layout.addWidget(spacer, 9000)
else:
#spacer = QWidget()
#spacer.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.layout.addWidget(spacer, 9000)
self.layout.addWidget(InternalMessage(self.message, self.account, self), 1, Qt.AlignRight)
self.layout.addWidget(MessageAvatar('user', self), 0, (Qt.AlignRight | Qt.AlignTop))
self.setLayout(self.layout)
def setUserPixmap(self, path):
p = QPixmap(path)
if p.isNull():
p = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
p = p.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() / 21.6), mode=Qt.SmoothTransformation)
2021-04-15 15:16:38 -05:00
self.userPixmap = p
def setConvoPixmap(self, path):
p = QPixmap(path)
if p.isNull():
p = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
p = p.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() / 21.6), mode=Qt.SmoothTransformation)
2021-04-15 15:16:38 -05:00
self.convoPixmap = p
class MessageAvatar(QLabel):
def __init__(self, futurepixmap, parent):
super().__init__(parent=parent)
self.parent = parent
self.pixmap = QPixmap()
self.setMargin(0)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
CallThread(self.awaitPixmap, None, futurepixmap).start()
#self.setPixmap(self.pixmap)
def setPath(self, path):
#if not self.pixmap.isNull():
# return
#self.pixmap = QPixmap(path)
#if self.pixmap.isNull():
# self.pixmap = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
#self.pixmap = self.pixmap.scaledToHeight(50, mode=Qt.SmoothTransformation)
self.pixmap = path
self.resize(self.pixmap.width(), self.pixmap.height())
self.setPixmap(self.pixmap)
self.setFixedWidth(self.pixmap.width())
def awaitPixmap(self, p):
if p == 'user':
while not self.parent.userPixmap:
monkeypatch.sleep(0.2)
self.resize(self.parent.userPixmap.width(), self.parent.userPixmap.height())
self.setPixmap(self.parent.userPixmap)
self.setFixedWidth(self.parent.userPixmap.width())
else:
while not self.parent.convoPixmap:
monkeypatch.sleep(0.2)
self.resize(self.parent.convoPixmap.width(), self.parent.convoPixmap.height())
self.setPixmap(self.parent.convoPixmap)
self.setFixedWidth(self.parent.convoPixmap.width())
class InternalMessage(QFrame):
def __init__(self, message, account, parent):
super().__init__(parent=parent)
self.parent = parent
self.message = message
self.account = account
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
self.attachment = None
if ex.darkModeAction.isChecked():
color_text="#ffffff"
color_bg = "#000000"
else:
color_text="#000000"
color_bg = "#ffffff"
self.setStyleSheet("InternalMessage{background-color: "+color_bg+";border-radius: 1em;padding: 0.5em;border: 1px solid "+color_text+";} QWidget{color:"+color_text+";}")
if self.message['content']:
self.label = EmojiText(self.message['content'], self.message['emojis'])
self.label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
else:
self.label = None
self.timestamp = QLabel(timeconvert.utc_to_local(self.message['created_at'], ex.twoFourTimeAction.isChecked()))
self.timestamp.setFont(ex.TimestampFont)
self.timestamp.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
if self.label:
self.layout.addWidget(self.label)
if not self.message['attachment']:
self.layout.addWidget(self.timestamp, 1, (Qt.AlignRight | Qt.AlignBottom))
else:
CallThread(self.getMime, self.addAttachment, self.message['attachment']['url']).start()
#self.setMinimumSize(QSize(50, computedHeight))
self.setLayout(self.layout)
def getMime(self, url):
with urllib.request.urlopen(url) as res:
# pleroma sux balls at mime types
mime = magic.from_buffer(res.read(2048), mime=True)
return mime
def addAttachment(self, mime):
self.attachment = MediaAttachment(self.message['attachment'], mime)
self.layout.addWidget(self.attachment)
self.layout.addWidget(self.timestamp, 1, (Qt.AlignRight | Qt.AlignBottom))
def sizeHint(self):
multiplier = 2
additive = 0
if self.label:
multiplier += self.label.layoutfinal.count()
if self.attachment:
additive += self.attachment.getMedia().height()
computedHeight = (ex.defaultFontMetrics.height() * multiplier) + additive
computedWidth = 50
if self.attachment:
if self.attachment.getMedia().width() > computedWidth:
computedWidth = self.attachment.getMedia().width()
return QSize(computedWidth, computedHeight)
def MediaAttachment(media, mime):
if mime.find("image/") != -1:
return ImageAttachment(media)
elif mime.find("video/") != -1:
return VideoAttachment(media)
elif mime.find("audio/") != -1:
return AudioAttachmentWrapper(media)
else:
return FileAttachment(media)
class FileAttachment(QPushButton):
def __init__(self, media):
super(FileAttachment, self).__init__()
self.url = QUrl(media['url'])
self.setCursor(Qt.PointingHandCursor)
self.setIcon(self.style().standardIcon(QStyle.SP_FileLinkIcon))
self.setFlat(True)
def mouseReleaseEvent(self, event):
if event.modifiers() == Qt.NoModifier and event.button() == Qt.LeftButton:
QDesktopServices.openUrl(self.url)
def getMedia(self):
return self
class AudioAttachmentWrapper(QWidget):
def __init__(self, media):
super(AudioAttachmentWrapper, self).__init__()
layout = QHBoxLayout()
self.file = FileAttachment(media)
self.audio = AudioAttachment(media)
layout.addWidget(self.file)
layout.addWidget(self.audio)
self.setLayout(layout)
def getMedia(self):
return QSize(self.file.getMedia().width()+self.audio.getMedia().width(), self.file.getMedia().height()+self.audio.getMedia().height())
class AudioAttachment(audiowidget.AudioPlayer):
def __init__(self, media):
super(AudioAttachment, self).__init__()
#CallThread(self.openFile, None, media['url']).start()
self.openFile(media['url'])
def getMedia(self):
return super().sizeHint()
class LinkLabel(QLabel):
def __init__(self, url, *args, **kwargs):
super(LinkLabel, self).__init__(*args, **kwargs)
self.url = QUrl(url)
self.setCursor(Qt.PointingHandCursor)
def mouseReleaseEvent(self, event):
bttn = event.button()
modif = event.modifiers()
if modif == Qt.NoModifier and bttn == Qt.LeftButton:
QDesktopServices.openUrl(self.url)
class VideoAttachment(videowidget.VideoPlayer):
def __init__(self, media, *args, **kwargs):
super().__init__(media['url'], *args, **kwargs)
#CallThread(self.openFile, None, media['url']).start()
self.openFile(media['url'])
def getMedia(self):
return QSize(self.width(), self.videoWidget.height()+50)
class ImageAttachment(LinkLabel):
def __init__(self, media):
super().__init__(media['remote_url'])
self.pixmap = QPixmap()
CallThread(getPic, self.setPath, media['url']).start()
def setPath(self, path):
self.pixmap = QPixmap(path)
if round(ex.width() * 0.7) < self.pixmap.width():
self.pixmap = self.pixmap.scaledToWidth(round(ex.width() * 0.7), mode=Qt.SmoothTransformation)
self.resize(self.pixmap.width(), self.pixmap.height())
self.setPixmap(self.pixmap)
#self.setFixedHeight(self.height())
self.url = QUrl(path)
def getMedia(self):
return self.pixmap
class SendArea(QWidget):
def __init__(self, chatID):
super().__init__()
self.layout = QHBoxLayout()
self.lastFile = None
self.mediaPreview = None
self.mediaID = None
self.Err = QErrorMessage()
self.chatID = chatID
self.textbox = TextBox()
self.mediaStack = QStackedWidget()
self.attachButton = QPushButton(self.style().standardIcon(QStyle.SP_FileDialogStart), 'Attach File')
self.attachButton.clicked.connect(self.newFileDialog)
self.attachButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
2021-04-17 11:13:26 -05:00
sendButton = QPushButton(QIcon(QPixmap(os.path.join(os.path.dirname(__file__), "send.svg"))), '')
2021-04-15 15:16:38 -05:00
sendButton.clicked.connect(self.sendMessage)
sendButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
self.mediaStack.addWidget(self.attachButton)
self.mediaStack.setCurrentWidget(self.attachButton)
self.layout.addWidget(self.textbox, 8)
self.layout.addWidget(self.mediaStack, 1)
self.layout.addWidget(sendButton, 1)
self.setFocusProxy(self.textbox)
self.setLayout(self.layout)
self.textbox.setSendFunction(self.sendMessage)
def getTextbox(self):
return self.textbox
def newFileDialog(self):
if self.mediaPreview == 'uploading':
self.Err.showMessage("Already uploading a file!")
return
qfd = QFileDialog(self)
qfd.setFileMode(QFileDialog.ExistingFile)
qfd.setNameFilters(["All Files (*)"])
qfd.filesSelected.connect(self.attachFile)
qfd.show()
def attachFile(self, paths):
if paths[0] == self.lastFile:
return
elif self.mediaPreview:
self.Err.showMessage("File already attached!")
return
u, i = ex.getCurrentAcc()
CallThread(ex.accts[u+i].uploadMedia, self.doneAttachFile, paths[0]).start()
self.mediaPreview = 'uploading'
self.lastFile = paths[0]
def doneAttachFile(self, media):
if not 'id' in media:
self.Err.showMessage('Error uploading file')
self.detachFile()
return
self.mediaID = media['id']
self.mediaPreview = QPushButton(ex.style().standardIcon(QStyle.SP_FileIcon), '')
self.mediaPreview.clicked.connect(self.detachDialog)
self.mediaPreview.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
self.mediaStack.addWidget(self.mediaPreview)
self.mediaStack.setCurrentWidget(self.mediaPreview)
def detachDialog(self):
dialog = DetachDialog()
dialog.accepted.connect(self.detachFile)
dialog.getInput()
def detachFile(self):
if self.mediaPreview and type(self.mediaPreview) != str:
self.mediaStack.removeWidget(self.mediaPreview)
self.mediaStack.setCurrentWidget(self.attachButton)
self.mediaPreview = None
self.lastFile = None
self.mediaID = None
def sendMessage(self):
u, i = ex.getCurrentAcc()
acc = ex.accts[u+i]
if self.textbox.toPlainText() == '':
text = None
else:
text = self.textbox.toPlainText()
CallThread(acc.sendMessage, None, self.chatID, content=text, media=self.mediaID).start()
self.textbox.setText('')
if self.mediaPreview:
self.detachFile()
self.textbox.setFocus(Qt.NoFocusReason)
class TextBox(QTextEdit):
def __init__(self, sendFunction=None):
super().__init__()
self.sendFunction = sendFunction or self.noop
def keyPressEvent(self, event):
if not event.matches(QKeySequence.InsertParagraphSeparator):
super().keyPressEvent(event)
else:
self.sendFunction()
def setSendFunction(self, sendFunction):
self.sendFunction = sendFunction
def noop(self):
return
class DetachDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Detach Dialog")
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.message = QLabel("Detach this file?")
self.layout.addWidget(self.message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def getInput(self):
self.exec_()
self.show()
self.raise_()
self.activateWindow()
2021-04-17 23:34:34 -05:00
class TotpCard(QDialog):
def __init__(self, acct, challenge_types, parent=None,):
super().__init__(parent=parent)
self.setWindowTitle("MFA Request")
self.result = None
self.waiting = False
QBtn = QDialogButtonBox.Ok
layout = QVBoxLayout()
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.message = QLabel("2FA Required for "+acct)
tmp = QHBoxLayout()
tmpw = QWidget()
tmpw.setLayout(tmp)
self.comboboxlabel = QLabel("2FA Code Type:")
self.combobox = QComboBox()
self.combobox.addItems(challenge_types)
self.combobox.setDuplicatesEnabled(False)
self.combobox.setEditable(False)
tmp.addWidget(self.comboboxlabel)
tmp.addWidget(self.combobox)
self.code = QLineEdit()
self.code.setPlaceholderText('2FA Code')
layout.addWidget(self.message)
layout.addWidget(tmpw)
layout.addWidget(self.code)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
self.setFocusProxy(self.code)
self.accepted.connect(self._finished)
def _finished(self):
ex.totpReady = (self.code.text(), self.combobox.currentText())
def getInput(self, *args, **kwargs):
self.exec_()
self.show()
self.raise_()
self.activateWindow()
2021-04-15 15:16:38 -05:00
class LoginDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Log In")
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.message = QLabel("Log in to your fediverse account")
self.username = QLineEdit()
self.username.setPlaceholderText('Username (e.g. Moon)')
self.instance = QLineEdit()
self.instance.setPlaceholderText('Instance (e.g. shitposter.club)')
self.password = QLineEdit()
self.password.setPlaceholderText('Password')
self.password.setEchoMode(QLineEdit.Password)
self.layout.addWidget(self.message)
self.layout.addWidget(self.username)
self.layout.addWidget(self.instance)
self.layout.addWidget(self.password)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.setFocusProxy(self.username)
def getInput(self, func):
self.show()
self.raise_()
self.activateWindow()
self.accepted.connect(self._finished)
self.finishedFunc = func
def _finished(self):
instance = self.instance.text()
instance = instance.replace('http://', '')
instance = instance.replace('https://', '')
if instance[len(instance) - 1] == '/':
instance = instance[0:(len(instance) - 2)]
self.finishedFunc(self.instance.text(), self.username.text(), password=self.password.text())
class ContactCard(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Contact Info")
QBtn = QDialogButtonBox.Ok
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.layout = QVBoxLayout()
self.message = QLabel("Written by knotteye\nContact me in the following places (in order of preference) with issues")
self.fedi = QLabel("Fedi: <a href='https://waldn.net/users/knotteye'>@knotteye@waldn.net</a>")
self.fedi.setOpenExternalLinks(True)
self.xmpp = QLabel('XMPP: <a href="xmpp://knotteye@telekem.net?message">knotteye@telekem.net</a>')
self.xmpp.setOpenExternalLinks(True)
self.email = QLabel('Email: <a href="mailto:knotteye@airmail.cc">knotteye@airmail.cc</a>')
self.email.setOpenExternalLinks(True)
self.layout.addWidget(self.message)
self.layout.addWidget(self.fedi)
self.layout.addWidget(self.xmpp)
self.layout.addWidget(self.email)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.show()
self.raise_()
self.activateWindow()
class LicenseArea(QScrollArea):
def __init__(self, licenseText):
super().__init__()
label = QLabel(licenseText)
self.setWidget(label)
2021-04-15 15:16:38 -05:00
class LicenseCard(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Required Licensing Information")
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
QBtn = QDialogButtonBox.Ok
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.layout = QVBoxLayout()
self.tabs = QTabWidget()
plchatlayout = QVBoxLayout()
plchatW = QWidget()
plchatW.setLayout(plchatlayout)
plchatlabel = QLabel("<a href='https://git.waldn.net/git/knotteye/plchat/'>Repository</a>")
2021-04-15 15:16:38 -05:00
plchatlabel.setTextFormat(Qt.RichText)
plchatlabel.setOpenExternalLinks(True)
plchatLicense = QLabel(misc.plchatLicense)
2021-04-15 15:16:38 -05:00
plchatscroll = QScrollArea()
plchatscroll.setWidget(plchatLicense)
plchatlayout.addWidget(plchatlabel)
plchatlayout.addWidget(plchatscroll)
self.tabs.addTab(plchatW, 'PlChat')
self.tabs.addTab(LicenseArea(misc.pyqt5License), "PyQt5")
self.tabs.addTab(LicenseArea(misc.videowidgetlicense), "videowidget.py")
self.tabs.addTab(LicenseArea(misc.videowidgetlicense), "audiowidget.py")
self.tabs.addTab(LicenseArea(misc.notifypylicense), "notify-py")
self.tabs.addTab(LicenseArea(misc.logurulicense), "loguru")
self.tabs.addTab(LicenseArea(misc.websocketslicense), "websockets")
self.tabs.addTab(LicenseArea(misc.sixlicense), "six")
self.tabs.addTab(LicenseArea(misc.magiclicense), "python-magic")
self.tabs.addTab(LicenseArea(misc.dateutillicense), "dateutil")
self.tabs.addTab(LicenseArea(misc.keyringlicense), "keyring")
2021-04-15 15:16:38 -05:00
self.layout.addWidget(self.tabs)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.show()
self.raise_()
self.activateWindow()
def sizeHint(self):
return QSize(650, 850)
2021-04-15 15:16:38 -05:00
class EmojiText(QWidget):
ceRegex = re.compile('(:[^:]+:)')
def __init__(self, text, emojiList, header=False):
super().__init__()
self.layoutfinal = QVBoxLayout()
self.layoutfinal.setSpacing(0)
self.layoutfinal.setContentsMargins(0,0,0,0)
for line in re.split(newlineRegex, text):
self.layoutfinal.addWidget(self.makeLine(line, emojiList, header))
self.setLayout(self.layoutfinal)
def makeLine(self, text, emojiList, header):
widget = QWidget()
layout = QWidget()
layout = QHBoxLayout()
layout.setSpacing(0)
layout.setContentsMargins(0,0,0,0)
widget.setContentsMargins(0,0,0,0)
customEmojis = []
if header:
size = ex.headerEmojiFontSize
else:
size = ex.emojiFontSize
for em in re.findall(self.ceRegex, text):
for emoji in emojiList:
if emoji['shortcode'] == em[1:len(em)-1]:
cemurl = emoji[STATIC_PREF+'url']
customEmojis.append(CustomEmoji(em[1:len(em)-1], cemurl, size))
i = 0
for stringpart in re.split(self.ceRegex, text):
if len(stringpart) > 2 and stringpart[0] == ':' and stringpart[len(stringpart)-1] == ':':
layout.addWidget(customEmojis[i], 1)
i += 1
else:
label = QLabel(stringpart)
if header:
label.setFont(ex.HeaderFont)
label.setTextFormat(Qt.RichText)
2021-04-16 21:33:42 -05:00
label.setWordWrap(True)
2021-04-15 15:16:38 -05:00
label.setTextInteractionFlags((Qt.TextBrowserInteraction))
label.setOpenExternalLinks(True)
layout.addWidget(label)
layout.addWidget(QWidget(), 9000)
widget.setLayout(layout)
return widget
class CustomEmoji(QLabel):
def __init__(self, shortcode, url=None, height=12):
super().__init__()
self.setScaledContents=True
if url:
CallThread(getPic, self.loadPic, url).start()
self.mheight = height
self.pixmap = None
self.setMargin(0)
def loadPic(self, path):
self.pixmap = QPixmap(path)
self.setPixmap(self.pixmap.scaledToHeight(self.mheight, mode=Qt.SmoothTransformation))
class CallThread(threading.Thread):
# Generic thread object that takes some function, a callback and a list of arguments
# The entire purpose of this object is to be a wrapper around running functions in a new thread
def __init__(self, function, callback, *args, **kwargs):
super(CallThread, self).__init__()
self.function = function
self.args = args or ()
self.kwargs = kwargs or {}
self.callback = callback
def run(self):
result = self.function(*self.args, **self.kwargs)
if not self.callback:
return
ex._eventloop.call_soon_threadsafe(self.callback, result)
class RegisterThread(threading.Thread):
def __init__(self, acct, callback):
super(RegisterThread, self).__init__()
self.acct = acct
self.callback = callback
def run(self):
self.acct.register()
2021-04-25 16:03:55 -05:00
try:
self.acct.login()
except ValueError:
ex._eventloop.call_soon_threadsafe(ex.badLogin, self.acct.username+'@'+self.acct.instance)
return
2021-04-15 15:16:38 -05:00
NotifThread(self.acct).start()
self.acct.setChatUpdate(ex.handlePleromaEvent)
self.callback(self.acct)
class NotifThread(threading.Thread):
def __init__(self, acct):
super(NotifThread, self).__init__()
self.acct = acct
self.loop = asyncio.new_event_loop()
self.daemon = True
asyncio.set_event_loop(self.loop)
def run(self):
self.loop.run_until_complete(self.acct.startStreaming())
class HLine(QFrame):
def __init__(self):
super(HLine, self).__init__()
self.setFrameShape(self.HLine|self.Sunken)
class VLine(QFrame):
def __init__(self):
super(VLine, self).__init__()
self.setFrameShape(self.VLine|self.Sunken)
class IconLabel(QLabel):
def __init__(self, icon, w=30, h=None):
super(IconLabel, self).__init__()
if not h:
h = w
#self.icon = self.style().standardIcon(icon)
self.icon = icon
self.setPixmap(self.icon.pixmap(QSize(w, h)))
Notification = Notify(
default_notification_title="PlChat",
default_notification_icon=ICON_PATH,
default_notification_audio=NOTIF_SOUND
2021-04-15 15:16:38 -05:00
)
def sendNotification(message, title=None, icon=None):
Notification.message = message
if title:
Notification.title = title
if icon:
Notification.icon = icon
Notification.send(block=False)
def getPic(url):
filename = urllib.parse.urlparse(url).netloc+os.path.basename(urllib.parse.urlparse(url).path)
filepath = CACHE+'/img/'+filename
try:
f = open(filepath, mode='xb+')
THREADS[filepath] = threading.current_thread()
except FileExistsError:
try:
while THREADS[filepath].is_alive():
monkeypatch.sleep(0.2)
return filepath
except KeyError:
return filepath
try:
imgdata = urllib.request.urlopen(url).read()
except Exception as E:
print("Got "+str(E)+" while downloading file: "+url)
return filepath
f.write(imgdata)
f.close()
return filepath
def getVid(url):
filename = urllib.parse.urlparse(url).netloc+os.path.basename(urllib.parse.urlparse(url).path)
filepath = CACHE+'/vid/'+filename
try:
f = open(filepath, mode='xb+')
THREADS[filepath] = threading.current_thread()
except FileExistsError:
try:
while THREADS[filepath].is_alive():
monkeypatch.sleep(0.2)
return filepath
except KeyError:
return filepath
try:
viddata = urllib.request.urlopen(url).read()
except Exception as E:
print("Got "+str(E)+" while downloading file: "+url)
return filepath
f.write(viddata)
f.close()
return filepath
def getAudio(url):
filename = urllib.parse.urlparse(url).netloc+os.path.basename(urllib.parse.urlparse(url).path)
filepath = CACHE+'/audio/'+filename
try:
f = open(filepath, mode='xb+')
THREADS[filepath] = threading.current_thread()
except FileExistsError:
try:
while THREADS[filepath].is_alive():
monkeypatch.sleep(0.2)
return filepath
except KeyError:
return filepath
try:
viddata = urllib.request.urlopen(url).read()
except Exception as E:
print("Got "+str(E)+" while downloading file: "+url)
return filepath
f.write(viddata)
f.close()
return filepath
def _mkdir(_dir):
if os.path.isdir(_dir): pass
elif os.path.isfile(_dir):
raise OSError("%s exists as a regular file." % _dir)
else:
parent, directory = os.path.split(_dir)
if parent and not os.path.isdir(parent): _mkdir(parent)
if directory: os.mkdir(_dir)
def getAvi(instance):
filename = instance+'avi.png'
filepath = APPDATA+'/'+filename
try:
f = open(filepath, mode='xb+')
except FileExistsError:
return
try:
imgdata = urllib.request.urlopen('https://'+instance+'/images/avi.png').read()
except Exception as E:
print("Couldn't get default avi for "+instance+", error: "+str(E))
return
f.write(imgdata)
f.close()
return filepath
def updateAnimationPref():
if ex.animatePicturesAction.isChecked():
STATIC_PREF = ''
else:
STATIC_PREF = 'static_'
if __name__ == '__main__':
2021-04-15 15:16:38 -05:00
_mkdir(CACHE+'/img/')
_mkdir(CACHE+'/vid/')
_mkdir(CACHE+'/audio/')
_mkdir(APPDATA)
app = QApplication(sys.argv)
app.setOrganizationName("plchat")
app.setApplicationName("plchat")
ex = App(app.processEvents)
sys.exit(ex._eventloop.run_forever())