Quellcode durchsuchen

🎉 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
version-14
Achilles Rasquinha vor 7 Jahren
committed by Nabin Hait
Ursprung
Commit
005cfe3dc8
80 geänderte Dateien mit 5520 neuen und 694 gelöschten Zeilen
  1. +0
    -3
      .vscode/settings.json
  2. +12
    -3
      frappe/async.py
  3. +1
    -1
      frappe/build.js
  4. +0
    -0
      frappe/chat/README.md
  5. +0
    -0
      frappe/chat/__init__.py
  6. +0
    -0
      frappe/chat/doctype/__init__.py
  7. +0
    -0
      frappe/chat/doctype/chat_message/__init__.py
  8. +8
    -0
      frappe/chat/doctype/chat_message/chat_message.js
  9. +245
    -0
      frappe/chat/doctype/chat_message/chat_message.json
  10. +162
    -0
      frappe/chat/doctype/chat_message/chat_message.py
  11. +23
    -0
      frappe/chat/doctype/chat_message/test_chat_message.js
  12. +19
    -0
      frappe/chat/doctype/chat_message/test_chat_message.py
  13. +0
    -0
      frappe/chat/doctype/chat_message_attachment/__init__.py
  14. +71
    -0
      frappe/chat/doctype/chat_message_attachment/chat_message_attachment.json
  15. +10
    -0
      frappe/chat/doctype/chat_message_attachment/chat_message_attachment.py
  16. +0
    -0
      frappe/chat/doctype/chat_profile/__init__.py
  17. +8
    -0
      frappe/chat/doctype/chat_profile/chat_profile.js
  18. +278
    -0
      frappe/chat/doctype/chat_profile/chat_profile.json
  19. +173
    -0
      frappe/chat/doctype/chat_profile/chat_profile.py
  20. +11
    -0
      frappe/chat/doctype/chat_profile/chat_profile_list.js
  21. +23
    -0
      frappe/chat/doctype/chat_profile/test_chat_profile.js
  22. +59
    -0
      frappe/chat/doctype/chat_profile/test_chat_profile.py
  23. +0
    -0
      frappe/chat/doctype/chat_room/__init__.py
  24. +8
    -0
      frappe/chat/doctype/chat_room/chat_room.js
  25. +314
    -0
      frappe/chat/doctype/chat_room/chat_room.json
  26. +296
    -0
      frappe/chat/doctype/chat_room/chat_room.py
  27. +23
    -0
      frappe/chat/doctype/chat_room/test_chat_room.js
  28. +10
    -0
      frappe/chat/doctype/chat_room/test_chat_room.py
  29. +0
    -0
      frappe/chat/doctype/chat_room_user/__init__.py
  30. +102
    -0
      frappe/chat/doctype/chat_room_user/chat_room_user.json
  31. +8
    -0
      frappe/chat/doctype/chat_room_user/chat_room_user.py
  32. +0
    -0
      frappe/chat/page/__init__.py
  33. +0
    -0
      frappe/chat/page/chat/__init__.py
  34. +11
    -0
      frappe/chat/page/chat/chat.js
  35. +20
    -0
      frappe/chat/page/chat/chat.json
  36. +27
    -0
      frappe/chat/page/chat/test_chat.js
  37. +13
    -0
      frappe/chat/util/__init__.py
  38. +35
    -0
      frappe/chat/util/test_util.py
  39. +114
    -0
      frappe/chat/util/util.py
  40. +1
    -0
      frappe/commands/utils.py
  41. +2
    -2
      frappe/core/doctype/page/page.json
  42. +23
    -0
      frappe/core/doctype/page/test_page.js
  43. +63
    -2
      frappe/core/doctype/user/user.json
  44. +3
    -0
      frappe/core/doctype/user/user.py
  45. +0
    -42
      frappe/desk/page/chat/chat.css
  46. +0
    -239
      frappe/desk/page/chat/chat.js
  47. +0
    -23
      frappe/desk/page/chat/chat.json
  48. +0
    -145
      frappe/desk/page/chat/chat.py
  49. +0
    -35
      frappe/desk/page/chat/chat_main.html
  50. +0
    -36
      frappe/desk/page/chat/chat_row.html
  51. +0
    -14
      frappe/desk/page/chat/chat_sidebar.html
  52. +6
    -0
      frappe/hooks.py
  53. +2
    -1
      frappe/modules.txt
  54. +16
    -6
      frappe/public/build.json
  55. +121
    -0
      frappe/public/css/chat.css
  56. +1
    -1
      frappe/public/css/desk-rtl.css
  57. +2551
    -0
      frappe/public/js/frappe/chat.js
  58. +7
    -1
      frappe/public/js/frappe/desk.js
  59. +2
    -2
      frappe/public/js/frappe/form/controls/text_editor.js
  60. +66
    -0
      frappe/public/js/frappe/form/grid.js
  61. +1
    -1
      frappe/public/js/frappe/misc/common.js
  62. +15
    -0
      frappe/public/js/frappe/peer.js
  63. +1
    -0
      frappe/public/js/frappe/request.js
  64. +1
    -1
      frappe/public/js/frappe/socketio_client.js
  65. +181
    -71
      frappe/public/js/frappe/ui/capture.js
  66. +17
    -5
      frappe/public/js/frappe/ui/dialog.js
  67. +12
    -0
      frappe/public/js/frappe/ui/toolbar/navbar.html
  68. +11
    -0
      frappe/public/js/frappe/ui/toolbar/toolbar.js
  69. +1
    -1
      frappe/public/js/frappe/ui/toolbar/user_progress_dialog.js
  70. +1
    -0
      frappe/public/js/lib/fuse.min.js
  71. +1
    -0
      frappe/public/js/lib/hyper.min.js
  72. +0
    -2
      frappe/public/js/lib/webcam.min.js
  73. +225
    -0
      frappe/public/less/chat.less
  74. BIN
      frappe/public/sounds/chat-message.mp3
  75. BIN
      frappe/public/sounds/chat-notification.mp3
  76. +1
    -0
      frappe/templates/includes/login/login.js
  77. +1
    -1
      frappe/test_runner.py
  78. +8
    -0
      frappe/website/js/website.js
  79. +1
    -1
      frappe/www/update-password.html
  80. +94
    -55
      socketio.js

+ 0
- 3
.vscode/settings.json Datei anzeigen

@@ -1,3 +0,0 @@
{
"python.linting.pylintEnabled": false
}

+ 12
- 3
frappe/async.py Datei anzeigen

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

+ 1
- 1
frappe/build.js Datei anzeigen

@@ -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 {


frappe/desk/page/chat/__init__.py → frappe/chat/README.md Datei anzeigen


+ 0
- 0
frappe/chat/__init__.py Datei anzeigen


+ 0
- 0
frappe/chat/doctype/__init__.py Datei anzeigen


+ 0
- 0
frappe/chat/doctype/chat_message/__init__.py Datei anzeigen


+ 8
- 0
frappe/chat/doctype/chat_message/chat_message.js Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_message/chat_message.json Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_message/chat_message.py Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_message/test_chat_message.js Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_message/test_chat_message.py Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_message_attachment/__init__.py Datei anzeigen


+ 71
- 0
frappe/chat/doctype/chat_message_attachment/chat_message_attachment.json Datei anzeigen

@@ -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
}

+ 10
- 0
frappe/chat/doctype/chat_message_attachment/chat_message_attachment.py Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_profile/__init__.py Datei anzeigen


+ 8
- 0
frappe/chat/doctype/chat_profile/chat_profile.js Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_profile/chat_profile.json Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_profile/chat_profile.py Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_profile/chat_profile_list.js Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_profile/test_chat_profile.js Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_profile/test_chat_profile.py Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_room/__init__.py Datei anzeigen


+ 8
- 0
frappe/chat/doctype/chat_room/chat_room.js Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_room/chat_room.json Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_room/chat_room.py Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_room/test_chat_room.js Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_room/test_chat_room.py Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_room_user/__init__.py Datei anzeigen


+ 102
- 0
frappe/chat/doctype/chat_room_user/chat_room_user.json Datei anzeigen

@@ -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
- 0
frappe/chat/doctype/chat_room_user/chat_room_user.py Datei anzeigen

@@ -0,0 +1,8 @@
# imports - module imports
from frappe.model.document import Document
import frappe

session = frappe.session

class ChatRoomUser(Document):
pass

+ 0
- 0
frappe/chat/page/__init__.py Datei anzeigen


+ 0
- 0
frappe/chat/page/chat/__init__.py Datei anzeigen


+ 11
- 0
frappe/chat/page/chat/chat.js Datei anzeigen

@@ -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
- 0
frappe/chat/page/chat/chat.json Datei anzeigen

@@ -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
- 0
frappe/chat/page/chat/test_chat.js Datei anzeigen

@@ -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
- 0
frappe/chat/util/__init__.py Datei anzeigen

@@ -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
- 0
frappe/chat/util/test_util.py Datei anzeigen

@@ -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
- 0
frappe/chat/util/util.py Datei anzeigen

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

+ 1
- 0
frappe/commands/utils.py Datei anzeigen

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


+ 2
- 2
frappe/core/doctype/page/page.json Datei anzeigen

@@ -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
- 0
frappe/core/doctype/page/test_page.js Datei anzeigen

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

});

+ 63
- 2
frappe/core/doctype/user/user.json Datei anzeigen

@@ -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",


+ 3
- 0
frappe/core/doctype/user/user.py Datei anzeigen

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


+ 0
- 42
frappe/desk/page/chat/chat.css Datei anzeigen

@@ -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;
}

+ 0
- 239
frappe/desk/page/chat/chat.js Datei anzeigen

@@ -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));
}



});

+ 0
- 23
frappe/desk/page/chat/chat.json Datei anzeigen

@@ -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"
}

+ 0
- 145
frappe/desk/page/chat/chat.py Datei anzeigen

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

+ 0
- 35
frappe/desk/page/chat/chat_main.html Datei anzeigen

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

+ 0
- 36
frappe/desk/page/chat/chat_row.html Datei anzeigen

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

+ 0
- 14
frappe/desk/page/chat/chat_sidebar.html Datei anzeigen

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

+ 6
- 0
frappe/hooks.py Datei anzeigen

@@ -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 = [


+ 2
- 1
frappe/modules.txt Datei anzeigen

@@ -8,4 +8,5 @@ Desk
Integrations
Printing
Contacts
Data Migration
Data Migration
Chat

+ 16
- 6
frappe/public/build.json Datei anzeigen

@@ -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
- 0
frappe/public/css/chat.css Datei anzeigen

@@ -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;
}

+ 1
- 1
frappe/public/css/desk-rtl.css Datei anzeigen

@@ -45,7 +45,7 @@
.list-id {
margin-left: 7px !important;
}
.avatar-small {
.avatar-small .avatar-sm {
margin-left: 5px;
margin-right: auto;
}


+ 2551
- 0
frappe/public/js/frappe/chat.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 7
- 1
frappe/public/js/frappe/desk.js Datei anzeigen

@@ -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') {


+ 2
- 2
frappe/public/js/frappe/form/controls/text_editor.js Datei anzeigen

@@ -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);
});
}


+ 66
- 0
frappe/public/js/frappe/form/grid.js Datei anzeigen

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


+ 1
- 1
frappe/public/js/frappe/misc/common.js Datei anzeigen

@@ -82,7 +82,7 @@ frappe.get_abbr = function(txt, max_length) {
// continue
return true;
}
87
abbr += w.trim()[0];
});



+ 15
- 0
frappe/public/js/frappe/peer.js Datei anzeigen

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

+ 1
- 0
frappe/public/js/frappe/request.js Datei anzeigen

@@ -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],


+ 1
- 1
frappe/public/js/frappe/socketio_client.js Datei anzeigen

@@ -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 {
}
}
}

}

+ 181
- 71
frappe/public/js/frappe/ui/capture.js Datei anzeigen

@@ -1,94 +1,204 @@
frappe.ui.Capture = class
// 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 =>
{
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>
const $element = $(element)
const width = $element.width()
const height = $element.height()

<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);
const $canvas = $('<canvas/>')
$canvas[0].width = width
$canvas[0].height = height

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();
const context = $canvas[0].getContext('2d')
context.drawImage($element[0], 0, 0, width, height)
const data_uri = $canvas[0].toDataURL('image/png')

Webcam.set(this.options);
}
return data_uri
}

open ( )
/**
* @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.dialog.show();

Webcam.attach('#frappe-capture');
this.options = frappe.ui.Capture.OPTIONS
this.set_options(options)
}

freeze ( )
set_options (options)
{
this.$btnBarSnap.hide();
this.$btnBarKnap.show();
this.options = { ...frappe.ui.Capture.OPTIONS, ...options }
Webcam.freeze();
return this
}

unfreeze ( )
render ( )
{
this.$btnBarSnap.show();
this.$btnBarKnap.hide();
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>&times</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()

Webcam.unfreeze();
}
$e.find('.fc-bcp').click(() =>
{
const data_url = frappe._.get_data_uri(video)
$e.find('.fc-p').attr('src', data_url)

click (callback)
{
$(this.dialog.body).find('#frappe-capture-btn-snap').click(() => {
this.freeze();
$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()

$(this.dialog.body).find('#frappe-capture-btn-discard').click(() => {
this.unfreeze();
});
$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)
})
})
}

$(this.dialog.body).find('#frappe-capture-btn-accept').click(() => {
Webcam.snap((data) => {
callback(data);
});
show ( )
{
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)
}

this.hide();
});
});
throw err
})
}

hide ( )
{
Webcam.reset();
if ( this.dialog )
this.dialog.hide()
}

$(this.dialog.$wrapper).remove();
submit (fn)
{
this.callback = fn
}
};
frappe.ui.Capture.DEFAULT_OPTIONS =
}
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>
`

+ 17
- 5
frappe/public/js/frappe/ui/dialog.js Datei anzeigen

@@ -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;


+ 12
- 0
frappe/public/js/frappe/ui/toolbar/navbar.html Datei anzeigen

@@ -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">


+ 11
- 0
frappe/public/js/frappe/ui/toolbar/toolbar.js Datei anzeigen

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


+ 1
- 1
frappe/public/js/frappe/ui/toolbar/user_progress_dialog.js Datei anzeigen

@@ -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
- 0
frappe/public/js/lib/fuse.min.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
frappe/public/js/lib/hyper.min.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 0
- 2
frappe/public/js/lib/webcam.min.js
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 225
- 0
frappe/public/less/chat.less Datei anzeigen

@@ -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 Datei anzeigen


BIN
frappe/public/sounds/chat-notification.mp3 Datei anzeigen


+ 1
- 0
frappe/templates/includes/login/login.js Datei anzeigen

@@ -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,


+ 1
- 1
frappe/test_runner.py Datei anzeigen

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



+ 8
- 0
frappe/website/js/website.js Datei anzeigen

@@ -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
});

+ 1
- 1
frappe/www/update-password.html Datei anzeigen

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


+ 94
- 55
socketio.js Datei anzeigen

@@ -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;
}


}

Laden…
Abbrechen
Speichern