🎉 NEW Frappe Chat (#4612)
* added doctypes, created frappe chat ui * added component layout with state-like abilities, added apis * updated user doctype, moved from state-like feature and component abstraction * added room component * fixed publish_realtime with after_commit = True * created room component and searchbar * minor fix * functional message parsing * update * Added Chat Profile * added chat message * more changes into chat room * fixed APIs, added client side scripting * added chat message attachements, more doc updates * Brand New UI with socket io room integration * completed socketio integration. off to room subscription and publish * realtime room update * raw update * initialized docs, added p2p connection for call tests * updated docs * added coverage, updated api for ease of use * raw commit * added test cases * Chat Room updates and new room creation * added chat group creation * added collapsible plugin * toggable room view * updated * [RAW] * updated UI for chat * Deleted Previous Chat Page * moved from frappe.Chat.Widget to frappe.Chat * modularized frappe-fab * added more docstrings * tried adding conversation tones * Added conversation_tones and refurbished chat popper * modified frappe.ui.Dialog, moved from AppBar to ActionBar, responsive for Mobile 💃 * moved RoomList item namespace * Configurable Desktop update, moved profile updates to on_update * added state change listeners * removed AppBar to ActionBar customizable 💃 * added destroy method * removed coverage, refactored group creation * Successful Chat Rooms and Group creation * sort rows based on last_message_timestamp or creation * added frappe._.compare * removed redundant less variables * Chat Room back button with custom routing and destroy methods * Added EmojiPicker * fixed multiple dialog render * setup quick access * added chat chime, functional chat message list updates at room list * deleted package-lock.json * realtime date updates * updated chat message list * functional message render and updates * added track seen * added typing status * updated typing status * valid typing statuses and quick search * Functional Quick Search * reverted fix * some more cleanup and promisifed * fixed hints close on click * updated fab boldness * close popper on click panel * close popper on click panel * reverted octicon-lg, fixed popper heading click * new frappe capture * removed webcamjs * added uploader and capture * removed chat FAB, added as notification instead * on message update
This commit is contained in:
parent
2f86cd82ff
commit
005cfe3dc8
80 changed files with 5526 additions and 700 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"python.linting.pylintEnabled": false
|
||||
}
|
||||
|
|
@ -89,8 +89,10 @@ def publish_realtime(event=None, message=None, room=None,
|
|||
room = get_user_room(user)
|
||||
elif doctype and docname:
|
||||
room = get_doc_room(doctype, docname)
|
||||
else:
|
||||
room = get_site_room()
|
||||
else:
|
||||
# frappe.chat
|
||||
room = get_chat_room(room)
|
||||
# end frappe.chat
|
||||
|
||||
if after_commit:
|
||||
params = [event, message, room]
|
||||
|
|
@ -110,7 +112,7 @@ def emit_via_redis(event, message, room):
|
|||
try:
|
||||
r.publish('events', frappe.as_json({'event': event, 'message': message, 'room': room}))
|
||||
except redis.exceptions.ConnectionError:
|
||||
# print frappe.get_traceback()
|
||||
# print(frappe.get_traceback())
|
||||
pass
|
||||
|
||||
def put_log(line_no, line, task_id=None):
|
||||
|
|
@ -194,3 +196,10 @@ def get_site_room():
|
|||
|
||||
def get_task_progress_room(task_id):
|
||||
return "".join([frappe.local.site, ":task_progress:", task_id])
|
||||
|
||||
# frappe.chat
|
||||
def get_chat_room(room):
|
||||
room = ''.join([frappe.local.site, ":room:", room])
|
||||
|
||||
return room
|
||||
# end frappe.chat room
|
||||
|
|
@ -154,7 +154,7 @@ function get_compiled_file(file, output_path, minify, force_compile) {
|
|||
|
||||
function babelify(content, path, minify) {
|
||||
let presets = ['env'];
|
||||
var plugins = ['transform-object-rest-spread']
|
||||
const plugins = ['transform-object-rest-spread']
|
||||
// Minification doesn't work when loading Frappe Desk
|
||||
// Avoid for now, trace the error and come back.
|
||||
try {
|
||||
|
|
|
|||
0
frappe/chat/__init__.py
Normal file
0
frappe/chat/__init__.py
Normal file
0
frappe/chat/doctype/__init__.py
Normal file
0
frappe/chat/doctype/__init__.py
Normal file
0
frappe/chat/doctype/chat_message/__init__.py
Normal file
0
frappe/chat/doctype/chat_message/__init__.py
Normal file
8
frappe/chat/doctype/chat_message/chat_message.js
Normal file
8
frappe/chat/doctype/chat_message/chat_message.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Message', {
|
||||
refresh: function(frm) {
|
||||
|
||||
}
|
||||
});
|
||||
245
frappe/chat/doctype/chat_message/chat_message.json
Normal file
245
frappe/chat/doctype/chat_message/chat_message.json
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 1,
|
||||
"creation": "2017-11-10 11:10:40.011099",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Direct\nGroup\nVisitor",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "room",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Room",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Chat Room",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Content",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "mentions",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Mentions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "urls",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "URLs",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-12-28 16:26:24.142473",
|
||||
"modified_by": "achilles@erpnext.com",
|
||||
"module": "Chat",
|
||||
"name": "Chat Message",
|
||||
"name_case": "",
|
||||
"owner": "arjun@gmail.com",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
162
frappe/chat/doctype/chat_message/chat_message.py
Normal file
162
frappe/chat/doctype/chat_message/chat_message.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# imports - standard imports
|
||||
import json
|
||||
|
||||
# imports - third-party imports
|
||||
import requests
|
||||
from bs4 import BeautifulSoup as Soup
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _, _dict
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.chat.util import (
|
||||
get_user_doc,
|
||||
check_url,
|
||||
dictify,
|
||||
get_emojis
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatMessage(Document):
|
||||
pass
|
||||
|
||||
def get_message_urls(content):
|
||||
soup = Soup(content, 'html.parser')
|
||||
anchors = soup.find_all('a')
|
||||
urls = [ ]
|
||||
|
||||
for anchor in anchors:
|
||||
text = anchor.text
|
||||
|
||||
if check_url(text):
|
||||
urls.append(text)
|
||||
|
||||
return urls
|
||||
|
||||
def get_message_mentions(content):
|
||||
mentions = [ ]
|
||||
tokens = content.split(' ')
|
||||
|
||||
for token in tokens:
|
||||
if token.startswith('@'):
|
||||
what = token[1:]
|
||||
if frappe.db.exists('User', what):
|
||||
mentions.append(what)
|
||||
else:
|
||||
if frappe.db.exists('User', token):
|
||||
mentions.append(token)
|
||||
|
||||
return mentions
|
||||
|
||||
def get_message_meta(content):
|
||||
'''
|
||||
Assumes content to be HTML. Sanitizes the content
|
||||
into a dict of metadata values.
|
||||
'''
|
||||
meta = _dict(
|
||||
links = [ ],
|
||||
mentions = [ ]
|
||||
)
|
||||
|
||||
meta.content = content
|
||||
meta.urls = get_message_urls(content)
|
||||
meta.mentions = get_message_mentions(content)
|
||||
|
||||
return meta
|
||||
|
||||
def sanitize_message_content(content):
|
||||
emojis = get_emojis()
|
||||
|
||||
tokens = content.split(' ')
|
||||
for token in tokens:
|
||||
if token.startswith(':') and token.endswith(':'):
|
||||
what = token[1:-1]
|
||||
|
||||
# Expensive, I know.
|
||||
for emoji in emojis:
|
||||
for alias in emoji.aliases:
|
||||
if what == alias:
|
||||
content = content.replace(token, emoji.emoji)
|
||||
|
||||
return content
|
||||
|
||||
def get_new_chat_message_doc(user, room, content, link = True):
|
||||
user = get_user_doc(user)
|
||||
room = frappe.get_doc('Chat Room', room)
|
||||
|
||||
meta = get_message_meta(content)
|
||||
mess = frappe.new_doc('Chat Message')
|
||||
mess.type = room.type
|
||||
mess.room = room.name
|
||||
mess.content = sanitize_message_content(content)
|
||||
mess.user = user.name
|
||||
|
||||
mess.mentions = json.dumps(meta.mentions)
|
||||
mess.urls = ','.join(meta.urls)
|
||||
mess.save()
|
||||
|
||||
if link:
|
||||
room.update(dict(
|
||||
last_message = mess.name
|
||||
))
|
||||
room.save()
|
||||
|
||||
return mess
|
||||
|
||||
def get_new_chat_message(user, room, content):
|
||||
mess = get_new_chat_message_doc(user, room, content)
|
||||
|
||||
resp = dict(
|
||||
name = mess.name,
|
||||
user = mess.user,
|
||||
room = mess.room,
|
||||
content = mess.content,
|
||||
urls = mess.urls,
|
||||
mentions = json.loads(mess.mentions),
|
||||
creation = mess.creation,
|
||||
seen = json.loads(mess._seen) if mess._seen else [ ],
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
@frappe.whitelist()
|
||||
def send(user, room, content):
|
||||
mess = get_new_chat_message(user, room, content)
|
||||
|
||||
frappe.publish_realtime('frappe.chat.message:create', mess, room = room,
|
||||
after_commit = True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def seen(message, user = None):
|
||||
mess = frappe.get_doc('Chat Message', message)
|
||||
mess.add_seen(user)
|
||||
|
||||
room = mess.room
|
||||
resp = dict(message = message, data = dict(seen = json.loads(mess._seen)))
|
||||
|
||||
frappe.publish_realtime('frappe.chat.message:update', resp, room = room, after_commit = True)
|
||||
|
||||
# This is fine for now. If you're "ReST"-ing it,
|
||||
# make sure you don't let the user see them.
|
||||
# Come again, Why are we even passing user?
|
||||
def get_messages(room, user = None, fields = None, pagination = 20):
|
||||
user = get_user_doc(user)
|
||||
|
||||
room = frappe.get_doc('Chat Room', room)
|
||||
mess = frappe.get_list('Chat Message',
|
||||
filters = [
|
||||
('Chat Message', 'room', '=', room.name),
|
||||
('Chat Message', 'type', '=', room.type)
|
||||
],
|
||||
fields = fields if fields else [
|
||||
'name', 'type',
|
||||
'room', 'content',
|
||||
'user', 'mentions', 'urls',
|
||||
'creation'
|
||||
]
|
||||
)
|
||||
|
||||
return mess
|
||||
23
frappe/chat/doctype/chat_message/test_chat_message.js
Normal file
23
frappe/chat/doctype/chat_message/test_chat_message.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* eslint-disable */
|
||||
// rename this file from _test_[name] to test_[name] to activate
|
||||
// and remove above this line
|
||||
|
||||
QUnit.test("test: Chat Message", function (assert) {
|
||||
let done = assert.async();
|
||||
|
||||
// number of asserts
|
||||
assert.expect(1);
|
||||
|
||||
frappe.run_serially([
|
||||
// insert a new Chat Message
|
||||
() => frappe.tests.make('Chat Message', [
|
||||
// values to be set
|
||||
{key: 'value'}
|
||||
]),
|
||||
() => {
|
||||
assert.equal(cur_frm.doc.key, 'value');
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
||||
});
|
||||
19
frappe/chat/doctype/chat_message/test_chat_message.py
Normal file
19
frappe/chat/doctype/chat_message/test_chat_message.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# imports - standard imports
|
||||
import unittest
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.chat.doctype.chat_message import chat_message
|
||||
from frappe.chat.util import create_test_user
|
||||
|
||||
session = frappe.session
|
||||
test_user = create_test_user(__name__)
|
||||
|
||||
class TestChatMessage(unittest.TestCase):
|
||||
def test_send(self):
|
||||
# TODO - Write the case once you're done with Chat Room
|
||||
# user = test_user
|
||||
# chat_message.send(user, room, 'foobar')
|
||||
pass
|
||||
0
frappe/chat/doctype/chat_message_attachment/__init__.py
Normal file
0
frappe/chat/doctype/chat_message_attachment/__init__.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 1,
|
||||
"creation": "2017-11-15 13:27:05.706207",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "attachment",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Attachment",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-11-15 13:33:27.405470",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Message Attachment",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ChatMessageAttachment(Document):
|
||||
pass
|
||||
0
frappe/chat/doctype/chat_profile/__init__.py
Normal file
0
frappe/chat/doctype/chat_profile/__init__.py
Normal file
8
frappe/chat/doctype/chat_profile/chat_profile.js
Normal file
8
frappe/chat/doctype/chat_profile/chat_profile.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Profile', {
|
||||
refresh: function(frm) {
|
||||
|
||||
}
|
||||
});
|
||||
278
frappe/chat/doctype/chat_profile/chat_profile.json
Normal file
278
frappe/chat/doctype/chat_profile/chat_profile.json
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "CP.#####",
|
||||
"beta": 1,
|
||||
"creation": "2017-11-13 18:26:57.943027",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Online",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Status",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Online\nAway\nBusy\nOffline",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "chat_background",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Chat Background",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "notifications",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Notifications",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "notification_tones",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Notification Tones",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "conversation_tones",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Conversation Tones",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "settings",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Settings",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "display_widget",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Display Widget",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-12-19 11:23:04.791395",
|
||||
"modified_by": "achilles@erpnext.com",
|
||||
"module": "Chat",
|
||||
"name": "Chat Profile",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
173
frappe/chat/doctype/chat_profile/chat_profile.py
Normal file
173
frappe/chat/doctype/chat_profile/chat_profile.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _, _dict # <- the best thing ever happened to frappe
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.chat.doctype.chat_room.chat_room import get_user_chat_rooms
|
||||
from frappe.chat.util import (
|
||||
get_user_doc,
|
||||
safe_json_loads,
|
||||
filter_dict,
|
||||
dictify
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
# TODO
|
||||
# User
|
||||
# [ ] Deleting a User should also delete its Chat Profile.
|
||||
# [ ] Ensuring username is mandatory when User has been created.
|
||||
|
||||
# Chat Profile
|
||||
# [x] Link Chat Profile DocType to User when User has been created.
|
||||
# [x] Once done, add a validator to check Chat Profile has been
|
||||
# created only once.
|
||||
# [x] Users can view other Users Chat Profile, but not update the same.
|
||||
# Not sure, but circular link would be helpful.
|
||||
|
||||
class ChatProfile(Document):
|
||||
# trigger from DocType
|
||||
def before_save(self):
|
||||
if not self.is_new():
|
||||
self.get_doc_before_save()
|
||||
|
||||
def on_update(self):
|
||||
user = get_user_doc()
|
||||
|
||||
if user.chat_profile:
|
||||
if user.chat_profile != self.name:
|
||||
frappe.throw(_("Sorry! You don't have permission to update this profile."))
|
||||
else:
|
||||
if not self.is_new():
|
||||
before = self.get_doc_before_save()
|
||||
after = self
|
||||
|
||||
diff = dictify(get_diff(before, after))
|
||||
if diff:
|
||||
fields = [change[0] for change in diff.changed]
|
||||
|
||||
# NOTE: Version DocType is the best thing ever. Selective Updates to Chat Rooms/Users FTW.
|
||||
|
||||
# status update are dispatched to current user and Direct Chat Rooms.
|
||||
if 'status' in fields:
|
||||
# TODO: you can add filters within get_user_chat_rooms
|
||||
rooms = get_user_chat_rooms(user)
|
||||
rooms = [r for r in rooms if r.type == 'Direct']
|
||||
resp = dict(
|
||||
user = user.name,
|
||||
data = dict(
|
||||
status = self.status
|
||||
)
|
||||
)
|
||||
|
||||
for room in rooms:
|
||||
frappe.publish_realtime('frappe.chat.profile:update', resp, room = room.name, after_commit = True)
|
||||
|
||||
if 'display_widget' in fields:
|
||||
resp = dict(
|
||||
user = user.name,
|
||||
data = dict(
|
||||
display_widget = bool(self.display_widget)
|
||||
)
|
||||
)
|
||||
frappe.publish_realtime('frappe.chat.profile:update', resp, user = user.name, after_commit = True)
|
||||
|
||||
def get_user_chat_profile_doc(user = None):
|
||||
user = get_user_doc(user)
|
||||
prof = frappe.get_doc('Chat Profile', user.chat_profile)
|
||||
|
||||
return prof
|
||||
|
||||
def get_user_chat_profile(user = None, fields = None):
|
||||
'''
|
||||
Returns the Chat Profile for a given user.
|
||||
'''
|
||||
user = get_user_doc(user)
|
||||
prof = get_user_chat_profile_doc(user)
|
||||
|
||||
data = dict(
|
||||
name = user.name,
|
||||
email = user.email,
|
||||
first_name = user.first_name,
|
||||
last_name = user.last_name,
|
||||
username = user.username,
|
||||
avatar = user.user_image,
|
||||
bio = user.bio,
|
||||
|
||||
status = prof.status,
|
||||
chat_bg = prof.chat_background,
|
||||
|
||||
notification_tones = bool(prof.notification_tones),
|
||||
conversation_tones = bool(prof.conversation_tones), # frappe, y u no jsonify 0,1 bools? :(
|
||||
display_widget = bool(prof.display_widget)
|
||||
)
|
||||
|
||||
try:
|
||||
data = filter_dict(data, fields)
|
||||
except KeyError as e:
|
||||
frappe.throw(str(e))
|
||||
|
||||
return data
|
||||
|
||||
def get_new_chat_profile_doc(user = None, link = True):
|
||||
user = get_user_doc(user)
|
||||
prof = frappe.new_doc('Chat Profile')
|
||||
prof.save()
|
||||
|
||||
if link:
|
||||
user.update(dict(
|
||||
chat_profile = prof.name
|
||||
))
|
||||
user.save()
|
||||
|
||||
return prof
|
||||
|
||||
@frappe.whitelist()
|
||||
def create(user, exists_ok = False, fields = None):
|
||||
'''
|
||||
Creates a Chat Profile for the current session user, throws error if exists.
|
||||
'''
|
||||
exists, fields = safe_json_loads(exists_ok, fields)
|
||||
user = get_user_doc(user)
|
||||
|
||||
if user.name != session.user:
|
||||
frappe.throw(_("Sorry! You don't have permission to create a profile for user {name}.".format(
|
||||
name = user.name
|
||||
)))
|
||||
|
||||
if user.chat_profile:
|
||||
if not exists:
|
||||
frappe.throw(_("Sorry! You cannot create more than one Chat Profile."))
|
||||
|
||||
prof = get_user_chat_profile(user, fields)
|
||||
else:
|
||||
prof = get_new_chat_profile_doc(user)
|
||||
prof = get_user_chat_profile(user, fields)
|
||||
|
||||
return dictify(prof)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(user = None, fields = None):
|
||||
'''
|
||||
Returns a user's Chat Profile.
|
||||
'''
|
||||
fields = safe_json_loads(fields)
|
||||
prof = get_user_chat_profile(user, fields)
|
||||
|
||||
return dictify(prof)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update(user, data):
|
||||
data = safe_json_loads(data)
|
||||
user = get_user_doc(user)
|
||||
|
||||
if user.name != session.user:
|
||||
frappe.throw(_("Sorry! You don't have permission to update Chat Profile for user {name}.".format(
|
||||
name = user.name
|
||||
)))
|
||||
|
||||
prof = get_user_chat_profile_doc(user)
|
||||
prof.update(data)
|
||||
prof.save()
|
||||
11
frappe/chat/doctype/chat_profile/chat_profile_list.js
Normal file
11
frappe/chat/doctype/chat_profile/chat_profile_list.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
frappe.listview_settings['Chat Profile'] =
|
||||
{
|
||||
get_indicator: function (doc)
|
||||
{
|
||||
const status = frappe._.squash(frappe.chat.profile.STATUSES.filter(
|
||||
s => s.name === doc.status
|
||||
));
|
||||
|
||||
return [__(status.name), status.color, `status,=,${status.name}`]
|
||||
}
|
||||
};
|
||||
23
frappe/chat/doctype/chat_profile/test_chat_profile.js
Normal file
23
frappe/chat/doctype/chat_profile/test_chat_profile.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* eslint-disable */
|
||||
// rename this file from _test_[name] to test_[name] to activate
|
||||
// and remove above this line
|
||||
|
||||
QUnit.test("test: Chat Profile", function (assert) {
|
||||
let done = assert.async();
|
||||
|
||||
// number of asserts
|
||||
assert.expect(1);
|
||||
|
||||
frappe.run_serially([
|
||||
// insert a new Chat Profile
|
||||
() => frappe.tests.make('Chat Profile', [
|
||||
// values to be set
|
||||
{key: 'value'}
|
||||
]),
|
||||
() => {
|
||||
assert.equal(cur_frm.doc.key, 'value');
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
||||
});
|
||||
59
frappe/chat/doctype/chat_profile/test_chat_profile.py
Normal file
59
frappe/chat/doctype/chat_profile/test_chat_profile.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# imports - standard imports
|
||||
import unittest
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.chat.doctype.chat_profile import chat_profile
|
||||
from frappe.chat.util import get_user_doc, create_test_user
|
||||
|
||||
session = frappe.session
|
||||
test_user = create_test_user(__name__)
|
||||
|
||||
class TestChatProfile(unittest.TestCase):
|
||||
def test_create(self):
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
chat_profile.create(test_user)
|
||||
|
||||
user = get_user_doc(session.user)
|
||||
if not user.chat_profile:
|
||||
chat_profile.create(user.name)
|
||||
prof = chat_profile.get(user.name)
|
||||
self.assertEquals(prof.status, 'Online')
|
||||
else:
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
chat_profile.create(user.name)
|
||||
|
||||
def test_get(self):
|
||||
user = session.user
|
||||
prof = chat_profile.get(user)
|
||||
|
||||
self.assertNotEquals(len(prof), 1)
|
||||
|
||||
prof = chat_profile.get(user, fields = ['status'])
|
||||
self.assertEquals(len(prof), 1)
|
||||
self.assertEquals(prof.status, 'Online')
|
||||
|
||||
prof = chat_profile.get(user, fields = ['status', 'chat_bg'])
|
||||
self.assertEquals(len(prof), 2)
|
||||
|
||||
def test_update(self):
|
||||
user = test_user
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
prof = chat_profile.update(user, data = dict(
|
||||
status = 'Online'
|
||||
))
|
||||
|
||||
user = get_user_doc(session.user)
|
||||
prev = chat_profile.get(user.name)
|
||||
|
||||
chat_profile.update(user.name, data = dict(
|
||||
status = 'Offline'
|
||||
))
|
||||
prof = chat_profile.get(user.name)
|
||||
self.assertEquals(prof.status, 'Offline')
|
||||
# revert
|
||||
chat_profile.update(user.name, data = dict(
|
||||
status = prev.status
|
||||
))
|
||||
0
frappe/chat/doctype/chat_room/__init__.py
Normal file
0
frappe/chat/doctype/chat_room/__init__.py
Normal file
8
frappe/chat/doctype/chat_room/chat_room.js
Normal file
8
frappe/chat/doctype/chat_room/chat_room.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Room', {
|
||||
refresh: function(frm) {
|
||||
|
||||
}
|
||||
});
|
||||
314
frappe/chat/doctype/chat_room/chat_room.json
Normal file
314
frappe/chat/doctype/chat_room/chat_room.json
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "CR.#####",
|
||||
"beta": 1,
|
||||
"creation": "2017-11-08 15:27:21.156667",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Direct",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Direct\nGroup\nVisitor",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.type==\"Group\"",
|
||||
"fieldname": "room_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.type==\"Group\"",
|
||||
"fieldname": "avatar",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Avatar",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "last_message",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Last Message",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "message_count",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Message Count",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Owner",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "user_list",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Users",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "users",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Users",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Chat Room User",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_field": "avatar",
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-12-17 15:53:24.103274",
|
||||
"modified_by": "achilles@erpnext.com",
|
||||
"module": "Chat",
|
||||
"name": "Chat Room",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"search_fields": "room_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "room_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
296
frappe/chat/doctype/chat_room/chat_room.py
Normal file
296
frappe/chat/doctype/chat_room/chat_room.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
# imports - standard imports
|
||||
import json
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe import _, _dict
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.chat.doctype.chat_message.chat_message import get_messages
|
||||
from frappe.chat.util import (
|
||||
get_user_doc,
|
||||
safe_json_loads,
|
||||
dictify,
|
||||
listify,
|
||||
squashify,
|
||||
assign_if_none
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
# TODO
|
||||
# [x] Only Owners can edit the DocType Record.
|
||||
# [ ] Show Chat Room List that only has session.user in it.
|
||||
# [ ] Make Chat Room pagination configurable.
|
||||
|
||||
class ChatRoom(Document):
|
||||
def validate(self):
|
||||
# TODO - Validations
|
||||
# [x] Direct/Visitor must have 2 users only.
|
||||
# [x] Groups must have 1 (1 being ther session user) or more users.
|
||||
# [x] Ensure group has a name.
|
||||
# [x] Check if there is a one-to-one conversation between 2 users (if Direct/Visitor).
|
||||
|
||||
# First, check if user isn't stupid by adding many users or himself/herself.
|
||||
# C'mon yo, you're the owner.
|
||||
users = get_chat_room_user_set(self.users)
|
||||
users = [u for u in users if u.user != session.user]
|
||||
|
||||
self.update(dict(
|
||||
users = users
|
||||
))
|
||||
|
||||
if self.type in ("Direct", "Visitor"):
|
||||
if len(users) != 1:
|
||||
frappe.throw(_('{type} room must have atmost one user.'.format(
|
||||
type = self.type
|
||||
)))
|
||||
|
||||
# squash to a single object if it's a list.
|
||||
other = squashify(users)
|
||||
|
||||
# I don't know which idiot would make username not unique.
|
||||
# Remember, this entire app assumes validation based on user's email.
|
||||
|
||||
# Okay, this must go only during creation. But alas, on click "Save" it does the same.
|
||||
if self.is_new():
|
||||
if is_one_on_one(self.owner, other.user, bidirectional = True):
|
||||
frappe.throw(_('Direct room with {other} already exists.'.format(
|
||||
other = other.user
|
||||
)))
|
||||
|
||||
if self.type == "Group" and not self.room_name:
|
||||
frappe.throw(_('Group name cannot be empty.'))
|
||||
|
||||
# trigger from DocType
|
||||
def before_save(self):
|
||||
if not self.is_new():
|
||||
self.get_doc_before_save()
|
||||
|
||||
def on_update(self):
|
||||
user = session.user
|
||||
if user != self.owner:
|
||||
frappe.throw(_("Sorry! You don't enough permissions to update this room."))
|
||||
|
||||
if not self.is_new():
|
||||
before = self.get_doc_before_save()
|
||||
after = self
|
||||
|
||||
# TODO
|
||||
# [ ] Check if DocType is itself updated. WARN if not.
|
||||
diff = dictify(get_diff(before, after)) # whoever you are, thank you for this.
|
||||
if diff:
|
||||
# notify only if there is an update.
|
||||
update = dict() # Update Goodies.
|
||||
# Types of Differences
|
||||
# 1. Changes
|
||||
for changed in diff.changed:
|
||||
field, old, new = changed
|
||||
|
||||
if field == 'last_message':
|
||||
doc_message = frappe.get_doc('Chat Message', new)
|
||||
update.update({
|
||||
field: dict(
|
||||
name = doc_message.name,
|
||||
user = doc_message.user,
|
||||
room = doc_message.room,
|
||||
content = doc_message.content,
|
||||
urls = doc_message.urls,
|
||||
mentions = doc_message.mentions,
|
||||
creation = doc_message.creation
|
||||
)
|
||||
})
|
||||
else:
|
||||
update.update({
|
||||
field: new
|
||||
})
|
||||
# 2. Added or Removed
|
||||
# TODO
|
||||
# [ ] Handle users.
|
||||
# I personally feel this could be done better by either creating a new event
|
||||
# or attaching to the below event. Questions like Who removed Whom? Who added Whom? etc.
|
||||
# For first-cut, let's simply update the latest user list.
|
||||
# This is because the Version DocType already handles it.
|
||||
|
||||
if diff.added or diff.removed:
|
||||
update.update({
|
||||
'users': [u.user for u in self.users]
|
||||
})
|
||||
|
||||
resp = dict(
|
||||
room = self.name,
|
||||
data = update
|
||||
)
|
||||
|
||||
frappe.publish_realtime('frappe.chat.room:update', resp,
|
||||
room = self.name, after_commit = True)
|
||||
|
||||
def on_trash(self):
|
||||
if self.owner != session.user:
|
||||
frappe.throw(_("Sorry, you're not authorized to delete this room."))
|
||||
|
||||
def is_admin(user, room):
|
||||
if user != session.user:
|
||||
frappe.throw(_("Sorry, you're not authorized to view this information."))
|
||||
|
||||
# TODO - I'm tired searching the bug.
|
||||
|
||||
def is_one_on_one(owner, other, bidirectional = False):
|
||||
'''
|
||||
checks if the owner and other have a direct conversation room.
|
||||
'''
|
||||
def get_room(owner, other):
|
||||
room = frappe.get_list('Chat Room', filters = [
|
||||
['Chat Room', 'type' , 'in', ('Direct', 'Visitor')],
|
||||
['Chat Room', 'owner', '=' , owner],
|
||||
['Chat Room User', 'user' , '=' , other]
|
||||
], distinct = True)
|
||||
|
||||
return room
|
||||
|
||||
exists = len(get_room(owner, other)) == 1
|
||||
if bidirectional:
|
||||
exists = exists or len(get_room(other, owner)) == 1
|
||||
|
||||
return exists
|
||||
|
||||
def get_chat_room_user_set(users):
|
||||
'''
|
||||
Returns a set of Chat Room Users
|
||||
|
||||
:param users: sequence of Chat Room Users
|
||||
:return: set of Chat Room Users
|
||||
'''
|
||||
seen, news = set(), list()
|
||||
|
||||
for u in users:
|
||||
if u.user not in seen:
|
||||
news.append(u)
|
||||
seen.add(u.user)
|
||||
|
||||
return news
|
||||
|
||||
def get_new_chat_room_doc(kind, owner, users = None, name = None):
|
||||
room = frappe.new_doc('Chat Room')
|
||||
room.type = kind
|
||||
room.owner = owner
|
||||
room.room_name = name
|
||||
|
||||
users = users if isinstance(users, list) or users is None else [users]
|
||||
docs = [ ]
|
||||
if users:
|
||||
for user in users:
|
||||
doc = frappe.new_doc('Chat Room User')
|
||||
doc.user = user
|
||||
|
||||
docs.append(doc)
|
||||
|
||||
room.users = docs
|
||||
room.save()
|
||||
|
||||
return room
|
||||
|
||||
def get_new_chat_room(kind, owner, users = None, name = None):
|
||||
room = get_new_chat_room_doc(kind = kind, owner = owner, users = users, name = name)
|
||||
room = get_user_chat_rooms(user = owner, rooms = room.name)
|
||||
|
||||
return room
|
||||
|
||||
def get_user_chat_rooms(user = None, rooms = None, fields = None):
|
||||
'''
|
||||
if user is None, defaults to session user.
|
||||
if room is None, returns the entire list of rooms subscribed by user.
|
||||
'''
|
||||
user = get_user_doc(user)
|
||||
|
||||
rooms = assign_if_none(rooms, [ ])
|
||||
fields = assign_if_none(fields, [ ])
|
||||
|
||||
param = [f for f in fields if f != 'users' or f != 'last_message']
|
||||
|
||||
rooms = frappe.get_list('Chat Room',
|
||||
or_filters = [
|
||||
['Chat Room', 'owner', '=', user.name],
|
||||
['Chat Room User', 'user', '=', user.name]
|
||||
],
|
||||
filters = [
|
||||
['Chat Room', 'name', 'in', rooms]
|
||||
] if rooms else None,
|
||||
fields = param + ['name'] if param or 'users' in fields else [
|
||||
'type', 'name', 'owner', 'room_name', 'avatar', 'creation'
|
||||
],
|
||||
distinct = True
|
||||
)
|
||||
|
||||
if not fields or 'users' in fields:
|
||||
for i, r in enumerate(rooms):
|
||||
doc_room = frappe.get_doc('Chat Room', r.name)
|
||||
rooms[i]['users'] = [ ]
|
||||
|
||||
for user in doc_room.users:
|
||||
rooms[i]['users'].append(user.user)
|
||||
|
||||
if not fields or 'last_message' in fields:
|
||||
for i, r in enumerate(rooms):
|
||||
doc_room = frappe.get_doc('Chat Room', r.name)
|
||||
if doc_room.last_message:
|
||||
doc_message = frappe.get_doc('Chat Message', doc_room.last_message)
|
||||
rooms[i]['last_message'] = dict(
|
||||
name = doc_message.name,
|
||||
user = doc_message.user,
|
||||
room = doc_message.room,
|
||||
content = doc_message.content,
|
||||
urls = doc_message.urls,
|
||||
mentions = doc_message.mentions,
|
||||
creation = doc_message.creation
|
||||
)
|
||||
else:
|
||||
rooms[i]['last_message'] = None
|
||||
|
||||
rooms = dictify(rooms)
|
||||
|
||||
return rooms
|
||||
|
||||
@frappe.whitelist()
|
||||
def create(kind, owner, users = None, name = None):
|
||||
users = safe_json_loads(users)
|
||||
if owner != session.user:
|
||||
frappe.throw(_("Sorry! You're not authorized to create a Chat Room."))
|
||||
|
||||
room = get_new_chat_room(kind = kind, owner = owner, users = users, name = name)
|
||||
room = squashify(room)
|
||||
|
||||
users = [room.owner] + [u for u in room.users]
|
||||
for u in users:
|
||||
frappe.publish_realtime('frappe.chat.room:create', room,
|
||||
user = u, after_commit = True)
|
||||
|
||||
return room
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(user, rooms = None, fields = None):
|
||||
rooms = safe_json_loads(rooms)
|
||||
fields = safe_json_loads(fields)
|
||||
|
||||
user = get_user_doc(user)
|
||||
|
||||
if user.name != frappe.session.user:
|
||||
frappe.throw(_("You're not authorized to view this room."))
|
||||
|
||||
data = get_user_chat_rooms(user = user, rooms = rooms, fields = fields)
|
||||
|
||||
return data
|
||||
|
||||
# Could we move pagination to a config, but how?
|
||||
# One possibility is to add to Chat Profile itself.
|
||||
# Actually yes.
|
||||
@frappe.whitelist()
|
||||
def get_history(room, user = None, pagination = 20):
|
||||
user = get_user_doc(user)
|
||||
mess = get_messages(room, pagination = pagination)
|
||||
|
||||
mess = squashify(mess)
|
||||
|
||||
return dictify(mess)
|
||||
23
frappe/chat/doctype/chat_room/test_chat_room.js
Normal file
23
frappe/chat/doctype/chat_room/test_chat_room.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* eslint-disable */
|
||||
// rename this file from _test_[name] to test_[name] to activate
|
||||
// and remove above this line
|
||||
|
||||
QUnit.test("test: Chat Room", function (assert) {
|
||||
let done = assert.async();
|
||||
|
||||
// number of asserts
|
||||
assert.expect(1);
|
||||
|
||||
frappe.run_serially([
|
||||
// insert a new Chat Room
|
||||
() => frappe.tests.make('Chat Room', [
|
||||
// values to be set
|
||||
{key: 'value'}
|
||||
]),
|
||||
() => {
|
||||
assert.equal(cur_frm.doc.key, 'value');
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
||||
});
|
||||
10
frappe/chat/doctype/chat_room/test_chat_room.py
Normal file
10
frappe/chat/doctype/chat_room/test_chat_room.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
class TestChatRoom(unittest.TestCase):
|
||||
pass
|
||||
0
frappe/chat/doctype/chat_room_user/__init__.py
Normal file
0
frappe/chat/doctype/chat_room_user/__init__.py
Normal file
102
frappe/chat/doctype/chat_room_user/chat_room_user.json
Normal file
102
frappe/chat/doctype/chat_room_user/chat_room_user.json
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 1,
|
||||
"creation": "2017-11-08 15:24:21.029314",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "is_admin",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Admin",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-11-28 11:50:06.165435",
|
||||
"modified_by": "achilles@erpnext.com",
|
||||
"module": "Chat",
|
||||
"name": "Chat Room User",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
8
frappe/chat/doctype/chat_room_user/chat_room_user.py
Normal file
8
frappe/chat/doctype/chat_room_user/chat_room_user.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
import frappe
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatRoomUser(Document):
|
||||
pass
|
||||
0
frappe/chat/page/__init__.py
Normal file
0
frappe/chat/page/__init__.py
Normal file
0
frappe/chat/page/chat/__init__.py
Normal file
0
frappe/chat/page/chat/__init__.py
Normal file
11
frappe/chat/page/chat/chat.js
Normal file
11
frappe/chat/page/chat/chat.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
frappe.pages.chat.on_page_load = function (container)
|
||||
{
|
||||
const page = new frappe.ui.Page({
|
||||
title: __('Chat'), parent: container
|
||||
});
|
||||
const $container = $(container).find('.layout-main')
|
||||
$container.html("")
|
||||
|
||||
// const chat = new frappe.Chat($container, { layout: frappe.Chat.Layout.PAGE });
|
||||
// chat.render();
|
||||
};
|
||||
20
frappe/chat/page/chat/chat.json
Normal file
20
frappe/chat/page/chat/chat.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"content": null,
|
||||
"creation": "2017-11-08 14:55:47.986307",
|
||||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"icon": "octicon octiocn-comment",
|
||||
"idx": 0,
|
||||
"modified": "2017-12-17 10:44:23.698446",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "chat",
|
||||
"owner": "Administrator",
|
||||
"page_name": "chat",
|
||||
"roles": [],
|
||||
"script": null,
|
||||
"standard": "Yes",
|
||||
"style": null,
|
||||
"system_page": 0,
|
||||
"title": "Chat"
|
||||
}
|
||||
27
frappe/chat/page/chat/test_chat.js
Normal file
27
frappe/chat/page/chat/test_chat.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
QUnit.test("test: Chat", function (assert)
|
||||
{
|
||||
const done = assert.async(3);
|
||||
|
||||
assert.expect(3);
|
||||
|
||||
// test - frappe._.fuzzy_search
|
||||
frappe.run_serially([
|
||||
() => assert.equal(frappe._.fuzzy_search("foo", ["foobar", "tooti"]), "foobar"),
|
||||
]);
|
||||
|
||||
// test - frappe.chat.profile.create
|
||||
frappe.run_serially([
|
||||
() => frappe.set_route('chat'),
|
||||
// empty promise
|
||||
() => frappe.chat.profile.create(),
|
||||
(profile) => {
|
||||
assert.equal(profile.status, "Online");
|
||||
},
|
||||
// one key only
|
||||
() => frappe.chat.profile.create("status"),
|
||||
(profile) => {
|
||||
assert.equal(Object.keys(profile).length, 1);
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
});
|
||||
13
frappe/chat/util/__init__.py
Normal file
13
frappe/chat/util/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# imports - module imports
|
||||
from frappe.chat.util.util import (
|
||||
get_user_doc,
|
||||
squashify,
|
||||
safe_json_loads,
|
||||
filter_dict,
|
||||
assign_if_none,
|
||||
listify,
|
||||
dictify,
|
||||
check_url,
|
||||
create_test_user,
|
||||
get_emojis
|
||||
)
|
||||
35
frappe/chat/util/test_util.py
Normal file
35
frappe/chat/util/test_util.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# imports - standard imports
|
||||
import unittest
|
||||
|
||||
# imports - module imports
|
||||
from frappe.chat.util import (
|
||||
get_user_doc,
|
||||
safe_json_loads
|
||||
)
|
||||
import frappe
|
||||
|
||||
class TestChatUtil(unittest.TestCase):
|
||||
def test_safe_json_loads(self):
|
||||
number = safe_json_loads("1")
|
||||
self.assertEquals(type(number), int)
|
||||
|
||||
number = safe_json_loads("1.0")
|
||||
self.assertEquals(type(number), float)
|
||||
|
||||
string = safe_json_loads("foobar")
|
||||
self.assertEquals(type(string), str)
|
||||
|
||||
array = safe_json_loads('[{ "foo": "bar" }]')
|
||||
self.assertEquals(type(array), list)
|
||||
|
||||
objekt = safe_json_loads('{ "foo": "bar" }')
|
||||
self.assertEquals(type(objekt), dict)
|
||||
|
||||
true, null = safe_json_loads("true", "null")
|
||||
self.assertEquals(true, True)
|
||||
self.assertEquals(null, None)
|
||||
|
||||
def test_get_user_doc(self):
|
||||
# Needs more test cases.
|
||||
user = get_user_doc()
|
||||
self.assertEquals(user.name, frappe.session.user)
|
||||
114
frappe/chat/util/util.py
Normal file
114
frappe/chat/util/util.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# imports - third-party imports
|
||||
import requests
|
||||
|
||||
# imports - compatibility imports
|
||||
import six
|
||||
|
||||
# imports - standard imports
|
||||
from collections import MutableSequence, Mapping, MutableMapping
|
||||
if six.PY2:
|
||||
from urlparse import urlparse # PY2
|
||||
else:
|
||||
from urllib.parse import urlparse # PY3
|
||||
import json
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe.exceptions import DuplicateEntryError
|
||||
from frappe import _dict
|
||||
import frappe
|
||||
|
||||
session = frappe.session
|
||||
|
||||
def get_user_doc(user = None):
|
||||
if isinstance(user, Document):
|
||||
return user
|
||||
|
||||
user = user or session.user
|
||||
user = frappe.get_doc('User', user)
|
||||
|
||||
return user
|
||||
|
||||
def squashify(what):
|
||||
if isinstance(what, MutableSequence) and len(what) == 1:
|
||||
return what[0]
|
||||
|
||||
return what
|
||||
|
||||
def safe_json_loads(*args):
|
||||
results = [ ]
|
||||
|
||||
for arg in args:
|
||||
try:
|
||||
arg = json.loads(arg)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
results.append(arg)
|
||||
|
||||
return squashify(results)
|
||||
|
||||
def filter_dict(what, keys, ignore = False):
|
||||
copy = dict()
|
||||
|
||||
if keys:
|
||||
for k in keys:
|
||||
if k not in what and not ignore:
|
||||
raise KeyError('{key} not in dict.'.format(key = k))
|
||||
else:
|
||||
copy.update({
|
||||
k: what[k]
|
||||
})
|
||||
else:
|
||||
copy = what.copy()
|
||||
|
||||
return copy
|
||||
|
||||
def assign_if_none(a, b):
|
||||
if a is None:
|
||||
a = b
|
||||
return a
|
||||
|
||||
def listify(arg):
|
||||
if not isinstance(arg, list):
|
||||
arg = [arg]
|
||||
return arg
|
||||
|
||||
def dictify(arg):
|
||||
if isinstance(arg, MutableSequence):
|
||||
for i, a in enumerate(arg):
|
||||
arg[i] = dictify(a)
|
||||
elif isinstance(arg, MutableMapping):
|
||||
arg = _dict(arg)
|
||||
|
||||
return arg
|
||||
|
||||
def check_url(what, raise_err = False):
|
||||
if not urlparse(what).scheme:
|
||||
if raise_err:
|
||||
raise ValueError('{what} not a valid URL.')
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_test_user(module):
|
||||
try:
|
||||
test_user = frappe.new_doc('User')
|
||||
test_user.first_name = '{module}'.format(module = module)
|
||||
test_user.email = 'testuser.{module}@example.com'.format(module = module)
|
||||
test_user.save()
|
||||
except DuplicateEntryError:
|
||||
frappe.log('Test User Chat Profile exists.')
|
||||
|
||||
def get_emojis():
|
||||
redis = frappe.cache()
|
||||
emojis = redis.hget('frappe_emojis', 'emojis')
|
||||
|
||||
if not emojis:
|
||||
resp = requests.get('http://git.io/frappe-emoji')
|
||||
if resp.ok:
|
||||
emojis = resp.json()
|
||||
redis.hset('frappe_emojis', 'emojis', emojis)
|
||||
|
||||
return dictify(emojis)
|
||||
|
|
@ -306,6 +306,7 @@ def console(context):
|
|||
@click.option('--test', multiple=True, help="Specific test")
|
||||
@click.option('--driver', help="For Travis")
|
||||
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
|
||||
@click.option('--coverage', is_flag=True, default=False, help='Display Coverage Report')
|
||||
@click.option('--module', help="Run tests in a module")
|
||||
@click.option('--profile', is_flag=True, default=False)
|
||||
@click.option('--junit-xml-output', help="Destination file path for junit xml report")
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@
|
|||
"no_copy": 0,
|
||||
"oldfieldname": "standard",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nYes\nNo",
|
||||
"options": "Yes\nNo",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
|
|
@ -358,7 +358,7 @@
|
|||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-05-03 17:24:10.162110",
|
||||
"modified": "2017-11-13 16:37:04.422547",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Page",
|
||||
|
|
|
|||
23
frappe/core/doctype/page/test_page.js
Normal file
23
frappe/core/doctype/page/test_page.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* eslint-disable */
|
||||
// rename this file from _test_[name] to test_[name] to activate
|
||||
// and remove above this line
|
||||
|
||||
QUnit.test("test: Page", function (assert) {
|
||||
let done = assert.async();
|
||||
|
||||
// number of asserts
|
||||
assert.expect(1);
|
||||
|
||||
frappe.run_serially([
|
||||
// insert a new Page
|
||||
() => frappe.tests.make('Page', [
|
||||
// values to be set
|
||||
{key: 'value'}
|
||||
]),
|
||||
() => {
|
||||
assert.equal(cur_frm.doc.key, 'value');
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -2048,6 +2048,67 @@
|
|||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "chat_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Chat",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "chat_profile",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Chat Profile",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Chat Profile",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
|
|
@ -2063,8 +2124,8 @@
|
|||
"istable": 0,
|
||||
"max_attachments": 5,
|
||||
"menu_index": 0,
|
||||
"modified": "2017-11-01 09:04:51.151347",
|
||||
"modified_by": "manas@erpnext.com",
|
||||
"modified": "2017-11-15 13:01:00.085916",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
"owner": "Administrator",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ from frappe.limits import get_limits
|
|||
from frappe.website.utils import is_signup_enabled
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
# imports - frappe.chat
|
||||
from frappe.chat.doctype.chat_profile.chat_profile import get_user_chat_profile_doc
|
||||
|
||||
STANDARD_USERS = ("Guest", "Administrator")
|
||||
|
||||
class MaxUsersReachedError(frappe.ValidationError): pass
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
.message-row .bot-icon {
|
||||
width: 24px;
|
||||
margin-right: 5px;
|
||||
padding-left: 7px;
|
||||
font-size: 18px;
|
||||
/*color: #FEEF72;*/
|
||||
}
|
||||
|
||||
|
||||
.message-bubble {
|
||||
padding: 0px 10px;
|
||||
padding-top: 2px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #d1d8dd;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.message-bubble::before {
|
||||
background-color: #d1d8dd;
|
||||
content: "\00a0";
|
||||
display: block;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 70px;
|
||||
transform: rotate( 45deg ) skew( -35deg );
|
||||
-moz-transform: rotate( 45deg ) skew( -35deg );
|
||||
-ms-transform: rotate( 45deg ) skew( -35deg );
|
||||
-o-transform: rotate( 45deg ) skew( -35deg );
|
||||
-webkit-transform: rotate( 45deg ) skew( -35deg );
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.message-bubble.my-message {
|
||||
color: white;
|
||||
background-color: #5E64FF;
|
||||
}
|
||||
|
||||
.message-bubble.my-message::before {
|
||||
background-color: #5E64FF;
|
||||
}
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.pages.chat.on_page_load = function(parent) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: parent,
|
||||
});
|
||||
|
||||
page.set_title('<span class="hidden-xs">' + __("Chat") + '</span>'
|
||||
+ '<span class="hidden-sm hidden-md hidden-lg message-to"></span>');
|
||||
|
||||
$(".navbar-center").html(__("Chat"));
|
||||
|
||||
frappe.pages.chat.chat = new frappe.Chat(parent);
|
||||
}
|
||||
|
||||
frappe.pages.chat.on_page_show = function() {
|
||||
// clear title prefix
|
||||
frappe.utils.set_title_prefix("");
|
||||
|
||||
frappe.breadcrumbs.add("Desk");
|
||||
}
|
||||
|
||||
frappe.Chat = Class.extend({
|
||||
init: function(wrapper, page) {
|
||||
this.wrapper = wrapper;
|
||||
this.page = wrapper.page;
|
||||
this.make();
|
||||
this.page.sidebar.addClass("col-sm-3");
|
||||
this.page.wrapper.find(".layout-main-section-wrapper").addClass("col-sm-9");
|
||||
this.page.wrapper.find(".page-title").removeClass("col-xs-6").addClass("col-xs-12");
|
||||
this.page.wrapper.find(".page-actions").removeClass("col-xs-6").addClass("hidden-xs");
|
||||
this.setup_realtime();
|
||||
},
|
||||
|
||||
make: function() {
|
||||
this.make_sidebar();
|
||||
},
|
||||
|
||||
setup_realtime: function() {
|
||||
var me = this;
|
||||
frappe.realtime.on('new_message', function(comment) {
|
||||
if(comment.modified_by !== frappe.session.user || comment.communication_type === 'Bot') {
|
||||
if(frappe.get_route()[0] === 'chat') {
|
||||
var current_contact = $(cur_page.page).find('[data-contact]').data('contact');
|
||||
var on_broadcast_page = current_contact === frappe.session.user;
|
||||
if ((current_contact == comment.owner)
|
||||
|| (on_broadcast_page && comment.broadcast)
|
||||
|| current_contact === 'Bot' && comment.communication_type === 'Bot') {
|
||||
|
||||
setTimeout(function() { me.prepend_comment(comment); }, 1000);
|
||||
}
|
||||
} else {
|
||||
frappe.utils.notify(__("Message from {0}", [frappe.user_info(comment.owner).fullname]), comment.content);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
prepend_comment: function(comment) {
|
||||
frappe.pages.chat.chat.list.data.unshift(comment);
|
||||
this.render_row(comment, true);
|
||||
},
|
||||
|
||||
make_sidebar: function() {
|
||||
var me = this;
|
||||
return frappe.call({
|
||||
module:'frappe.desk',
|
||||
page:'chat',
|
||||
method:'get_active_users',
|
||||
callback: function(r,rt) {
|
||||
// sort
|
||||
r.message.sort(function(a, b) { return cint(b.has_session) - cint(a.has_session); });
|
||||
|
||||
// render
|
||||
me.page.sidebar.html(frappe.render_template("chat_sidebar", {data: r.message}));
|
||||
|
||||
// bind click
|
||||
me.page.sidebar.find("a").on("click", function() {
|
||||
var li = $(this).parents("li:first");
|
||||
if (li.hasClass("active"))
|
||||
return false;
|
||||
|
||||
var contact = li.attr("data-user");
|
||||
|
||||
// active
|
||||
me.page.sidebar.find("li.active").removeClass("active");
|
||||
me.page.sidebar.find('[data-user="'+ contact +'"]').addClass("active");
|
||||
|
||||
me.make_messages(contact);
|
||||
});
|
||||
|
||||
$(me.page.sidebar.find("a")[0]).click();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
make_messages: function(contact) {
|
||||
var me = this;
|
||||
|
||||
this.page.main.html($(frappe.render_template("chat_main", { "contact": contact })));
|
||||
|
||||
var text_area = this.page.main.find(".messages-textarea").on("focusout", function() {
|
||||
// on touchscreen devices, scroll to top
|
||||
// so that static navbar and page head don't overlap the textarea
|
||||
if (frappe.dom.is_touchscreen()) {
|
||||
frappe.utils.scroll_to($(this).parents(".message-box"));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var post_btn = this.page.main.find(".btn-post").on("click", function() {
|
||||
var btn = $(this);
|
||||
var message_box = btn.parents(".message-box");
|
||||
var textarea = message_box.find("textarea");
|
||||
var contact = btn.attr("data-contact");
|
||||
var txt = textarea.val();
|
||||
var send_email = message_box.find('input[type="checkbox"]:checked').length > 0;
|
||||
|
||||
if(txt) {
|
||||
return frappe.call({
|
||||
module: 'frappe.desk',
|
||||
page:'chat',
|
||||
method:'post',
|
||||
args: {
|
||||
txt: txt,
|
||||
contact: contact,
|
||||
notify: send_email ? 1 : 0
|
||||
},
|
||||
callback:function(r,rt) {
|
||||
textarea.val('');
|
||||
if (!r.exc) {
|
||||
me.prepend_comment(r.message);
|
||||
}
|
||||
},
|
||||
btn: this
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
text_area.keydown("meta+return ctrl+return", function(e) {
|
||||
post_btn.trigger("click");
|
||||
});
|
||||
|
||||
this.page.wrapper.find(".page-head .message-to").html(frappe.user.full_name(contact));
|
||||
|
||||
this.make_message_list(contact);
|
||||
|
||||
this.list.run();
|
||||
|
||||
frappe.utils.scroll_to();
|
||||
},
|
||||
|
||||
make_message_list: function(contact) {
|
||||
var me = this;
|
||||
|
||||
this.list = new frappe.ui.BaseList({
|
||||
parent: this.page.main.find(".message-list"),
|
||||
page: this.page,
|
||||
method: 'frappe.desk.page.chat.chat.get_list',
|
||||
args: {
|
||||
contact: contact
|
||||
},
|
||||
hide_refresh: true,
|
||||
freeze: false,
|
||||
render_view: function (values) {
|
||||
values.map(function (value) {
|
||||
me.render_row(value);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
render_row: function(value, prepend) {
|
||||
this.prepare(value)
|
||||
|
||||
var wrapper = $('<div class="list-row">')
|
||||
.data("data", this.meta)
|
||||
|
||||
if(!prepend)
|
||||
wrapper.appendTo($(".result-list")).get(0);
|
||||
else
|
||||
wrapper.prependTo($(".result-list")).get(0);
|
||||
|
||||
var row = $(frappe.render_template("chat_row", {
|
||||
data: value
|
||||
})).appendTo(wrapper)
|
||||
row.find(".avatar, .indicator").tooltip();
|
||||
},
|
||||
|
||||
delete: function(ele) {
|
||||
$(ele).parent().css('opacity', 0.6);
|
||||
return frappe.call({
|
||||
method: 'frappe.desk.page.chat.chat.delete',
|
||||
args: {name : $(ele).attr('data-name')},
|
||||
callback: function() {
|
||||
$(ele).parents(".list-row:first").toggle(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function() {},
|
||||
|
||||
get_contact: function() {
|
||||
var route = location.hash;
|
||||
if(route.indexOf('/')!=-1) {
|
||||
var name = decodeURIComponent(route.split('/')[1]);
|
||||
if(name.indexOf('__at__')!=-1) {
|
||||
name = name.replace('__at__', '@');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
},
|
||||
|
||||
prepare: function(data) {
|
||||
if(data.communication_type==="Notification" || data.comment_type==="Shared") {
|
||||
data.is_system_message = 1;
|
||||
}
|
||||
|
||||
if(data.owner==data.reference_name
|
||||
&& data.communication_type!=="Notification"
|
||||
&& data.comment_type!=="Bot") {
|
||||
data.is_public = true;
|
||||
}
|
||||
|
||||
if(data.owner==data.reference_name && data.communication_type !== "Bot") {
|
||||
data.is_mine = true;
|
||||
}
|
||||
|
||||
if(data.owner==data.reference_name && data.communication_type === "Bot") {
|
||||
data.owner = 'bot';
|
||||
}
|
||||
|
||||
data.content = frappe.markdown(data.content.substr(0, 1000));
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"content": null,
|
||||
"creation": "2012-06-14 18:44:56",
|
||||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"icon": "",
|
||||
"idx": 1,
|
||||
"modified": "2016-03-31 02:02:13.503910",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "chat",
|
||||
"owner": "Administrator",
|
||||
"page_name": "chat",
|
||||
"roles": [
|
||||
{
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"script": null,
|
||||
"standard": "Yes",
|
||||
"style": null,
|
||||
"title": "Chat"
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.desk.notifications import delete_notification_count_for
|
||||
from frappe.core.doctype.user.user import STANDARD_USERS
|
||||
from frappe.utils import cint
|
||||
from frappe import _
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_list(arg=None):
|
||||
"""get list of messages"""
|
||||
frappe.form_dict['start'] = int(frappe.form_dict['start'])
|
||||
frappe.form_dict['page_length'] = int(frappe.form_dict['page_length'])
|
||||
frappe.form_dict['user'] = frappe.session['user']
|
||||
|
||||
# set all messages as read
|
||||
frappe.db.sql("""UPDATE `tabCommunication` set seen = 1
|
||||
where
|
||||
communication_type in ('Chat', 'Notification')
|
||||
and seen = 0
|
||||
and reference_doctype = 'User'
|
||||
and reference_name = %s""", frappe.session.user)
|
||||
|
||||
delete_notification_count_for("Chat")
|
||||
|
||||
frappe.local.flags.commit = True
|
||||
|
||||
fields = '''name, owner, modified, content, communication_type,
|
||||
comment_type, reference_doctype, reference_name'''
|
||||
|
||||
if frappe.form_dict.contact == 'Bot':
|
||||
return frappe.db.sql("""select {0} from `tabCommunication`
|
||||
where
|
||||
comment_type = 'Bot'
|
||||
and reference_doctype = 'User'
|
||||
and reference_name = %(user)s
|
||||
order by creation desc
|
||||
limit %(start)s, %(page_length)s""".format(fields),
|
||||
frappe.local.form_dict, as_dict=1)
|
||||
|
||||
if frappe.form_dict.contact == frappe.session.user:
|
||||
# return messages
|
||||
return frappe.db.sql("""select {0} from `tabCommunication`
|
||||
where
|
||||
communication_type in ('Chat', 'Notification')
|
||||
and comment_type != 'Bot'
|
||||
and reference_doctype ='User'
|
||||
and (owner=%(contact)s
|
||||
or reference_name=%(user)s
|
||||
or owner=reference_name)
|
||||
order by creation desc
|
||||
limit %(start)s, %(page_length)s""".format(fields),
|
||||
frappe.local.form_dict, as_dict=1)
|
||||
else:
|
||||
return frappe.db.sql("""select {0} from `tabCommunication`
|
||||
where
|
||||
communication_type in ('Chat', 'Notification')
|
||||
and comment_type != 'Bot'
|
||||
and reference_doctype ='User'
|
||||
and ((owner=%(contact)s and reference_name=%(user)s)
|
||||
or (owner=%(user)s and reference_name=%(contact)s))
|
||||
order by creation desc
|
||||
limit %(start)s, %(page_length)s""".format(fields),
|
||||
frappe.local.form_dict, as_dict=1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_active_users():
|
||||
data = frappe.db.sql("""select name,
|
||||
(select count(*) from tabSessions where user=tabUser.name
|
||||
and timediff(now(), lastupdate) < time("01:00:00")) as has_session
|
||||
from tabUser
|
||||
where enabled=1 and
|
||||
ifnull(user_type, '')!='Website User' and
|
||||
name not in ({})
|
||||
order by first_name""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS, as_dict=1)
|
||||
|
||||
# make sure current user is at the top, using has_session = 100
|
||||
users = [d.name for d in data]
|
||||
|
||||
if frappe.session.user in users:
|
||||
data[users.index(frappe.session.user)]["has_session"] = 100
|
||||
|
||||
else:
|
||||
# in case of administrator
|
||||
data.append({"name": frappe.session.user, "has_session": 100})
|
||||
|
||||
if 'System Manager' in frappe.get_roles():
|
||||
data.append({"name": "Bot", "has_session": 100})
|
||||
|
||||
return data
|
||||
|
||||
@frappe.whitelist()
|
||||
def post(txt, contact, parenttype=None, notify=False, subject=None):
|
||||
"""post message"""
|
||||
|
||||
comment_type = None
|
||||
if contact == 'Bot':
|
||||
contact = frappe.session.user
|
||||
comment_type = 'Bot'
|
||||
|
||||
d = frappe.new_doc('Communication')
|
||||
d.communication_type = 'Notification' if parenttype else 'Chat'
|
||||
d.subject = subject
|
||||
d.content = txt
|
||||
d.reference_doctype = 'User'
|
||||
d.reference_name = contact
|
||||
d.sender = frappe.session.user
|
||||
|
||||
if comment_type:
|
||||
d.comment_type = comment_type
|
||||
|
||||
d.insert(ignore_permissions=True)
|
||||
|
||||
delete_notification_count_for("Chat")
|
||||
|
||||
if notify and cint(notify):
|
||||
_notify(contact, txt, subject)
|
||||
|
||||
return d
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete(arg=None):
|
||||
frappe.get_doc("Communication", frappe.form_dict['name']).delete()
|
||||
|
||||
def _notify(contact, txt, subject=None):
|
||||
from frappe.utils import get_fullname, get_url
|
||||
|
||||
try:
|
||||
if not isinstance(contact, list):
|
||||
contact = [frappe.db.get_value("User", contact, "email") or contact]
|
||||
frappe.sendmail(\
|
||||
recipients=contact,
|
||||
sender= frappe.db.get_value("User", frappe.session.user, "email"),
|
||||
subject=subject or _("New Message from {0}").format(get_fullname(frappe.session.user)),
|
||||
template="new_message",
|
||||
args={
|
||||
"from": get_fullname(frappe.session.user),
|
||||
"message": txt,
|
||||
"link": get_url()
|
||||
},
|
||||
header=[_('New Message'), 'orange'])
|
||||
except frappe.OutgoingEmailError:
|
||||
pass
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<div class="message-box">
|
||||
<div class="media timeline-head">
|
||||
<span class="pull-left avatar avatar-medium hidden-xs">
|
||||
<img class="media-object" src="{%= frappe.user.image() %}">
|
||||
</span>
|
||||
<div class="media-body">
|
||||
<textarea style="height: 120px" style="margin-top: 10px;"
|
||||
class="form-control messages-textarea"></textarea>
|
||||
</div>
|
||||
<div style="padding-top: 15px;">
|
||||
<span class="text-muted small hidden-xs"
|
||||
style="margin-left: 45px;">{{ __("Ctrl + Enter to post") }}</span>
|
||||
<button class="pull-right btn btn-primary btn-sm btn-post" data-contact="{%= contact %}">
|
||||
{%= __("Post") %}
|
||||
</button>
|
||||
{% if (contact === user) { %}
|
||||
<span class="pull-right"
|
||||
style="margin-top: 4px; margin-right: 10px;">
|
||||
<i class="octicon octicon-rss"></i>
|
||||
<span class="text-muted small">{%= __("Public") %}</span>
|
||||
</span>
|
||||
{% } %}
|
||||
<div class="pull-right checkbox text-muted small"
|
||||
style="margin-right: 15px; margin-top: 7px;">
|
||||
<label>
|
||||
<input type="checkbox" class="is-email"
|
||||
style="margin-top: 1px">
|
||||
{%= __("Email") %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-list"></div>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<div class="row message-row small">
|
||||
<div class="col-sm-9">
|
||||
<div class="media">
|
||||
{% if (data.is_public) { %}
|
||||
<span class="pull-left hidden-xs" title="{{ __("Public") }}">
|
||||
<i class="octicon octicon-rss text-muted" style="margin-top: 3px;"></i></span>
|
||||
{% } else { %}
|
||||
<span class="pull-left hidden-xs"
|
||||
style="width: 20px; height: 16px; display: inline-block;"></span>
|
||||
{% } %}
|
||||
<div class="pull-left hidden-xs">
|
||||
<span class="avatar avatar-small" title="{%= frappe.user.full_name(data.owner) %} ">
|
||||
<img class="media-object {{ data.is_system_message ? "grayscale" : "" }}"
|
||||
src="{%= frappe.user.image(data.owner) %}">
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-body {{ data.is_system_message ? "system-message" : "" }} message-bubble {{ data.is_mine ? "my-message" : "" }}">
|
||||
{%= data.content %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 text-right message-row-right">
|
||||
<div class="text-muted">
|
||||
<span class="hidden-sm hidden-md hidden-lg">
|
||||
{%= frappe.user.full_name(data.owner) %},
|
||||
</span>
|
||||
{%= comment_when(data.modified) %}
|
||||
</div>
|
||||
{% if (data.owner==user) { %}
|
||||
<div>
|
||||
<a class="delete text-extra-muted" data-name="{%= data.name %}"
|
||||
onclick="frappe.pages.chat.chat.delete(this)">Delete</a>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<ul class="nav nav-pills nav-stacked">
|
||||
{% for (var i=0, l= data.length; i < l; i++) { var contact = data[i]; %}
|
||||
<li data-user="{%= contact.name %}" class="h6 module-sidebar-item">
|
||||
<a class="messages-sidebar-link ellipsis">
|
||||
<span class="indicator {% if(contact.has_session > 0) { %} green {% } else { %} grey {% } %}">
|
||||
<span class="avatar avatar-small hidden-sm hidden-md hidden-lg" title="{%= frappe.user.full_name(contact.name) %} ">
|
||||
<img class="media-object" src="{%= frappe.user.image(contact.name) %}">
|
||||
</span>
|
||||
<span class="hidden-xs">{%= contact.name===user ? __("Everyone") : frappe.user.full_name(contact.name) %}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% } %}
|
||||
</ul>
|
||||
|
|
@ -63,6 +63,8 @@ before_tests = "frappe.utils.install.before_tests"
|
|||
|
||||
email_append_to = ["Event", "ToDo", "Communication"]
|
||||
|
||||
get_rooms = 'frappe.chat.doctype.chat_room.chat_room.get_rooms'
|
||||
|
||||
calendars = ["Event"]
|
||||
|
||||
# login
|
||||
|
|
@ -185,6 +187,10 @@ sounds = [
|
|||
{"name": "error", "src": "/assets/frappe/sounds/error.mp3", "volume": 0.1},
|
||||
# {"name": "alert", "src": "/assets/frappe/sounds/alert.mp3"},
|
||||
# {"name": "chime", "src": "/assets/frappe/sounds/chime.mp3"},
|
||||
|
||||
# frappe chat sounds
|
||||
{ "name": "chat-message", "src": "/assets/frappe/sounds/chat-message.mp3", "volume": 0.1 },
|
||||
{ "name": "chat-notification", "src": "/assets/frappe/sounds/chat-noticication.mp3", "volume": 0.1 }
|
||||
]
|
||||
|
||||
bot_parsers = [
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ Desk
|
|||
Integrations
|
||||
Printing
|
||||
Contacts
|
||||
Data Migration
|
||||
Data Migration
|
||||
Chat
|
||||
|
|
@ -3,7 +3,9 @@
|
|||
"public/css/font-awesome.css",
|
||||
"public/css/octicons/octicons.css",
|
||||
"public/css/website.css",
|
||||
"public/css/avatar.css"
|
||||
"public/css/avatar.css",
|
||||
|
||||
"public/css/chat.css"
|
||||
],
|
||||
"js/frappe-web.min.js": [
|
||||
"public/js/frappe/class.js",
|
||||
|
|
@ -20,7 +22,11 @@
|
|||
"public/js/lib/microtemplate.js",
|
||||
"public/js/frappe/query_string.js",
|
||||
"website/js/website.js",
|
||||
"public/js/frappe/misc/rating_icons.html"
|
||||
"public/js/frappe/misc/rating_icons.html",
|
||||
|
||||
"public/js/lib/hyper.min.js",
|
||||
"public/js/lib/fuse.min.js",
|
||||
"public/js/frappe/chat.js"
|
||||
],
|
||||
"js/control.min.js": [
|
||||
"public/js/frappe/ui/capture.js",
|
||||
|
|
@ -124,7 +130,7 @@
|
|||
"public/css/mobile.css",
|
||||
"public/css/kanban.css",
|
||||
"public/css/controls.css",
|
||||
"public/css/tags.css"
|
||||
"public/css/chat.css"
|
||||
],
|
||||
"css/frappe-rtl.css": [
|
||||
"public/css/bootstrap-rtl.css",
|
||||
|
|
@ -146,11 +152,13 @@
|
|||
"public/js/lib/datepicker/datepicker.min.js",
|
||||
"public/js/lib/datepicker/locale-all.js",
|
||||
"public/js/lib/frappe-charts/frappe-charts.min.iife.js",
|
||||
"public/js/lib/webcam.min.js",
|
||||
"public/js/lib/leaflet/leaflet.js",
|
||||
"public/js/lib/leaflet/leaflet.draw.js",
|
||||
"public/js/lib/leaflet/L.Control.Locate.js",
|
||||
"public/js/lib/leaflet/easy-button.js"
|
||||
"public/js/lib/leaflet/easy-button.js",
|
||||
|
||||
"public/js/lib/hyper.min.js",
|
||||
"public/js/lib/fuse.min.js"
|
||||
],
|
||||
"js/desk.min.js": [
|
||||
"public/js/frappe/class.js",
|
||||
|
|
@ -247,7 +255,9 @@
|
|||
"public/js/frappe/ui/comment.js",
|
||||
"public/js/frappe/misc/rating_icons.html",
|
||||
|
||||
"public/js/frappe/feedback.js"
|
||||
"public/js/frappe/feedback.js",
|
||||
|
||||
"public/js/frappe/chat.js"
|
||||
],
|
||||
"css/module.min.css": [
|
||||
"public/css/module.css"
|
||||
|
|
|
|||
121
frappe/public/css/chat.css
Normal file
121
frappe/public/css/chat.css
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-heavy {
|
||||
font-weight: 900;
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatar {
|
||||
padding: 1px;
|
||||
}
|
||||
.frappe-fab {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.frappe-fab:hover {
|
||||
box-shadow: 0px 5px 9px 0px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.frappe-fab.frappe-fab-sm {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.frappe-fab.frappe-fab-lg {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
.frappe-chat .panel {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
.frappe-chat .panel .panel-heading {
|
||||
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
.frappe-chat .panel .panel-body {
|
||||
padding: 10px;
|
||||
}
|
||||
.frappe-chat .panel .frappe-chat-room-footer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
}
|
||||
.frappe-chat .frappe-chat-form .form-control {
|
||||
font-size: 12px;
|
||||
}
|
||||
.frappe-chat .frappe-chat-form .dropdown-menu {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.frappe-chat .frappe-chat-form .btn {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
.frappe-chat .frappe-chat-form .list-group {
|
||||
margin-bottom: 0px !important;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.frappe-chat .frappe-chat-form .list-group .list-group-item:first-child,
|
||||
.frappe-chat .frappe-chat-form .list-group .list-group-item:last-child {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
.frappe-chat-popper {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin: 20px;
|
||||
z-index: 1035;
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin: 20px;
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel {
|
||||
position: relative;
|
||||
box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.25);
|
||||
width: 350px;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel .panel-body {
|
||||
width: 350px;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading {
|
||||
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading .action {
|
||||
padding: 5px;
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading a {
|
||||
color: #FFF;
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading .text-muted {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.frappe-chat-popper .frappe-chat-popper-collapse .panel-span {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
z-index: 1037;
|
||||
overflow: auto;
|
||||
border-radius: none;
|
||||
}
|
||||
.frappe-chat-emoji .dropdown-menu {
|
||||
min-width: 250px;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.frappe-chat-emoji .panel {
|
||||
margin-bottom: 0 !important;
|
||||
height: 300px;
|
||||
}
|
||||
.frappe-chat-emoji .panel .form-group {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
.list-id {
|
||||
margin-left: 7px !important;
|
||||
}
|
||||
.avatar-small {
|
||||
.avatar-small .avatar-sm {
|
||||
margin-left: 5px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
|
|
|||
2551
frappe/public/js/frappe/chat.js
Normal file
2551
frappe/public/js/frappe/chat.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -18,6 +18,12 @@ $(document).ready(function() {
|
|||
});
|
||||
}
|
||||
frappe.start_app();
|
||||
|
||||
// frappe.Chat
|
||||
// Removing it from here as per rushabh@frappe.io's request.
|
||||
// const chat = new frappe.Chat()
|
||||
// chat.render();
|
||||
// end frappe.Chat
|
||||
});
|
||||
|
||||
frappe.Application = Class.extend({
|
||||
|
|
@ -29,7 +35,7 @@ frappe.Application = Class.extend({
|
|||
this.startup();
|
||||
},
|
||||
startup: function() {
|
||||
frappe.socketio.init();
|
||||
frappe.socketio.init()
|
||||
frappe.model.init();
|
||||
|
||||
if(frappe.boot.status==='failed') {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
|
|||
tooltip: 'Camera',
|
||||
click: () => {
|
||||
const capture = new frappe.ui.Capture();
|
||||
capture.open();
|
||||
capture.show();
|
||||
|
||||
capture.click((data) => {
|
||||
capture.submit((data) => {
|
||||
context.invoke('editor.insertImage', data);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ frappe.ui.form.Grid = Class.extend({
|
|||
this.wrapper.find(".grid-add-row").click(function() {
|
||||
me.add_new_row(null, null, true);
|
||||
me.set_focus_on_row();
|
||||
|
||||
// excel like paste.
|
||||
me.setup_paste();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
|
|
@ -71,7 +75,69 @@ frappe.ui.form.Grid = Class.extend({
|
|||
this.df.on_setup(this);
|
||||
}
|
||||
|
||||
this.setup_paste();
|
||||
},
|
||||
|
||||
setup_paste: function ( ) {
|
||||
const me = this;
|
||||
$(document).ready(function ( ) {
|
||||
const $grid = $(me.wrapper).find('.form-grid');
|
||||
const $rows = $grid.find('.grid-body .rows');
|
||||
const $areas = $rows.find('.field-area');
|
||||
$areas.click(function ( ) {
|
||||
// Who even comes up with this BS?
|
||||
if ( $(this).css('display') === 'block' ) {
|
||||
const $input = $(this).find('input');
|
||||
|
||||
$input.on('paste', function (e) {
|
||||
setTimeout(function ( ) {
|
||||
const value = e.target.value;
|
||||
|
||||
// Do stuff here.
|
||||
const rows = value.split(' ');
|
||||
if ( rows.length ) {
|
||||
const first = rows[0];
|
||||
|
||||
// Who cares validating? They'll do it for us.
|
||||
const $area = $rows.find('.field-area').last();
|
||||
if ( $area.css('display') === 'block' ) {
|
||||
const $input = $area.find('input');
|
||||
$input.val(first);
|
||||
}
|
||||
|
||||
for (var i = 1 ; i < rows.length ; ++i) {
|
||||
me.add_new_row(null, null, true);
|
||||
me.set_focus_on_row();
|
||||
|
||||
// const $area = $rows.find('.field-area').last();
|
||||
// if ( $area.css('display') === 'block' ) {
|
||||
// const $input = $area.find('input');
|
||||
// $input.val(rows[i]);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// for (var i = 1 ; i < rows.length - 1 ; ++i) {
|
||||
// // // Assuming only single column.
|
||||
// // const elements = rows[i];
|
||||
|
||||
// // me.add_new_row(null, null, true);
|
||||
// // me.set_focus_on_row();
|
||||
|
||||
// // // const grid_row = me.grid_rows[me.grid_rows.length - rows.length];
|
||||
// // // console.log(me.grid_rows.length);
|
||||
// // const grid_row = me.grid_rows[me.grid_rows.length - 1];
|
||||
// // console.log(me.grid_rows.length - 1);
|
||||
// }
|
||||
// end stuff
|
||||
|
||||
}, 100); // When you can't even
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setup_check: function() {
|
||||
var me = this;
|
||||
this.wrapper.on('click', '.grid-row-check', function(e) {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ frappe.get_abbr = function(txt, max_length) {
|
|||
// continue
|
||||
return true;
|
||||
}
|
||||
|
||||
87
|
||||
abbr += w.trim()[0];
|
||||
});
|
||||
|
||||
|
|
|
|||
15
frappe/public/js/frappe/peer.js
Normal file
15
frappe/public/js/frappe/peer.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
frappe.Peer = class
|
||||
{
|
||||
constructor ( )
|
||||
{
|
||||
this.peer = new Peer()
|
||||
this.peer.on('open', (ID) => {
|
||||
console.log(`A new peer connection has occured with ID: ${ID}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
frappe.Peer.boot = ( ) =>
|
||||
{
|
||||
const client = new frappe.Peer()
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ frappe.request.waiting_for_ajax = [];
|
|||
|
||||
// generic server call (call page, object)
|
||||
frappe.call = function(opts) {
|
||||
|
||||
if (typeof arguments[0]==='string') {
|
||||
opts = {
|
||||
method: arguments[0],
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ frappe.socketio = {
|
|||
// notify that the user has closed this doc
|
||||
frappe.socketio.socket.emit('doc_close', doctype, docname);
|
||||
},
|
||||
|
||||
setup_listeners: function() {
|
||||
frappe.socketio.socket.on('task_status_change', function(data) {
|
||||
frappe.socketio.process_response(data, data.status.toLowerCase());
|
||||
|
|
@ -390,5 +391,4 @@ frappe.socketio.SocketIOUploader = class SocketIOUploader {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,94 +1,204 @@
|
|||
// frappe.ui.Capture
|
||||
// Author - Achilles Rasquinha <achilles@frappe.io>
|
||||
|
||||
/**
|
||||
* @description Converts a canvas, image or a video to a data URL string.
|
||||
*
|
||||
* @param {HTMLElement} element - canvas, img or video.
|
||||
* @returns {string} - The data URL string.
|
||||
*
|
||||
* @example
|
||||
* frappe._.get_data_uri(video)
|
||||
* // returns "data:image/pngbase64,..."
|
||||
*/
|
||||
frappe._.get_data_uri = element =>
|
||||
{
|
||||
const $element = $(element)
|
||||
const width = $element.width()
|
||||
const height = $element.height()
|
||||
|
||||
const $canvas = $('<canvas/>')
|
||||
$canvas[0].width = width
|
||||
$canvas[0].height = height
|
||||
|
||||
const context = $canvas[0].getContext('2d')
|
||||
context.drawImage($element[0], 0, 0, width, height)
|
||||
|
||||
const data_uri = $canvas[0].toDataURL('image/png')
|
||||
|
||||
return data_uri
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Frappe's Capture object.
|
||||
*
|
||||
* @example
|
||||
* const capture = frappe.ui.Capture()
|
||||
* capture.show()
|
||||
*
|
||||
* capture.click((data_uri) => {
|
||||
* // do stuff
|
||||
* })
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Taking_still_photos
|
||||
*/
|
||||
frappe.ui.Capture = class
|
||||
{
|
||||
constructor (options = { })
|
||||
{
|
||||
this.options = Object.assign({}, frappe.ui.Capture.DEFAULT_OPTIONS, options);
|
||||
this.dialog = new frappe.ui.Dialog();
|
||||
this.template =
|
||||
`
|
||||
<div class="text-center">
|
||||
<div class="img-thumbnail" style="border: none;">
|
||||
<div id="frappe-capture"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="frappe-capture-btn-toolbar" style="padding-top: 15px; padding-bottom: 15px;">
|
||||
<div class="text-center">
|
||||
<div id="frappe-capture-btn-toolbar-snap">
|
||||
<a id="frappe-capture-btn-snap">
|
||||
<i class="fa fa-fw fa-2x fa-circle-o"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group" id="frappe-capture-btn-toolbar-knap">
|
||||
<button class="btn btn-default" id="frappe-capture-btn-discard">
|
||||
<i class="fa fa-fw fa-arrow-left"/>
|
||||
</button>
|
||||
<button class="btn btn-default" id="frappe-capture-btn-accept">
|
||||
<i class="fa fa-fw fa-arrow-right"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$(this.dialog.body).append(this.template);
|
||||
|
||||
this.$btnBarSnap = $(this.dialog.body).find('#frappe-capture-btn-toolbar-snap');
|
||||
this.$btnBarKnap = $(this.dialog.body).find('#frappe-capture-btn-toolbar-knap');
|
||||
this.$btnBarKnap.hide();
|
||||
|
||||
Webcam.set(this.options);
|
||||
this.options = frappe.ui.Capture.OPTIONS
|
||||
this.set_options(options)
|
||||
}
|
||||
|
||||
open ( )
|
||||
|
||||
set_options (options)
|
||||
{
|
||||
this.dialog.show();
|
||||
|
||||
Webcam.attach('#frappe-capture');
|
||||
}
|
||||
|
||||
freeze ( )
|
||||
{
|
||||
this.$btnBarSnap.hide();
|
||||
this.$btnBarKnap.show();
|
||||
this.options = { ...frappe.ui.Capture.OPTIONS, ...options }
|
||||
|
||||
Webcam.freeze();
|
||||
return this
|
||||
}
|
||||
|
||||
render ( )
|
||||
{
|
||||
return navigator.mediaDevices.getUserMedia({ video: true }).then(stream =>
|
||||
{
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: this.options.title,
|
||||
animate: this.options.animate,
|
||||
action:
|
||||
{
|
||||
secondary:
|
||||
{
|
||||
label: "<b>×</b>"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const $e = $(frappe.ui.Capture.TEMPLATE)
|
||||
|
||||
const video = $e.find('video')[0]
|
||||
video.srcObject = stream
|
||||
video.play()
|
||||
|
||||
const $container = $(this.dialog.body)
|
||||
$container.html($e)
|
||||
|
||||
$e.find('.fc-btf').hide()
|
||||
|
||||
$e.find('.fc-bcp').click(() =>
|
||||
{
|
||||
const data_url = frappe._.get_data_uri(video)
|
||||
$e.find('.fc-p').attr('src', data_url)
|
||||
|
||||
$e.find('.fc-s').hide()
|
||||
$e.find('.fc-p').show()
|
||||
|
||||
$e.find('.fc-btu').hide()
|
||||
$e.find('.fc-btf').show()
|
||||
})
|
||||
|
||||
$e.find('.fc-br').click(() =>
|
||||
{
|
||||
$e.find('.fc-p').hide()
|
||||
$e.find('.fc-s').show()
|
||||
|
||||
$e.find('.fc-btf').hide()
|
||||
$e.find('.fc-btu').show()
|
||||
})
|
||||
|
||||
$e.find('.fc-bs').click(() =>
|
||||
{
|
||||
const data_url = frappe._.get_data_uri(video)
|
||||
this.hide()
|
||||
|
||||
if (this.callback)
|
||||
this.callback(data_url)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
unfreeze ( )
|
||||
show ( )
|
||||
{
|
||||
this.$btnBarSnap.show();
|
||||
this.$btnBarKnap.hide();
|
||||
this.render().then(() =>
|
||||
{
|
||||
this.dialog.show()
|
||||
}).catch(err => {
|
||||
if ( this.options.error )
|
||||
{
|
||||
const alert = `<span class="indicator red"/> ${frappe.ui.Capture.ERR_MESSAGE}`
|
||||
frappe.show_alert(alert, 3)
|
||||
}
|
||||
|
||||
Webcam.unfreeze();
|
||||
}
|
||||
|
||||
click (callback)
|
||||
{
|
||||
$(this.dialog.body).find('#frappe-capture-btn-snap').click(() => {
|
||||
this.freeze();
|
||||
|
||||
$(this.dialog.body).find('#frappe-capture-btn-discard').click(() => {
|
||||
this.unfreeze();
|
||||
});
|
||||
|
||||
$(this.dialog.body).find('#frappe-capture-btn-accept').click(() => {
|
||||
Webcam.snap((data) => {
|
||||
callback(data);
|
||||
});
|
||||
|
||||
this.hide();
|
||||
});
|
||||
});
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
hide ( )
|
||||
{
|
||||
Webcam.reset();
|
||||
|
||||
$(this.dialog.$wrapper).remove();
|
||||
if ( this.dialog )
|
||||
this.dialog.hide()
|
||||
}
|
||||
};
|
||||
frappe.ui.Capture.DEFAULT_OPTIONS =
|
||||
|
||||
submit (fn)
|
||||
{
|
||||
this.callback = fn
|
||||
}
|
||||
}
|
||||
frappe.ui.Capture.OPTIONS =
|
||||
{
|
||||
width: 480, height: 320, flip_horiz: true
|
||||
};
|
||||
title: __(`Camera`),
|
||||
animate: false,
|
||||
error: false,
|
||||
}
|
||||
frappe.ui.Capture.ERR_MESSAGE = __("Unable to load camera.")
|
||||
frappe.ui.Capture.TEMPLATE =
|
||||
`
|
||||
<div class="frappe-capture">
|
||||
<div class="panel panel-default">
|
||||
<img class="fc-p img-responsive"/>
|
||||
<div class="fc-s embed-responsive embed-responsive-16by9">
|
||||
<video class="embed-responsive-item">${frappe.ui.Capture.ERR_MESSAGE}</video>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fc-btf">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="pull-left">
|
||||
<button class="btn btn-default fc-br">
|
||||
<small>${__('Retake')}</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-primary fc-bs">
|
||||
<small>${__('Submit')}</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fc-btu">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
${
|
||||
''
|
||||
// <div class="pull-left">
|
||||
// <button class="btn btn-default">
|
||||
// <small>${__('Take Video')}</small>
|
||||
// </button>
|
||||
// </div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default fc-bcp">
|
||||
<small>${__('Take Photo')}</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
|
@ -11,14 +11,20 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({
|
|||
this.display = false;
|
||||
this.is_dialog = true;
|
||||
|
||||
$.extend(this, opts);
|
||||
$.extend(this, { animate: true, size: null }, opts);
|
||||
this._super();
|
||||
this.make();
|
||||
},
|
||||
make: function() {
|
||||
this.$wrapper = frappe.get_modal("", "");
|
||||
|
||||
this.wrapper = this.$wrapper.find('.modal-dialog')
|
||||
.get(0);
|
||||
if ( this.size == "small" )
|
||||
$(this.wrapper).addClass("modal-sm");
|
||||
else if ( this.size == "large" )
|
||||
$(this.wrapper).addClass("modal-lg");
|
||||
|
||||
this.make_head();
|
||||
this.body = this.$wrapper.find(".modal-body").get(0);
|
||||
this.header = this.$wrapper.find(".modal-header");
|
||||
|
|
@ -27,12 +33,13 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({
|
|||
this._super();
|
||||
|
||||
// show footer
|
||||
if(this.primary_action) {
|
||||
this.set_primary_action(this.primary_action_label || __("Submit"), this.primary_action);
|
||||
this.action = this.action || { primary: { }, secondary: { } };
|
||||
if(this.primary_action || this.action.primary) {
|
||||
this.set_primary_action(this.primary_action_label || this.action.primary.label || __("Submit"), this.primary_action || this.action.primary.click);
|
||||
}
|
||||
|
||||
if (this.secondary_action_label) {
|
||||
this.get_close_btn().html(this.secondary_action_label);
|
||||
if (this.secondary_action_label || this.action.secondary.label) {
|
||||
this.get_close_btn().html(this.secondary_action_label || this.action.secondary.label);
|
||||
}
|
||||
|
||||
var me = this;
|
||||
|
|
@ -101,6 +108,11 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({
|
|||
},
|
||||
show: function() {
|
||||
// show it
|
||||
if ( this.animate ) {
|
||||
this.$wrapper.addClass('fade')
|
||||
} else {
|
||||
this.$wrapper.removeClass('fade')
|
||||
}
|
||||
this.$wrapper.modal("show");
|
||||
this.primary_action_fulfilled = false;
|
||||
this.is_visible = true;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
|
||||
<!--
|
||||
<li class="dropdown dropdown-help dropdown-mobile">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#"
|
||||
onclick="return false;" style="height: 40px;">
|
||||
|
|
@ -68,6 +69,17 @@
|
|||
{%= __("About") %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
-->
|
||||
|
||||
<!-- Frappe Chat -->
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle frappe-chat-btn" style="height: 40px; text-align: center;">
|
||||
<div>
|
||||
<i class="octicon octicon-comment" style="margin-top: 5px;"/>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<!-- end Frappe Chat -->
|
||||
|
||||
<li class="dropdown dropdown-navbar-new-comments dropdown-mobile">
|
||||
<a class="btn dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
|
|
|
|||
|
|
@ -21,12 +21,23 @@ frappe.ui.toolbar.Toolbar = Class.extend({
|
|||
make: function() {
|
||||
this.setup_sidebar();
|
||||
this.setup_help();
|
||||
|
||||
// Frappe Chat (added to toolbar as per rushabh@frappe.io request)
|
||||
this.setup_chat()
|
||||
// end Frappe Chat
|
||||
|
||||
this.setup_progress_dialog();
|
||||
this.bind_events();
|
||||
|
||||
$(document).trigger('toolbar_setup');
|
||||
},
|
||||
|
||||
setup_chat ( )
|
||||
{
|
||||
const chat = new frappe.Chat({ target: '.navbar .frappe-chat-btn' })
|
||||
chat.render()
|
||||
},
|
||||
|
||||
bind_events: function() {
|
||||
$(document).on("notification-update", function() {
|
||||
frappe.ui.notifications.update_notifications();
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ frappe.setup.UserProgressDialog = class UserProgressDialog {
|
|||
frappe.call({
|
||||
method: "frappe.desk.user_progress.update_and_get_user_progress",
|
||||
callback: function(r) {
|
||||
// console.log("states", r.message);
|
||||
console.log("states", r.message);
|
||||
let states = r.message;
|
||||
let changed = 0;
|
||||
let completed = 0;
|
||||
|
|
|
|||
1
frappe/public/js/lib/fuse.min.js
vendored
Normal file
1
frappe/public/js/lib/fuse.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frappe/public/js/lib/hyper.min.js
vendored
Normal file
1
frappe/public/js/lib/hyper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
frappe/public/js/lib/webcam.min.js
vendored
2
frappe/public/js/lib/webcam.min.js
vendored
File diff suppressed because one or more lines are too long
225
frappe/public/less/chat.less
Normal file
225
frappe/public/less/chat.less
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// variables
|
||||
@color-white: #FFF; //
|
||||
|
||||
@font-weight-bold: 700; //
|
||||
@font-weight-heavy: 900; //
|
||||
|
||||
@frappe-chat-popper-panel-width: 350px; //
|
||||
@frappe-chat-popper-panel-height: 500px; //
|
||||
|
||||
@frappe-chat-form-font-size: 12px;
|
||||
|
||||
|
||||
@frappe-fab-width: 48px;
|
||||
@frappe-fab-height: 48px;
|
||||
@frappe-fab-lg: 56px;
|
||||
@frappe-fab-sm: 40px;
|
||||
// https://github.com/twbs/bootstrap/blob/v3.3.7/less/variables.less#L278
|
||||
// Keep z-index of the FAB button higher than others, lower than modal background.
|
||||
@frappe-fab-box-shadow: 0px 3px 6px 0px rgba(0,0,0,.25);
|
||||
@frappe-fab-box-shadow-hover: 0px 5px 9px 0px rgba(0,0,0,.25);
|
||||
|
||||
@frappe-chat-panel-heading-box-shadow: 0px 2px 2px 0px rgba(0,0,0,.14); //
|
||||
@frappe-chat-panel-body-padding: 10px;
|
||||
@frappe-chat-panel-heading-action-padding: 5px;
|
||||
|
||||
@frappe-chat-popper-z-index: 1035;
|
||||
@frappe-chat-popper-margin: 20px;
|
||||
@frappe-chat-popper-panel-box-shadow: @frappe-fab-box-shadow;
|
||||
// z-index greater than FAB, lesser than modal.
|
||||
@frappe-chat-popper-panel-span-z-index: 1037;
|
||||
|
||||
@frappe-chat-form-list-group-height: 150px;
|
||||
@frappe-chat-form-menu-border-radius: 4px;
|
||||
|
||||
@frappe-chat-emoji-width: 250px;
|
||||
@frappe-chat-emoji-height: 300px;
|
||||
|
||||
.font-bold
|
||||
{
|
||||
font-weight: @font-weight-bold;
|
||||
}
|
||||
|
||||
.font-heavy
|
||||
{
|
||||
font-weight: @font-weight-heavy;
|
||||
}
|
||||
|
||||
.cursor-pointer
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar
|
||||
{
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.frappe-fab
|
||||
{
|
||||
width: @frappe-fab-width;
|
||||
height: @frappe-fab-height;
|
||||
border-radius: 50%;
|
||||
box-shadow: @frappe-fab-box-shadow;
|
||||
|
||||
&:hover
|
||||
{
|
||||
box-shadow: @frappe-fab-box-shadow-hover;
|
||||
};
|
||||
|
||||
&.frappe-fab-sm
|
||||
{
|
||||
width: @frappe-fab-sm;
|
||||
height: @frappe-fab-sm;
|
||||
};
|
||||
|
||||
&.frappe-fab-lg
|
||||
{
|
||||
width: @frappe-fab-lg;
|
||||
height: @frappe-fab-lg;
|
||||
};
|
||||
};
|
||||
|
||||
.frappe-chat
|
||||
{
|
||||
.panel
|
||||
{
|
||||
margin-bottom: 0px !important;
|
||||
|
||||
.panel-heading
|
||||
{
|
||||
box-shadow: @frappe-chat-panel-heading-box-shadow;
|
||||
}
|
||||
|
||||
.panel-body
|
||||
{
|
||||
padding: @frappe-chat-panel-body-padding;
|
||||
}
|
||||
|
||||
.frappe-chat-room-footer
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-chat-form
|
||||
{
|
||||
.form-control
|
||||
{
|
||||
font-size: @frappe-chat-form-font-size;
|
||||
}
|
||||
|
||||
.dropdown-menu
|
||||
{
|
||||
border-radius: @frappe-chat-form-menu-border-radius;
|
||||
}
|
||||
|
||||
.btn
|
||||
{
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
.list-group
|
||||
{
|
||||
margin-bottom: 0px !important;
|
||||
max-height: @frappe-chat-form-list-group-height;
|
||||
overflow-y: auto;
|
||||
|
||||
.list-group-item:first-child, .list-group-item:last-child
|
||||
{
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-chat-popper
|
||||
{
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin: @frappe-chat-popper-margin;
|
||||
|
||||
z-index: @frappe-chat-popper-z-index;
|
||||
|
||||
.frappe-chat-popper-collapse
|
||||
{
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin: @frappe-chat-popper-margin;
|
||||
// margin-bottom: calc(@frappe-chat-popper-margin + 50px);
|
||||
|
||||
& > .panel
|
||||
{
|
||||
position: relative;
|
||||
box-shadow: @frappe-chat-popper-panel-box-shadow;
|
||||
width: @frappe-chat-popper-panel-width;
|
||||
height: @frappe-chat-popper-panel-height;
|
||||
overflow-y: auto;
|
||||
|
||||
.panel-body
|
||||
{
|
||||
width: @frappe-chat-popper-panel-width;
|
||||
height: @frappe-chat-popper-panel-height;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
& > .panel-heading
|
||||
{
|
||||
box-shadow: @frappe-chat-panel-heading-box-shadow;
|
||||
|
||||
.action
|
||||
{
|
||||
padding: @frappe-chat-panel-heading-action-padding;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
color: @color-white;
|
||||
}
|
||||
|
||||
.text-muted
|
||||
{
|
||||
color: @color-white !important;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
.panel-span
|
||||
{
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
z-index: @frappe-chat-popper-panel-span-z-index;
|
||||
overflow: auto;
|
||||
border-radius: none;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
.frappe-chat-emoji
|
||||
{
|
||||
.dropdown-menu
|
||||
{
|
||||
min-width: @frappe-chat-emoji-width;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.panel
|
||||
{
|
||||
margin-bottom: 0 !important;
|
||||
height: @frappe-chat-emoji-height;
|
||||
|
||||
.form-group
|
||||
{
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
};
|
||||
BIN
frappe/public/sounds/chat-message.mp3
Normal file
BIN
frappe/public/sounds/chat-message.mp3
Normal file
Binary file not shown.
BIN
frappe/public/sounds/chat-notification.mp3
Normal file
BIN
frappe/public/sounds/chat-notification.mp3
Normal file
Binary file not shown.
|
|
@ -114,6 +114,7 @@ login.signup = function() {
|
|||
// Login
|
||||
login.call = function(args, callback) {
|
||||
login.set_indicator("{{ _('Verifying...') }}", 'blue');
|
||||
|
||||
return frappe.call({
|
||||
type: "POST",
|
||||
args: args,
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False):
|
|||
|
||||
out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite)
|
||||
|
||||
|
||||
if profile:
|
||||
pr.disable()
|
||||
s = StringIO()
|
||||
|
|
@ -200,7 +201,6 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False):
|
|||
|
||||
return out
|
||||
|
||||
|
||||
def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False):
|
||||
import os
|
||||
|
||||
|
|
|
|||
|
|
@ -408,3 +408,11 @@ $(document).on("page-change", function() {
|
|||
element && element.scrollIntoView(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$(document).ready(function ( ) {
|
||||
// frappe.Chat
|
||||
// const chat = new frappe.Chat();
|
||||
// chat.render();
|
||||
// end frappe.Chat
|
||||
});
|
||||
|
|
@ -117,7 +117,7 @@ frappe.ready(function() {
|
|||
method: 'frappe.core.doctype.user.user.test_password_strength',
|
||||
args: args,
|
||||
callback: function(r) {
|
||||
// console.log(r.message);
|
||||
console.log(r.message);
|
||||
},
|
||||
statusCode: {
|
||||
401: function() {
|
||||
|
|
|
|||
149
socketio.js
149
socketio.js
|
|
@ -1,6 +1,6 @@
|
|||
var app = require('express')();
|
||||
var http = require('http').Server(app);
|
||||
var io = require('socket.io')(http);
|
||||
var server = require('http').Server(app);
|
||||
var io = require('socket.io')(server);
|
||||
var cookie = require('cookie')
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
|
@ -22,117 +22,151 @@ var files_struct = {
|
|||
var subscriber = redis.createClient(conf.redis_socketio || conf.redis_async_broker_port);
|
||||
|
||||
// serve socketio
|
||||
http.listen(conf.socketio_port, function() {
|
||||
console.log('listening on *:', conf.socketio_port); //eslint-disable-line
|
||||
server.listen(conf.socketio_port, function () {
|
||||
console.log('listening on *:', conf.socketio_port); //eslint-disable-line
|
||||
});
|
||||
|
||||
// test route
|
||||
app.get('/', function(req, res) {
|
||||
app.get('/', function (req, res) {
|
||||
res.sendfile('index.html');
|
||||
});
|
||||
|
||||
// on socket connection
|
||||
io.on('connection', function(socket) {
|
||||
io.on('connection', function (socket) {
|
||||
|
||||
console.log(socket.request.headers)
|
||||
|
||||
if (get_hostname(socket.request.headers.host) != get_hostname(socket.request.headers.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("connection!");
|
||||
if (!socket.request.headers.cookie) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var sid = cookie.parse(socket.request.headers.cookie).sid
|
||||
if(!sid) {
|
||||
if (!sid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(flags[sid]) {
|
||||
|
||||
|
||||
|
||||
if (flags[sid]) {
|
||||
// throttle this function
|
||||
return;
|
||||
}
|
||||
|
||||
flags[sid] = sid;
|
||||
setTimeout(function() { flags[sid] = null; }, 10000);
|
||||
setTimeout(function () {
|
||||
flags[sid] = null;
|
||||
}, 10000);
|
||||
|
||||
socket.user = cookie.parse(socket.request.headers.cookie).user_id;
|
||||
socket.files = {};
|
||||
|
||||
// console.log("firing get_user_info");
|
||||
// frappe.chat
|
||||
socket.on("frappe.chat.room:subscribe", function (rooms) {
|
||||
if (!Array.isArray(rooms)) {
|
||||
rooms = [rooms];
|
||||
}
|
||||
|
||||
for (var room of rooms) {
|
||||
console.log('frappe.chat: Subscribing ' + socket.user + ' to room ' + room);
|
||||
room = get_chat_room(socket, room);
|
||||
|
||||
console.log('frappe.chat: Subscribing ' + socket.user + ' to event ' + room);
|
||||
socket.join(room);
|
||||
}
|
||||
});
|
||||
socket.on("frappe.chat.message:typing", function (data) {
|
||||
const user = data.user;
|
||||
const room = get_chat_room(socket, data.room);
|
||||
|
||||
console.log('frappe.chat: Dispatching ' + user + ' typing to room ' + room);
|
||||
|
||||
io.to(room).emit('frappe.chat.room:typing', {
|
||||
room: data.room,
|
||||
user: user
|
||||
});
|
||||
});
|
||||
|
||||
console.log("firing get_user_info");
|
||||
request.get(get_url(socket, '/api/method/frappe.async.get_user_info'))
|
||||
.type('form')
|
||||
.query({
|
||||
sid: sid
|
||||
})
|
||||
.end(function(err, res) {
|
||||
if(err) {
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
if(res.status == 200) {
|
||||
if (res.status == 200) {
|
||||
var room = get_user_room(socket, res.body.message.user);
|
||||
// console.log('joining', room);
|
||||
console.log('joining', room);
|
||||
socket.join(room);
|
||||
socket.join(get_site_room(socket));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
delete socket.files;
|
||||
})
|
||||
|
||||
socket.on('task_subscribe', function(task_id) {
|
||||
socket.on('task_subscribe', function (task_id) {
|
||||
var room = get_task_room(socket, task_id);
|
||||
socket.join(room);
|
||||
});
|
||||
|
||||
socket.on('task_unsubscribe', function(task_id) {
|
||||
socket.on('task_unsubscribe', function (task_id) {
|
||||
var room = get_task_room(socket, task_id);
|
||||
socket.leave(room);
|
||||
});
|
||||
|
||||
socket.on('progress_subscribe', function(task_id) {
|
||||
socket.on('progress_subscribe', function (task_id) {
|
||||
var room = get_task_room(socket, task_id);
|
||||
socket.join(room);
|
||||
send_existing_lines(task_id, socket);
|
||||
});
|
||||
|
||||
socket.on('doc_subscribe', function(doctype, docname) {
|
||||
// console.log('trying to subscribe', doctype, docname)
|
||||
socket.on('doc_subscribe', function (doctype, docname) {
|
||||
console.log('trying to subscribe', doctype, docname)
|
||||
can_subscribe_doc({
|
||||
socket: socket,
|
||||
sid: sid,
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
callback: function(err, res) {
|
||||
callback: function (err, res) {
|
||||
var room = get_doc_room(socket, doctype, docname);
|
||||
// console.log('joining', room)
|
||||
console.log('joining', room)
|
||||
socket.join(room);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('doc_unsubscribe', function(doctype, docname) {
|
||||
socket.on('doc_unsubscribe', function (doctype, docname) {
|
||||
var room = get_doc_room(socket, doctype, docname);
|
||||
socket.leave(room);
|
||||
});
|
||||
|
||||
socket.on('task_unsubscribe', function(task_id) {
|
||||
socket.on('task_unsubscribe', function (task_id) {
|
||||
var room = 'task:' + task_id;
|
||||
socket.leave(room);
|
||||
});
|
||||
|
||||
socket.on('doc_open', function(doctype, docname) {
|
||||
socket.on('doc_open', function (doctype, docname) {
|
||||
// show who is currently viewing the form
|
||||
can_subscribe_doc({
|
||||
socket: socket,
|
||||
sid: sid,
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
callback: function(err, res) {
|
||||
callback: function (err, res) {
|
||||
var room = get_open_doc_room(socket, doctype, docname);
|
||||
// console.log('joining', room)
|
||||
console.log('joining', room)
|
||||
socket.join(room);
|
||||
|
||||
send_viewers({
|
||||
|
|
@ -144,7 +178,7 @@ io.on('connection', function(socket) {
|
|||
});
|
||||
});
|
||||
|
||||
socket.on('doc_close', function(doctype, docname) {
|
||||
socket.on('doc_close', function (doctype, docname) {
|
||||
// remove this user from the list of 'who is currently viewing the form'
|
||||
var room = get_open_doc_room(socket, doctype, docname);
|
||||
socket.leave(room);
|
||||
|
|
@ -197,17 +231,18 @@ io.on('connection', function(socket) {
|
|||
});
|
||||
});
|
||||
|
||||
subscriber.on("message", function(channel, message) {
|
||||
message = JSON.parse(message);
|
||||
io.to(message.room).emit(message.event, message.message);
|
||||
// console.log(message.room, message.event, message.message)
|
||||
subscriber.on("message", function (channel, message, room) {
|
||||
message = JSON.parse(message)
|
||||
console.log(message)
|
||||
io.to(message.room).emit(message.event, message.message)
|
||||
});
|
||||
|
||||
|
||||
subscriber.subscribe("events");
|
||||
|
||||
function send_existing_lines(task_id, socket) {
|
||||
var room = get_task_room(socket, task_id);
|
||||
subscriber.hgetall('task_log:' + task_id, function(err, lines) {
|
||||
subscriber.hgetall('task_log:' + task_id, function (err, lines) {
|
||||
io.to(room).emit('task_progress', {
|
||||
"task_id": task_id,
|
||||
"message": {
|
||||
|
|
@ -218,11 +253,11 @@ function send_existing_lines(task_id, socket) {
|
|||
}
|
||||
|
||||
function get_doc_room(socket, doctype, docname) {
|
||||
return get_site_name(socket) + ':doc:'+ doctype + '/' + docname;
|
||||
return get_site_name(socket) + ':doc:' + doctype + '/' + docname;
|
||||
}
|
||||
|
||||
function get_open_doc_room(socket, doctype, docname) {
|
||||
return get_site_name(socket) + ':open_doc:'+ doctype + '/' + docname;
|
||||
return get_site_name(socket) + ':open_doc:' + doctype + '/' + docname;
|
||||
}
|
||||
|
||||
function get_user_room(socket, user) {
|
||||
|
|
@ -237,19 +272,25 @@ function get_task_room(socket, task_id) {
|
|||
return get_site_name(socket) + ':task_progress:' + task_id;
|
||||
}
|
||||
|
||||
// frappe.chat
|
||||
// If you're thinking on multi-site or anything, please
|
||||
// update frappe.async as well.
|
||||
function get_chat_room(socket, room) {
|
||||
var room = get_site_name(socket) + ":room:" + room;
|
||||
|
||||
return room
|
||||
}
|
||||
|
||||
function get_site_name(socket) {
|
||||
if (socket.request.headers['x-frappe-site-name']) {
|
||||
return get_hostname(socket.request.headers['x-frappe-site-name']);
|
||||
}
|
||||
else if (['localhost', '127.0.0.1'].indexOf(socket.request.headers.host) !== -1
|
||||
&& conf.default_site) {
|
||||
} else if (['localhost', '127.0.0.1'].indexOf(socket.request.headers.host) !== -1 &&
|
||||
conf.default_site) {
|
||||
// from currentsite.txt since host is localhost
|
||||
return conf.default_site;
|
||||
}
|
||||
else if (socket.request.headers.origin) {
|
||||
} else if (socket.request.headers.origin) {
|
||||
return get_hostname(socket.request.headers.origin);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return get_hostname(socket.request.headers.host);
|
||||
}
|
||||
}
|
||||
|
|
@ -259,7 +300,7 @@ function get_hostname(url) {
|
|||
if (url.indexOf("://") > -1) {
|
||||
url = url.split('/')[2];
|
||||
}
|
||||
return ( url.match(/:/g) ) ? url.slice( 0, url.indexOf(":") ) : url
|
||||
return (url.match(/:/g)) ? url.slice(0, url.indexOf(":")) : url
|
||||
}
|
||||
|
||||
function get_url(socket, path) {
|
||||
|
|
@ -270,8 +311,8 @@ function get_url(socket, path) {
|
|||
}
|
||||
|
||||
function can_subscribe_doc(args) {
|
||||
if(!args) return;
|
||||
if(!args.doctype || !args.docname) return;
|
||||
if (!args) return;
|
||||
if (!args.doctype || !args.docname) return;
|
||||
request.get(get_url(args.socket, '/api/method/frappe.async.can_subscribe_doc'))
|
||||
.type('form')
|
||||
.query({
|
||||
|
|
@ -279,7 +320,7 @@ function can_subscribe_doc(args) {
|
|||
doctype: args.doctype,
|
||||
docname: args.docname
|
||||
})
|
||||
.end(function(err, res) {
|
||||
.end(function (err, res) {
|
||||
if (!res) {
|
||||
console.log("No response for doc_subscribe");
|
||||
|
||||
|
|
@ -318,7 +359,7 @@ function send_viewers(args) {
|
|||
var viewers = [];
|
||||
for (var i in io.sockets.sockets) {
|
||||
var s = io.sockets.sockets[i];
|
||||
if (clients.indexOf(s.id)!==-1) {
|
||||
if (clients.indexOf(s.id) !== -1) {
|
||||
// this socket is connected to the room
|
||||
viewers.push(s.user);
|
||||
}
|
||||
|
|
@ -339,8 +380,8 @@ function get_conf() {
|
|||
socketio_port: 3000
|
||||
};
|
||||
|
||||
var read_config = function(path) {
|
||||
if(fs.existsSync(path)){
|
||||
var read_config = function (path) {
|
||||
if (fs.existsSync(path)) {
|
||||
var bench_config = JSON.parse(fs.readFileSync(path));
|
||||
for (var key in bench_config) {
|
||||
if (bench_config[key]) {
|
||||
|
|
@ -355,11 +396,9 @@ function get_conf() {
|
|||
read_config('sites/common_site_config.json');
|
||||
|
||||
// detect current site
|
||||
if(fs.existsSync('sites/currentsite.txt')) {
|
||||
if (fs.existsSync('sites/currentsite.txt')) {
|
||||
conf.default_site = fs.readFileSync('sites/currentsite.txt').toString().trim();
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue