From 9d199fd2e7c35ca6ea9b392e9b88a2484a45b240 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:24:07 +0800 Subject: [PATCH 01/31] MDL-63303 javascript: fix bug in auto_rows.js allowing it to shrink --- lib/amd/build/auto_rows.min.js | 2 +- lib/amd/src/auto_rows.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/amd/build/auto_rows.min.js b/lib/amd/build/auto_rows.min.js index edc87d988a2..f884f9d7769 100644 --- a/lib/amd/build/auto_rows.min.js +++ b/lib/amd/build/auto_rows.min.js @@ -1 +1 @@ -define(["jquery"],function(a){var b={ELEMENT:"[data-auto-rows]"},c={ROW_CHANGE:"autorows:rowchange"},d=function(a){var b=a.attr("rows"),c=a.data("min-rows"),d=a.attr("data-max-rows"),e=a.height(),f=a.innerHeight(),g=f-e,h=a[0].scrollHeight,i=(h-g)/(e/b);return a.css("height",""),i=d?d:i},e=function(b){var e=a(b.target),f=e.data("min-rows"),g=e.attr("rows");"undefined"==typeof f&&e.data("min-rows",g);var h=d(e);h!=g&&(e.attr("rows",h),e.trigger(c.ROW_CHANGE))},f=function(c){a(c).data("auto-rows")?a(c).on("input propertychange",e.bind(this)):a(c).on("input propertychange",b.ELEMENT,e.bind(this))};return{init:f,events:c}}); \ No newline at end of file +define(["jquery"],function(a){var b={ELEMENT:"[data-auto-rows]"},c={ROW_CHANGE:"autorows:rowchange"},d=function(a){var b=a.attr("rows"),c=a.data("min-rows"),d=a.attr("data-max-rows"),e=a.height(),f=a.innerHeight(),g=f-e,h=a[0].scrollHeight,i=(h-g)/(e/b);return a.css("height",""),i=d?d:i},e=function(b){var e=a(b.target),f=e.data("min-rows"),g=e.attr("rows");"undefined"==typeof f&&e.data("min-rows",g),e.attr("rows",1);var h=d(e);e.attr("rows",h),h!=g&&e.trigger(c.ROW_CHANGE)},f=function(c){a(c).data("auto-rows")?a(c).on("input propertychange",e.bind(this)):a(c).on("input propertychange",b.ELEMENT,e.bind(this))};return{init:f,events:c}}); \ No newline at end of file diff --git a/lib/amd/src/auto_rows.js b/lib/amd/src/auto_rows.js index 644d3a906cb..0814b48c4ee 100644 --- a/lib/amd/src/auto_rows.js +++ b/lib/amd/src/auto_rows.js @@ -80,10 +80,14 @@ define(['jquery'], function($) { if (typeof minRows === "undefined") { element.data('min-rows', currentRows); } + + // Reset element to single row so that the scroll height of the + // element is correctly calculated each time. + element.attr('rows', 1); var rows = calculateRows(element); + element.attr('rows', rows); if (rows != currentRows) { - element.attr('rows', rows); element.trigger(EVENTS.ROW_CHANGE); } }; From c912cd73247efa991add1ce931d82b6c5a46cbdb Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Thu, 25 Oct 2018 10:07:52 +0800 Subject: [PATCH 02/31] MDL-63303 core: add debug info to exceptions --- lib/setuplib.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/setuplib.php b/lib/setuplib.php index 3bd14ae9041..2550a146cf7 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -98,6 +98,8 @@ class moodle_exception extends Exception { * @param string $debuginfo optional debugging information */ function __construct($errorcode, $module='', $link='', $a=NULL, $debuginfo=null) { + global $CFG; + if (empty($module) || $module == 'moodle' || $module == 'core') { $module = 'error'; } @@ -116,11 +118,21 @@ class moodle_exception extends Exception { $haserrorstring = false; } - if (defined('PHPUNIT_TEST') and PHPUNIT_TEST and $debuginfo) { - $message = "$message ($debuginfo)"; + $isinphpunittest = (defined('PHPUNIT_TEST') && PHPUNIT_TEST); + $hasdebugdeveloper = ( + isset($CFG->debugdisplay) && + isset($CFG->debug) && + $CFG->debugdisplay && + $CFG->debug === DEBUG_DEVELOPER + ); + + if ($debuginfo) { + if ($isinphpunittest || $hasdebugdeveloper) { + $message = "$message ($debuginfo)"; + } } - if (!$haserrorstring and defined('PHPUNIT_TEST') and PHPUNIT_TEST) { + if (!$haserrorstring and $isinphpunittest) { // Append the contents of $a to $debuginfo so helpful information isn't lost. // This emulates what {@link get_exception_info()} does. Unfortunately that // function is not used by phpunit. From 456e6d81453f1ca53d02040b3fb0e9d6d6d09685 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:27:50 +0800 Subject: [PATCH 03/31] MDL-63303 core: allow subsystems to add nav bar and top of body content --- lib/outputrenderers.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 75f8e3af5de..d7399fd54fe 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -705,6 +705,14 @@ class core_renderer extends renderer_base { $output .= "\n".$CFG->additionalhtmltopofbody; } + // Give subsystems an opportunity to inject extra html content. The callback + // must always return a string containing valid html. + foreach (\core_component::get_core_subsystems() as $name => $path) { + if ($path) { + $output .= component_callback($name, 'before_standard_top_of_body_html', [], ''); + } + } + // Give plugins an opportunity to inject extra html content. The callback // must always return a string containing valid html. $pluginswithfunction = get_plugins_with_function('before_standard_top_of_body_html', 'lib.php'); @@ -3267,6 +3275,14 @@ EOD; public function navbar_plugin_output() { $output = ''; + // Give subsystems an opportunity to inject extra html content. The callback + // must always return a string containing valid html. + foreach (\core_component::get_core_subsystems() as $name => $path) { + if ($path) { + $output .= component_callback($name, 'render_navbar_output', [$this], ''); + } + } + if ($pluginsfunction = get_plugins_with_function('render_navbar_output')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { From 7b91dcf2642624eee3834a79bf80915e6abbe7af Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:30:22 +0800 Subject: [PATCH 04/31] MDL-63303 core: add new icons for messaging --- lib/classes/output/icon_system_fontawesome.php | 2 ++ pix/i/sendmessage.png | Bin 0 -> 430 bytes pix/i/sendmessage.svg | 6 ++++++ pix/i/trash.png | Bin 0 -> 455 bytes pix/i/trash.svg | 6 ++++++ pix/i/window_close.png | Bin 0 -> 500 bytes pix/i/window_close.svg | 6 ++++++ pix/t/sendmessage.png | Bin 0 -> 430 bytes pix/t/sendmessage.svg | 2 ++ theme/boost/scss/moodle/icons.scss | 1 + theme/boost/style/moodle.css | 12 ++++++++++++ 11 files changed, 35 insertions(+) create mode 100644 pix/i/sendmessage.png create mode 100644 pix/i/sendmessage.svg create mode 100644 pix/i/trash.png create mode 100644 pix/i/trash.svg create mode 100644 pix/i/window_close.png create mode 100644 pix/i/window_close.svg create mode 100644 pix/t/sendmessage.png create mode 100644 pix/t/sendmessage.svg diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php index b684751c384..e5e8ec76d31 100644 --- a/lib/classes/output/icon_system_fontawesome.php +++ b/lib/classes/output/icon_system_fontawesome.php @@ -294,6 +294,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:i/scheduled' => 'fa-calendar-check-o', 'core:i/search' => 'fa-search', 'core:i/section' => 'fa-folder-o', + 'core:i/sendmessage' => 'fa-paper-plane', 'core:i/settings' => 'fa-cog', 'core:i/show' => 'fa-eye-slash', 'core:i/siteevent' => 'fa-globe', @@ -332,6 +333,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:t/collapsed_empty' => 'fa-plus-square-o', 'core:t/collapsed_rtl' => 'fa-plus-square', 'core:t/collapsed' => 'fa-plus-square', + 'core:t/collapsedcaret' => 'fa-caret-right', 'core:t/contextmenu' => 'fa-cog', 'core:t/copy' => 'fa-copy', 'core:t/delete' => 'fa-trash', diff --git a/pix/i/sendmessage.png b/pix/i/sendmessage.png new file mode 100644 index 0000000000000000000000000000000000000000..dcf25dc493781abd79d0d60765fd69cbec62908a GIT binary patch literal 430 zcmV;f0a5;mP);Z28?jK(r1CD`dp!$`*?I=(v6jHHRY!}!B9N+>N2T;{Y zTLEirOhh(RbstCo3LFAyAPHcMsQ|Rh!1w)GpscD#U*vZYSrw51U^wgxg5c*DipAoP z>$-VWUHGQ=0xYTO9I*T?|52~kA0h|V+Pvd9H%;U_W6T852W&SPeaPis&^PIvB@QZQ;YwZ;< z^IJ_-SF+jcS>&J%Lh?0GZs(xWDL*wDjd{=WMvXC(z$x&YN~P{Q@W5L8===Ui$Hs2) Y30BE=lpMPYDgXcg07*qoM6N<$g1mpWSO5S3 literal 0 HcmV?d00001 diff --git a/pix/i/sendmessage.svg b/pix/i/sendmessage.svg new file mode 100644 index 00000000000..48c19f159a2 --- /dev/null +++ b/pix/i/sendmessage.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/pix/i/trash.png b/pix/i/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..dd54a6efd43f0d9349f721a9081fec50a03def46 GIT binary patch literal 455 zcmV;&0XY7NP)IhEW}O=Q$$PALaclP?eYYoflSm=OCP~P zQtfPE9>Kz(jrJniXd#F{APP2uAXu1WugxUHHJjB03&8`!&fGKioO@?s27VdxolU;) z&w8FW+#UPsC z6c~)ea&Z;90><6^4S4F_=1>ruTgN91|lRkw-~aG7XOtJSuF#~#G*!!RsIgB_pDa=AQ| xPN!#5sZ`QG0-(`o)H9jPS)ovP?V+&#@Bv#wYA0tKO`-q*002ovPDHLkV1h#k$!Y)q literal 0 HcmV?d00001 diff --git a/pix/i/trash.svg b/pix/i/trash.svg new file mode 100644 index 00000000000..1d36252150a --- /dev/null +++ b/pix/i/trash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/pix/i/window_close.png b/pix/i/window_close.png new file mode 100644 index 0000000000000000000000000000000000000000..a57632ea2011d411394c017320d6b130e126d953 GIT binary patch literal 500 zcmV{40jDB{A6 zyOb=Og*(Ith^^cq5kg9lDiIVu!7`Hq*20LzEP}jl_4I#F^UvE|II;v_Fc`d2)#rx< ztLpDotMyBgB)I@S=V1qwiO5FUpQr`3unP zb|>!MEgc{t*J+wo^XDK4rm9+3)p`&F57&~WX;nn73pq)Wc##QADwWE+X0y5R3gS3^ zrmDBV>(Ub}b(Uq>?PxSQ-3Pu)003E*6-~Kq`kewMKs{gRs-h^GnOTF!?J5=Us4cKq z=<4Fv+Ep&V-Fxe8F|#`}yIXIUyZ1^5h{%^Xj^Dsyp=S0Cr~x%I`<^ep-|wG^NLTl2>5-V>1osBSn5KxGRYpeiC?3%uGE??M0NYkGe~{=JIG6+65zd&orO qN&tq#;Y%}naY%ap(?2A0qy7QpC#_6jSqnM<0000 + + + + + diff --git a/pix/t/sendmessage.png b/pix/t/sendmessage.png new file mode 100644 index 0000000000000000000000000000000000000000..dcf25dc493781abd79d0d60765fd69cbec62908a GIT binary patch literal 430 zcmV;f0a5;mP);Z28?jK(r1CD`dp!$`*?I=(v6jHHRY!}!B9N+>N2T;{Y zTLEirOhh(RbstCo3LFAyAPHcMsQ|Rh!1w)GpscD#U*vZYSrw51U^wgxg5c*DipAoP z>$-VWUHGQ=0xYTO9I*T?|52~kA0h|V+Pvd9H%;U_W6T852W&SPeaPis&^PIvB@QZQ;YwZ;< z^IJ_-SF+jcS>&J%Lh?0GZs(xWDL*wDjd{=WMvXC(z$x&YN~P{Q@W5L8===Ui$Hs2) Y30BE=lpMPYDgXcg07*qoM6N<$g1mpWSO5S3 literal 0 HcmV?d00001 diff --git a/pix/t/sendmessage.svg b/pix/t/sendmessage.svg new file mode 100644 index 00000000000..9755f874b23 --- /dev/null +++ b/pix/t/sendmessage.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/theme/boost/scss/moodle/icons.scss b/theme/boost/scss/moodle/icons.scss index 7defdf20652..a5a2748d646 100644 --- a/theme/boost/scss/moodle/icons.scss +++ b/theme/boost/scss/moodle/icons.scss @@ -90,6 +90,7 @@ $iconsizes: map-merge(( .icon { height: $length !important; /* stylelint-disable-line declaration-no-important */ width: $length !important; /* stylelint-disable-line declaration-no-important */ + font-size: $length !important; /* stylelint-disable-line declaration-no-important */ } } } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 44c7096408a..68dc4d76694 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -10518,36 +10518,48 @@ div.editor_atto_toolbar button .icon { height: 0 !important; /* stylelint-disable-line declaration-no-important */ width: 0 !important; + /* stylelint-disable-line declaration-no-important */ + font-size: 0 !important; /* stylelint-disable-line declaration-no-important */ } .icon-size-1 .icon { height: 4px !important; /* stylelint-disable-line declaration-no-important */ width: 4px !important; + /* stylelint-disable-line declaration-no-important */ + font-size: 4px !important; /* stylelint-disable-line declaration-no-important */ } .icon-size-2 .icon { height: 8px !important; /* stylelint-disable-line declaration-no-important */ width: 8px !important; + /* stylelint-disable-line declaration-no-important */ + font-size: 8px !important; /* stylelint-disable-line declaration-no-important */ } .icon-size-3 .icon { height: 16px !important; /* stylelint-disable-line declaration-no-important */ width: 16px !important; + /* stylelint-disable-line declaration-no-important */ + font-size: 16px !important; /* stylelint-disable-line declaration-no-important */ } .icon-size-4 .icon { height: 24px !important; /* stylelint-disable-line declaration-no-important */ width: 24px !important; + /* stylelint-disable-line declaration-no-important */ + font-size: 24px !important; /* stylelint-disable-line declaration-no-important */ } .icon-size-5 .icon { height: 48px !important; /* stylelint-disable-line declaration-no-important */ width: 48px !important; + /* stylelint-disable-line declaration-no-important */ + font-size: 48px !important; /* stylelint-disable-line declaration-no-important */ } .helplink .icon { From a69193fa7092565ea66c7dbec0040d8c884cbbee Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:31:27 +0800 Subject: [PATCH 05/31] MDL-63303 theme_boost: add switch styling for checkbox --- theme/boost/scss/moodle/core.scss | 96 +++++++++++++++++++++++++++++++ theme/boost/style/moodle.css | 60 +++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 7fc00a8717e..02208db7c63 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2149,3 +2149,99 @@ div.editor_atto_toolbar button .icon { display: none; } } + +$switch-height: ($input-height-inner * .8) !default; +$switch-height-half: ($switch-height / 2) !default; +$switch-border-radius: $switch-height !default; +$switch-bg: $custom-control-indicator-bg !default; +$switch-checked-bg: map-get($theme-colors, 'primary') !default; +$switch-disabled-bg: $custom-control-indicator-disabled-bg !default; +$switch-disabled-color: $custom-control-label-disabled-color !default; +$switch-thumb-bg: $white !default; +$switch-thumb-border-radius: 50% !default; +$switch-thumb-padding: 2px !default; +$switch-thumb-padding-double: ($switch-thumb-padding * 2) !default; +$switch-focus-box-shadow: 0 0 0 $input-btn-focus-width rgba(map-get($theme-colors, 'primary'), .25); +$switch-transition: .2s all !default; + +.switch { + position: relative; + + input { + position: absolute; + height: 1px; + width: 1px; + background: none; + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + overflow: hidden; + padding: 0; + + + label { + position: relative; + min-height: $switch-height; + min-width: $switch-height * 2; + line-height: $switch-height; + border-radius: $switch-border-radius; + display: inline-block; + cursor: pointer; + outline: none; + user-select: none; + vertical-align: middle; + padding-left: (($switch-height * 2) + .5rem); + } + + + label::before, + + label::after { + content: ''; + position: absolute; + top: calc(50% - #{$switch-height-half}); + left: 0; + width: ($switch-height * 2); + height: $switch-height; + bottom: 0; + display: block; + } + + + label::before { + right: 0; + background-color: $switch-bg; + border-radius: $switch-border-radius; + transition: $switch-transition; + } + + + label::after { + margin-top: $switch-thumb-padding; + left: $switch-thumb-padding; + width: calc(#{$switch-height} - #{$switch-thumb-padding-double}); + height: calc(#{$switch-height} - #{$switch-thumb-padding-double}); + border-radius: $switch-thumb-border-radius; + background-color: $switch-thumb-bg; + transition: $switch-transition; + } + + &:checked + label::before { + background-color: $switch-checked-bg; + } + + &:checked + label::after { + margin-left: $switch-height; + } + + &:focus + label::before { + outline: none; + box-shadow: $switch-focus-box-shadow; + } + + &:disabled + label { + color: $switch-disabled-color; + cursor: not-allowed; + } + + &:disabled + label::before { + background-color: $switch-disabled-bg; + } + } +} + diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 68dc4d76694..b45f320579e 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -10468,6 +10468,66 @@ div.editor_atto_toolbar button .icon { .dir-ltr .dir-ltr-hide { display: none; } +.switch { + position: relative; } + .switch input { + position: absolute; + height: 1px; + width: 1px; + background: none; + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + overflow: hidden; + padding: 0; } + .switch input + label { + position: relative; + min-height: 1.725rem; + min-width: 3.45rem; + line-height: 1.725rem; + border-radius: 1.725rem; + display: inline-block; + cursor: pointer; + outline: none; + user-select: none; + vertical-align: middle; + padding-left: 3.95rem; } + .switch input + label::before, + .switch input + label::after { + content: ''; + position: absolute; + top: calc(50% - 0.8625rem); + left: 0; + width: 3.45rem; + height: 1.725rem; + bottom: 0; + display: block; } + .switch input + label::before { + right: 0; + background-color: #dee2e6; + border-radius: 1.725rem; + transition: 0.2s all; } + .switch input + label::after { + margin-top: 2px; + left: 2px; + width: calc(1.725rem - 4px); + height: calc(1.725rem - 4px); + border-radius: 50%; + background-color: #fff; + transition: 0.2s all; } + .switch input:checked + label::before { + background-color: #1177d1; } + .switch input:checked + label::after { + margin-left: 1.725rem; } + .switch input:focus + label::before { + outline: none; + box-shadow: 0 0 0 0.2rem rgba(17, 119, 209, 0.25); } + .switch input:disabled + label { + color: #868e96; + cursor: not-allowed; } + .switch input:disabled + label::before { + background-color: #e9ecef; } + .icon { font-size: 16px; width: 16px; From c2fc2c25ad05af26bfb6a85d11346138099b2e76 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:32:12 +0800 Subject: [PATCH 06/31] MDL-63303 theme_bootstrapbase: add bs4 compat classes --- .../bootstrapbase/less/moodle/bs4-compat.less | 284 ++++++++++++++++-- theme/bootstrapbase/style/moodle.css | 270 ++++++++++++++++- 2 files changed, 521 insertions(+), 33 deletions(-) diff --git a/theme/bootstrapbase/less/moodle/bs4-compat.less b/theme/bootstrapbase/less/moodle/bs4-compat.less index 937b4349df1..fb8802ab672 100644 --- a/theme/bootstrapbase/less/moodle/bs4-compat.less +++ b/theme/bootstrapbase/less/moodle/bs4-compat.less @@ -176,6 +176,170 @@ margin: 3 * @baseFontSize !important; } + +// Bootstrap 4 stable utilities +.p-1 { + padding: @baseFontSize / 4 !important; +} +.p-2 { + padding: @baseFontSize / 2 !important; +} +.p-3 { + padding: @baseFontSize !important; +} +.p-4 { + padding: @baseFontSize * 1.5 !important; +} + +.pl-1, +.px-1 { + padding-left: @baseFontSize / 4 !important; +} +.pl-2, +.px-2 { + padding-left: @baseFontSize / 2 !important; +} +.pl-3, +.px-3 { + padding-left: @baseFontSize !important; +} +.pl-4, +.px-4 { + padding-left: @baseFontSize * 1.5 !important; +} +.pr-1, +.px-1 { + padding-right: @baseFontSize / 4 !important; +} +.pr-2, +.px-2 { + padding-right: @baseFontSize / 2 !important; +} +.pr-3, +.px-3 { + padding-right: @baseFontSize !important; +} +.pr-4, +.px-4 { + padding-right: @baseFontSize * 1.5 !important; +} +.pt-1, +.py-1 { + padding-top: @baseFontSize / 4 !important; +} +.pt-2, +.py-2 { + padding-top: @baseFontSize / 2 !important; +} +.pt-3, +.py-3 { + padding-top: @baseFontSize !important; +} +.pt-4, +.py-4 { + padding-top: @baseFontSize * 1.5 !important; +} +.pb-1, +.py-1 { + padding-bottom: @baseFontSize / 4 !important; +} +.pb-2, +.py-2 { + padding-bottom: @baseFontSize / 2 !important; +} +.pb-3, +.py-3 { + padding-bottom: @baseFontSize !important; +} +.pb-4, +.py-4 { + padding-bottom: @baseFontSize * 1.5 !important; +} + + +.ml-1, +.mx-1 { + margin-left: @baseFontSize / 4 !important; +} +.ml-2, +.mx-2 { + margin-left: @baseFontSize / 2 !important; +} +.ml-3, +.mx-3 { + margin-left: @baseFontSize !important; +} +.ml-4, +.mx-4 { + margin-left: @baseFontSize * 1.5 !important; +} +.mr-1, +.mx-1 { + margin-right: @baseFontSize / 4 !important; +} +.mr-2, +.mx-2 { + margin-right: @baseFontSize / 2 !important; +} +.mr-3, +.mx-3 { + margin-right: @baseFontSize !important; +} +.mr-4, +.mx-4 { + margin-right: @baseFontSize * 1.5 !important; +} +.mb-1, +.my-1 { + margin-bottom: @baseFontSize / 4 !important; +} +.mb-2, +.my-2 { + margin-bottom: @baseFontSize / 2 !important; +} +.mb-3, +.my-3 { + margin-bottom: @baseFontSize !important; +} +.mb-4, +.my-4 { + margin-bottom: @baseFontSize * 1.5 !important; +} +.mt-1, +.my-1 { + margin-top: @baseFontSize / 4 !important; +} +.mt-2, +.my-2 { + margin-top: @baseFontSize / 2 !important; +} +.mt-3, +.my-3 { + margin-top: @baseFontSize !important; +} +.mt-4, +.my-4 { + margin-top: @baseFontSize * 1.5 !important; +} + + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} +.mr-auto, +.mx-auto { + margin-right: auto !important; +} +.mt-auto, +.my-auto { + margin-top: auto !important; +} +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + .d-inline { display: inline !important; } @@ -303,10 +467,46 @@ &-primary[href] { background-color: darken(@blue, 10%); } + &-faded, + &-light { + background-color: #f8f9fa !important; + } + &-white { + background-color: @white !important; + } + &-secondary { + background-color: #e9ecef !important; + } } -.bg-faded { - background-color: @grayLighter; +.border-left { + border-left: 1px solid #dee2e6 !important; +} +.border-right { + border-right: 1px solid #dee2e6 !important; +} +.border-top { + border-top: 1px solid #dee2e6 !important; +} +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} + + +.border-0 { + border: 0 !important; +} +.border-top-0 { + border-top: 0 !important; +} +.border-right-0 { + border-right: 0 !important; +} +.border-bottom-0 { + border-bottom: 0 !important; +} +.border-left-0 { + border-left: 0 !important; } .w-100 { @@ -332,11 +532,6 @@ .flex-column { flex-direction: column !important; } - -.align-self-stretch { - align-self: stretch; -} - .align-items-start { align-items: flex-start !important; } @@ -344,29 +539,80 @@ .align-items-center { align-items: center !important; } - -.ml-auto { - margin-left: auto; +.h-100 { + height: 100% !important; +} +.position-absolute { + position: absolute !important; +} +.position-relative { + position: relative !important; +} +.align-self-center { + align-self: center !important; +} +.align-items-center { + align-items: center !important; +} +.align-self-stretch { + align-self: stretch !important; +} +.justify-content-around { + justify-content: space-around !important; +} +.flex-grow { + flex-grow: 1; +} +.flex-shrink { + flex-shrink: 1; } -.mr-auto { - margin-right: auto; +.rounded-circle { + .border-radius(50%); +} +.rounded { + .border-radius(4px); +} +.small { + font-size: 75%; } -.mt-auto { - margin-top: auto; +.box-initial { + box-sizing: initial; + & * { + box-sizing: initial; + } } - -.mr-2 { - margin-right: @baseFontSize / 2 !important; +.box-border { + box-sizing: border-box; + & * { + box-sizing: border-box; + } +} +.list-group-item { + background-color: @white; +} +.list-group-item-action { + &:hover, + &:focus { + background-color: #f8f9fa; + text-decoration: none; + } } - .icon-size-3 { height: 36px !important; width: 36px !important; img { height: 16px !important; width: 16px !important; - padding-right: 0; } } +.icon-size-4 { + height: 44px !important; + width: 44px !important; + img { + height: 24px !important; + width: 24px !important; + } +} + diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index 868a53c3c72..64d8d3c99f1 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -21805,6 +21805,162 @@ ul.indented-list { .m-a-3 { margin: 42px !important; } +.p-1 { + padding: 3.5px !important; +} +.p-2 { + padding: 7px !important; +} +.p-3 { + padding: 14px !important; +} +.p-4 { + padding: 21px !important; +} +.pl-1, +.px-1 { + padding-left: 3.5px !important; +} +.pl-2, +.px-2 { + padding-left: 7px !important; +} +.pl-3, +.px-3 { + padding-left: 14px !important; +} +.pl-4, +.px-4 { + padding-left: 21px !important; +} +.pr-1, +.px-1 { + padding-right: 3.5px !important; +} +.pr-2, +.px-2 { + padding-right: 7px !important; +} +.pr-3, +.px-3 { + padding-right: 14px !important; +} +.pr-4, +.px-4 { + padding-right: 21px !important; +} +.pt-1, +.py-1 { + padding-top: 3.5px !important; +} +.pt-2, +.py-2 { + padding-top: 7px !important; +} +.pt-3, +.py-3 { + padding-top: 14px !important; +} +.pt-4, +.py-4 { + padding-top: 21px !important; +} +.pb-1, +.py-1 { + padding-bottom: 3.5px !important; +} +.pb-2, +.py-2 { + padding-bottom: 7px !important; +} +.pb-3, +.py-3 { + padding-bottom: 14px !important; +} +.pb-4, +.py-4 { + padding-bottom: 21px !important; +} +.ml-1, +.mx-1 { + margin-left: 3.5px !important; +} +.ml-2, +.mx-2 { + margin-left: 7px !important; +} +.ml-3, +.mx-3 { + margin-left: 14px !important; +} +.ml-4, +.mx-4 { + margin-left: 21px !important; +} +.mr-1, +.mx-1 { + margin-right: 3.5px !important; +} +.mr-2, +.mx-2 { + margin-right: 7px !important; +} +.mr-3, +.mx-3 { + margin-right: 14px !important; +} +.mr-4, +.mx-4 { + margin-right: 21px !important; +} +.mb-1, +.my-1 { + margin-bottom: 3.5px !important; +} +.mb-2, +.my-2 { + margin-bottom: 7px !important; +} +.mb-3, +.my-3 { + margin-bottom: 14px !important; +} +.mb-4, +.my-4 { + margin-bottom: 21px !important; +} +.mt-1, +.my-1 { + margin-top: 3.5px !important; +} +.mt-2, +.my-2 { + margin-top: 7px !important; +} +.mt-3, +.my-3 { + margin-top: 14px !important; +} +.mt-4, +.my-4 { + margin-top: 21px !important; +} +.ml-auto, +.mx-auto { + margin-left: auto !important; +} +.mr-auto, +.mx-auto { + margin-right: auto !important; +} +.mt-auto, +.my-auto { + margin-top: auto !important; +} +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} .d-inline { display: inline !important; } @@ -21894,8 +22050,42 @@ ul.indented-list { .bg-primary[href] { background-color: #0378a9; } -.bg-faded { - background-color: #eee; +.bg-faded, +.bg-light { + background-color: #f8f9fa !important; +} +.bg-white { + background-color: #fff !important; +} +.bg-secondary { + background-color: #e9ecef !important; +} +.border-left { + border-left: 1px solid #dee2e6 !important; +} +.border-right { + border-right: 1px solid #dee2e6 !important; +} +.border-top { + border-top: 1px solid #dee2e6 !important; +} +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} +.border-0 { + border: 0 !important; +} +.border-top-0 { + border-top: 0 !important; +} +.border-right-0 { + border-right: 0 !important; +} +.border-bottom-0 { + border-bottom: 0 !important; +} +.border-left-0 { + border-left: 0 !important; } .w-100 { width: 100%; @@ -21915,26 +22105,71 @@ ul.indented-list { .flex-column { flex-direction: column !important; } -.align-self-stretch { - align-self: stretch; -} .align-items-start { align-items: flex-start !important; } .align-items-center { align-items: center !important; } -.ml-auto { - margin-left: auto; +.h-100 { + height: 100% !important; } -.mr-auto { - margin-right: auto; +.position-absolute { + position: absolute !important; } -.mt-auto { - margin-top: auto; +.position-relative { + position: relative !important; } -.mr-2 { - margin-right: 7px !important; +.align-self-center { + align-self: center !important; +} +.align-items-center { + align-items: center !important; +} +.align-self-stretch { + align-self: stretch !important; +} +.justify-content-around { + justify-content: space-around !important; +} +.flex-grow { + flex-grow: 1; +} +.flex-shrink { + flex-shrink: 1; +} +.rounded-circle { + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; +} +.rounded { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.small { + font-size: 75%; +} +.box-initial { + box-sizing: initial; +} +.box-initial * { + box-sizing: initial; +} +.box-border { + box-sizing: border-box; +} +.box-border * { + box-sizing: border-box; +} +.list-group-item { + background-color: #fff; +} +.list-group-item-action:hover, +.list-group-item-action:focus { + background-color: #f8f9fa; + text-decoration: none; } .icon-size-3 { height: 36px !important; @@ -21943,5 +22178,12 @@ ul.indented-list { .icon-size-3 img { height: 16px !important; width: 16px !important; - padding-right: 0; +} +.icon-size-4 { + height: 44px !important; + width: 44px !important; +} +.icon-size-4 img { + height: 24px !important; + width: 24px !important; } From 9e189a914d950bc1b8f08bdf30407eb7be681006 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 29 Oct 2018 14:57:48 +0800 Subject: [PATCH 07/31] MDL-63303 core_favourites: add get and count functions --- .../local/service/user_favourite_service.php | 45 ++++++++++ favourites/tests/service_test.php | 85 ++++++++++++++++++- lib/db/services.php | 2 + message/classes/api.php | 7 +- message/tests/api_test.php | 8 +- 5 files changed, 138 insertions(+), 9 deletions(-) diff --git a/favourites/classes/local/service/user_favourite_service.php b/favourites/classes/local/service/user_favourite_service.php index 16718b82f00..a63692a675c 100644 --- a/favourites/classes/local/service/user_favourite_service.php +++ b/favourites/classes/local/service/user_favourite_service.php @@ -156,4 +156,49 @@ class user_favourite_service { ] ); } + + /** + * Get the favourite. + * + * @param string $component the frankenstyle component name. + * @param string $itemtype the type of the favourited item. + * @param int $itemid the id of the item which was favourited (not the favourite's id). + * @param \context $context the context of the item which was favourited. + * @return favourite|null + */ + public function get_favourite(string $component, string $itemtype, int $itemid, \context $context) { + try { + return $this->repo->find_favourite( + $this->userid, + $component, + $itemtype, + $itemid, + $context->id + ); + } catch (\dml_missing_record_exception $e) { + return null; + } + } + + /** + * Count the favourite by item type. + * + * @param string $component the frankenstyle component name. + * @param string $itemtype the type of the favourited item. + * @param \context|null $context the context of the item which was favourited. + * @return favourite|null + */ + public function count_favourites_by_type(string $component, string $itemtype, \context $context = null) { + $criteria = [ + 'userid' => $this->userid, + 'component' => $component, + 'itemtype' => $itemtype + ]; + + if ($context) { + $criteria['contextid'] = $context->id; + } + + return $this->repo->count_by($criteria); + } } diff --git a/favourites/tests/service_test.php b/favourites/tests/service_test.php index 939e5446629..5ead879fff7 100644 --- a/favourites/tests/service_test.php +++ b/favourites/tests/service_test.php @@ -111,7 +111,7 @@ class user_favourite_service_testcase extends advanced_testcase { return $fakerow; } } - throw new \moodle_exception("Item not found"); + throw new \dml_missing_record_exception("Item not found"); }) ); $mockrepo->expects($this->any()) @@ -127,16 +127,17 @@ class user_favourite_service_testcase extends advanced_testcase { }) ); $mockrepo->expects($this->any()) - ->method('exists_by') + ->method('count_by') ->will($this->returnCallback(function(array $criteria) use (&$mockstore) { + $count = 0; // Check the mockstore for all objects with properties matching the key => val pairs in $criteria. foreach ($mockstore as $index => $mockrow) { $mockrowarr = (array)$mockrow; if (array_diff($criteria, $mockrowarr) == []) { - return true; + $count++; } } - return false; + return $count; }) ); $mockrepo->expects($this->any()) @@ -149,6 +150,19 @@ class user_favourite_service_testcase extends advanced_testcase { } }) ); + $mockrepo->expects($this->any()) + ->method('exists_by') + ->will($this->returnCallback(function(array $criteria) use (&$mockstore) { + // Check the mockstore for all objects with properties matching the key => val pairs in $criteria. + foreach ($mockstore as $index => $mockrow) { + $mockrowarr = (array)$mockrow; + if (array_diff($criteria, $mockrowarr) == []) { + return true; + } + } + return false; + }) + ); return $mockrepo; } @@ -352,4 +366,67 @@ class user_favourite_service_testcase extends advanced_testcase { ) ); } + + /** + * Test confirming the behaviour of the get_favourite() method. + */ + public function test_get_favourite() { + list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses(); + + // Get a user_favourite_service for the user. + $repo = $this->get_mock_repository([]); + $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo); + + // Favourite a course. + $fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context); + + $result = $service->get_favourite( + 'core_course', + 'course', + $course1context->instanceid, + $course1context + ); + // Verify we can get the favourite. + $this->assertEquals($fav1->id, $result->id); + + // And one that we know doesn't exist. + $this->assertNull( + $service->get_favourite( + 'core_course', + 'someothertype', + $course1context->instanceid, + $course1context + ) + ); + } + + /** + * Test confirming the behaviour of the count_favourites_by_type() method. + */ + public function test_count_favourites_by_type() { + list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses(); + + // Get a user_favourite_service for the user. + $repo = $this->get_mock_repository([]); + $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo); + + $this->assertEquals(0, $service->count_favourites_by_type('core_course', 'course', $course1context)); + // Favourite a course. + $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context); + + $this->assertEquals(1, $service->count_favourites_by_type('core_course', 'course', $course1context)); + + // Favourite another course. + $service->create_favourite('core_course', 'course', $course2context->instanceid, $course1context); + + $this->assertEquals(2, $service->count_favourites_by_type('core_course', 'course', $course1context)); + + // Favourite a course in another context. + $service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context); + + // Doesn't affect original context. + $this->assertEquals(2, $service->count_favourites_by_type('core_course', 'course', $course1context)); + // Gets counted if we include all contexts. + $this->assertEquals(3, $service->count_favourites_by_type('core_course', 'course')); + } } diff --git a/lib/db/services.php b/lib/db/services.php index 1f4483dc6dc..5b96bb570fa 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1257,6 +1257,7 @@ $functions = array( 'classpath' => 'message/externallib.php', 'description' => 'Mark a conversation or group of conversations as favourites/starred conversations.', 'type' => 'write', + 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'core_message_unset_favourite_conversations' => array( @@ -1265,6 +1266,7 @@ $functions = array( 'classpath' => 'message/externallib.php', 'description' => 'Unset a conversation or group of conversations as favourites/starred conversations.', 'type' => 'write', + 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'core_notes_create_notes' => array( diff --git a/message/classes/api.php b/message/classes/api.php index abc917c22f5..b497ebe4d89 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -813,8 +813,13 @@ class api { if (!self::is_user_in_conversation($userid, $conversationid)) { throw new \moodle_exception("Conversation doesn't exist or user is not a member"); } + $systemcontext = \context_system::instance(); $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); - return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, \context_system::instance()); + if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $systemcontext)) { + return $favourite; + } else { + return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $systemcontext); + } } /** diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 78dfbfd2f53..30add021e0c 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -950,7 +950,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase { // Favourite the first conversation as user 1. $conversationid1 = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); - \core_message\api::set_favourite_conversation($conversationid1, $user1->id); + $favourite = \core_message\api::set_favourite_conversation($conversationid1, $user1->id); // Verify we have a single favourite conversation a user 1. $this->assertCount(1, \core_message\api::get_conversations($user1->id, 0, 20, null, true)); @@ -958,9 +958,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase { // Verify we have no favourites as user2, despite being a member in that conversation. $this->assertCount(0, \core_message\api::get_conversations($user2->id, 0, 20, null, true)); - // Try to favourite the same conversation again. - $this->expectException(\moodle_exception::class); - \core_message\api::set_favourite_conversation($conversationid1, $user1->id); + // Try to favourite the same conversation again should just return the existing favourite. + $repeatresult = \core_message\api::set_favourite_conversation($conversationid1, $user1->id); + $this->assertEquals($favourite->id, $repeatresult->id); } /** From 32b4212e5019e0c7a555d0feca036b572aa8af1e Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 5 Nov 2018 14:13:54 +0800 Subject: [PATCH 08/31] MDL-63303 message: fix get_conversation_messages --- message/classes/api.php | 8 ++------ message/classes/helper.php | 11 +++++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/message/classes/api.php b/message/classes/api.php index b497ebe4d89..ddf2f4b773d 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -1036,12 +1036,8 @@ class api { } } - $arrmessages = array(); - if ($messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto)) { - $arrmessages = helper::format_conversation_messages($userid, $convid, $messages); - } - - return $arrmessages; + $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto); + return helper::format_conversation_messages($userid, $convid, $messages); } /** diff --git a/message/classes/helper.php b/message/classes/helper.php index b83cd8ede3f..74b0059dfcc 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -218,10 +218,13 @@ class helper { $memberids = array_unique(array_map(function($message) { return $message->useridfrom; }, $messages)); - // Get members information. - $arrmembers = self::get_member_info($userid, $memberids); - // Add the members to the conversation. - $conversation['members'] = $arrmembers; + + if (!empty($memberids)) { + // Get members information. + $conversation['members'] = self::get_member_info($userid, $memberids); + } else { + $conversation['members'] = array(); + } return $conversation; } From c61353ae417ca5fe1957de4fc7813b98e958d176 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 6 Nov 2018 14:03:11 +0800 Subject: [PATCH 09/31] MDL-63303 message: fix mark_all_conversation_messages_as_read ext func --- message/externallib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message/externallib.php b/message/externallib.php index 0c9c0559007..65ccda6c316 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -3196,7 +3196,7 @@ class core_message_external extends external_api { * @since 3.6 */ public static function mark_all_conversation_messages_as_read_returns() { - return new external_warnings(); + return null; } /** From 38004e777f3cdfe2ceb81d9c8f70cb40e26f085d Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 6 Nov 2018 14:14:29 +0800 Subject: [PATCH 10/31] MDL-63303 message: fix get_member_info iscontact check --- message/classes/helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/message/classes/helper.php b/message/classes/helper.php index 74b0059dfcc..38b4e554030 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -507,11 +507,11 @@ class helper { $userssql = "SELECT $userfields, u.deleted, mc.id AS contactid, mub.id AS blockedid FROM {user} u LEFT JOIN {message_contacts} mc - ON (mc.userid = ? AND mc.contactid = u.id) + ON ((mc.userid = ? AND mc.contactid = u.id) OR (mc.userid = u.id AND mc.contactid = ?)) LEFT JOIN {message_users_blocked} mub ON (mub.userid = ? AND mub.blockeduserid = u.id) WHERE u.id $useridsql"; - $usersparams = array_merge([$referenceuserid, $referenceuserid], $usersparams); + $usersparams = array_merge([$referenceuserid, $referenceuserid, $referenceuserid], $usersparams); $otherusers = $DB->get_records_sql($userssql, $usersparams); $members = []; From 8350978aa150a16fe95b2f5ef569c012244a4b3a Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 5 Nov 2018 15:30:59 +0800 Subject: [PATCH 11/31] MDL-63303 message: add get_member_info external function --- lib/db/services.php | 9 +++++ message/externallib.php | 74 +++++++++++++++++++++++++++++++++++++++++ version.php | 2 +- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/lib/db/services.php b/lib/db/services.php index 5b96bb570fa..6aecd22850e 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1105,6 +1105,15 @@ $functions = array( 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), 'ajax' => true, ), + 'core_message_get_member_info' => array( + 'classname' => 'core_message_external', + 'methodname' => 'get_member_info', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve a user message profiles', + 'type' => 'read', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + 'ajax' => true, + ), 'core_message_get_unread_conversations_count' => array( 'classname' => 'core_message_external', 'methodname' => 'get_unread_conversations_count', diff --git a/message/externallib.php b/message/externallib.php index 65ccda6c316..b8a1dc16fb2 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -3953,4 +3953,78 @@ class core_message_external extends external_api { public static function unset_favourite_conversations_returns() { return new external_warnings(); } + + /** + * Returns description of method parameters for get_member_info() method. + * + * @return external_function_parameters + */ + public static function get_member_info_parameters() { + return new external_function_parameters( + array( + 'referenceuserid' => new external_value(PARAM_INT, 'id of the user'), + 'userids' => new external_multiple_structure( + new external_value(PARAM_INT, 'id of members to get') + ), + 'includecontactrequests' => new external_value(PARAM_BOOL, 'include contact requests in response', VALUE_DEFAULT, false), + 'includeprivacyinfo' => new external_value(PARAM_BOOL, 'include privacy info in response', VALUE_DEFAULT, false) + ) + ); + } + + /** + * Returns conversation member info for the supplied users, relative to the supplied referenceuserid. + * + * This is the basic structure used when returning members, and includes information about the relationship between each member + * and the referenceuser, such as a whether the referenceuser has marked the member as a contact, or has blocked them. + * + * @param int $referenceuserid the id of the user which check contact and blocked status. + * @param array $userids + * @return array the array of objects containing member info. + * @throws moodle_exception if messaging is disabled or if the user cannot perform the action. + */ + public static function get_member_info( + int $referenceuserid, + array $userids, + bool $includecontactrequests = false, + bool $includeprivacyinfo = false + ) { + global $CFG, $USER; + + // All the business logic checks that really shouldn't be in here. + if (empty($CFG->messaging)) { + throw new moodle_exception('disabled', 'message'); + } + $params = [ + 'referenceuserid' => $referenceuserid, + 'userids' => $userids, + 'includecontactrequests' => $includecontactrequests, + 'includeprivacyinfo' => $includeprivacyinfo + ]; + $params = self::validate_parameters(self::get_member_info_parameters(), $params); + $systemcontext = context_system::instance(); + self::validate_context($systemcontext); + + if (($USER->id != $referenceuserid) && !has_capability('moodle/site:readallmessages', $systemcontext)) { + throw new moodle_exception('You do not have permission to perform this action.'); + } + + return \core_message\helper::get_member_info( + $params['referenceuserid'], + $params['userids'], + $params['includecontactrequests'], + $params['includeprivacyinfo'] + ); + } + + /** + * Get member info return description. + * + * @return external_description + */ + public static function get_member_info_returns() { + return new external_multiple_structure( + self::get_conversation_member_structure(true) + ); + } } diff --git a/version.php b/version.php index 93ff0a3fd83..49d5cac851e 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018111301.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018111301.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 0866b336353211400d46e56d3d605d8ef1906540 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 30 Oct 2018 15:10:05 +0800 Subject: [PATCH 12/31] MDL-63303 message: add fields to contact requests --- message/classes/api.php | 116 ++++++++---- message/externallib.php | 36 +++- message/tests/api_test.php | 226 +++++++++++++++++++++++- message/tests/externallib_test.php | 14 +- message/tests/privacy_provider_test.php | 26 +-- 5 files changed, 350 insertions(+), 68 deletions(-) diff --git a/message/classes/api.php b/message/classes/api.php index ddf2f4b773d..9788b21ced2 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -1960,19 +1960,65 @@ class api { public static function get_conversation_between_users(array $userids) { global $DB; - $hash = helper::get_conversation_hash($userids); + $conversations = self::get_individual_conversations_between_users([$userids]); + $conversation = $conversations[0]; - $params = [ - 'type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, - 'convhash' => $hash - ]; - if ($conversation = $DB->get_record('message_conversations', $params)) { + if ($conversation) { return $conversation->id; } return false; } + /** + * Returns the conversations between sets of users. + * + * The returned array of results will be in the same order as the requested + * arguments, null will be returned if there is no conversation for that user + * pair. + * + * For example: + * If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations + * exist. One between 1 and 2 and another between 5 and 6. + * + * Then if we call: + * $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]); + * + * The conversations array will look like: + * [, null, ]; + * + * Where null is returned for the pairing of [3, 4] since no record exists. + * + * @param array $useridsets An array of arrays where the inner array is the set of user ids + * @return stdClass[] Array of conversation records + */ + public static function get_individual_conversations_between_users(array $useridsets) : array { + global $DB; + + if (empty($useridsets)) { + return []; + } + + $hashes = array_map(function($userids) { + return helper::get_conversation_hash($userids); + }, $useridsets); + + list($inorequalsql, $params) = $DB->get_in_or_equal($hashes); + array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL); + $where = "type = ? AND convhash ${inorequalsql}"; + $conversations = array_fill(0, count($hashes), null); + $records = $DB->get_records_select('message_conversations', $where, $params); + + foreach (array_values($records) as $record) { + $index = array_search($record->convhash, $hashes); + if ($index !== false) { + $conversations[$index] = $record; + } + } + + return $conversations; + } + /** * Creates a conversation between two users. * @@ -2106,8 +2152,9 @@ class api { * * @param int $userid The id of the user who is creating the contact request * @param int $requesteduserid The id of the user being requested + * @return \stdClass the request */ - public static function create_contact_request(int $userid, int $requesteduserid) { + public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass { global $DB; $request = new \stdClass(); @@ -2115,32 +2162,9 @@ class api { $request->requesteduserid = $requesteduserid; $request->timecreated = time(); - $DB->insert_record('message_contact_requests', $request); + $request->id = $DB->insert_record('message_contact_requests', $request); - // Send a notification. - $userfrom = \core_user::get_user($userid); - $userfromfullname = fullname($userfrom); - $userto = \core_user::get_user($requesteduserid); - $url = new \moodle_url('/message/pendingcontactrequests.php'); - - $subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname); - $fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname); - - $message = new \core\message\message(); - $message->courseid = SITEID; - $message->component = 'moodle'; - $message->name = 'messagecontactrequests'; - $message->notification = 1; - $message->userfrom = $userfrom; - $message->userto = $userto; - $message->subject = $subject; - $message->fullmessage = text_to_html($fullmessage); - $message->fullmessageformat = FORMAT_HTML; - $message->fullmessagehtml = $fullmessage; - $message->smallmessage = ''; - $message->contexturl = $url->out(false); - - message_send($message); + return $request; } @@ -2207,6 +2231,17 @@ class api { return []; } + /** + * Count how many contact requests the user has received. + * + * @param \stdClass $user The user to fetch contact requests for + * @return int The count + */ + public static function count_received_contact_requests(\stdClass $user) : int { + global $DB; + return $DB->count_records('message_contact_requests', ['requesteduserid' => $user->id]); + } + /** * Handles adding a contact. * @@ -2354,6 +2389,23 @@ class api { return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]); } + /** + * Get contact requests between users. + * + * @param int $userid The id of the user who is creating the contact request + * @param int $requesteduserid The id of the user being requested + * @return \stdClass[] + */ + public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array { + global $DB; + + $sql = "SELECT * + FROM {message_contact_requests} mcr + WHERE (mcr.userid = ? AND mcr.requesteduserid = ?) + OR (mcr.userid = ? AND mcr.requesteduserid = ?)"; + return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]); + } + /** * Checks if a contact request already exists between users. * diff --git a/message/externallib.php b/message/externallib.php index b8a1dc16fb2..42c4491055f 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -842,21 +842,27 @@ class core_message_external extends external_api { throw new required_capability_exception($context, $capability, 'nopermissions', ''); } + $result = [ + 'warnings' => [] + ]; + if (!\core_message\api::can_create_contact($params['userid'], $params['requesteduserid'])) { - $warning[] = [ + $result['warnings'][] = [ 'item' => 'user', 'itemid' => $params['requesteduserid'], 'warningcode' => 'cannotcreatecontactrequest', 'message' => 'You are unable to create a contact request for this user' ]; - return $warning; + } else { + if ($requests = \core_message\api::get_contact_requests_between_users($params['userid'], $params['requesteduserid'])) { + // There should only ever be one but just in case there are multiple then we can return the first. + $result['request'] = array_shift($requests); + } else { + $result['request'] = \core_message\api::create_contact_request($params['userid'], $params['requesteduserid']); + } } - if (!\core_message\api::does_contact_request_exist($params['userid'], $params['requesteduserid'])) { - \core_message\api::create_contact_request($params['userid'], $params['requesteduserid']); - } - - return []; + return $result; } /** @@ -865,7 +871,21 @@ class core_message_external extends external_api { * @return external_description */ public static function create_contact_request_returns() { - return new external_warnings(); + return new external_single_structure( + array( + 'request' => new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'Message id'), + 'userid' => new external_value(PARAM_INT, 'User from id'), + 'requesteduserid' => new external_value(PARAM_INT, 'User to id'), + 'timecreated' => new external_value(PARAM_INT, 'Time created'), + ), + 'request record', + VALUE_OPTIONAL + ), + 'warnings' => new external_warnings() + ) + ); } /** diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 30add021e0c..61fe9b04cba 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -4570,13 +4570,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); - \core_message\api::create_contact_request($user1->id, $user2->id); - - $request = $DB->get_records('message_contact_requests'); - - $this->assertCount(1, $request); - - $request = reset($request); + $request = \core_message\api::create_contact_request($user1->id, $user2->id); $this->assertEquals($user1->id, $request->userid); $this->assertEquals($user2->id, $request->requesteduserid); @@ -4628,6 +4622,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase { * Test retrieving contact requests. */ public function test_get_contact_requests() { + global $PAGE; + $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); @@ -4643,6 +4639,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $this->assertCount(1, $requests); $request = reset($requests); + $userpicture = new \user_picture($user2); + $profileimageurl = $userpicture->get_url($PAGE)->out(false); $this->assertEquals($user2->id, $request->id); $this->assertEquals(fullname($user2), $request->fullname); @@ -4819,6 +4817,78 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $this->assertTrue(\core_message\api::does_contact_request_exist($user2->id, $user1->id)); } + /** + * Test the count_received_contact_requests() function. + */ + public function test_count_received_contact_requests() { + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + $this->assertEquals(0, \core_message\api::count_received_contact_requests($user1)); + + \core_message\api::create_contact_request($user2->id, $user1->id); + + $this->assertEquals(1, \core_message\api::count_received_contact_requests($user1)); + + \core_message\api::create_contact_request($user3->id, $user1->id); + + $this->assertEquals(2, \core_message\api::count_received_contact_requests($user1)); + + \core_message\api::create_contact_request($user1->id, $user4->id); + // Function should ignore sent requests. + $this->assertEquals(2, \core_message\api::count_received_contact_requests($user1)); + } + + /** + * Test the get_contact_requests_between_users() function. + */ + public function test_get_contact_requests_between_users() { + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + $this->assertEquals([], \core_message\api::get_contact_requests_between_users($user1->id, $user2->id)); + + $request1 = \core_message\api::create_contact_request($user2->id, $user1->id); + $results = \core_message\api::get_contact_requests_between_users($user1->id, $user2->id); + $results = array_values($results); + + $this->assertCount(1, $results); + $result = $results[0]; + $this->assertEquals($request1->id, $result->id); + + $request2 = \core_message\api::create_contact_request($user1->id, $user2->id); + $results = \core_message\api::get_contact_requests_between_users($user1->id, $user2->id); + $results = array_values($results); + + $this->assertCount(2, $results); + $actual = [(int) $results[0]->id, (int) $results[1]->id]; + $expected = [(int) $request1->id, (int) $request2->id]; + + sort($actual); + sort($expected); + + $this->assertEquals($expected, $actual); + + // Request from a different user. + \core_message\api::create_contact_request($user3->id, $user1->id); + + $results = \core_message\api::get_contact_requests_between_users($user1->id, $user2->id); + $results = array_values($results); + + $this->assertCount(2, $results); + $actual = [(int) $results[0]->id, (int) $results[1]->id]; + $expected = [(int) $request1->id, (int) $request2->id]; + + sort($actual); + sort($expected); + + $this->assertEquals($expected, $actual); + } + /** * Test the user in conversation check. */ @@ -5084,6 +5154,148 @@ class core_message_api_testcase extends core_message_messagelib_testcase { ); } + + /** + * Test an empty array returned when no args given. + */ + public function test_get_individual_conversations_between_users_no_user_sets() { + $this->assertEmpty(\core_message\api::get_individual_conversations_between_users([])); + } + + /** + * Test a conversation is not returned if there is none. + */ + public function test_get_individual_conversations_between_users_no_conversation() { + $generator = $this->getDataGenerator(); + $user1 = $generator->create_user(); + $user2 = $generator->create_user(); + + $this->assertEquals( + [null], + \core_message\api::get_individual_conversations_between_users([[$user1->id, $user2->id]]) + ); + } + + /** + * Test the result set includes null if there is no conversation between users. + */ + public function test_get_individual_conversations_between_users_partial_conversations() { + $generator = $this->getDataGenerator(); + $user1 = $generator->create_user(); + $user2 = $generator->create_user(); + $user3 = $generator->create_user(); + $type = \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL; + + $conversation1 = \core_message\api::create_conversation($type, [$user1->id, $user2->id]); + $conversation2 = \core_message\api::create_conversation($type, [$user1->id, $user3->id]); + + $results = \core_message\api::get_individual_conversations_between_users([ + [$user1->id, $user2->id], + [$user2->id, $user3->id], + [$user1->id, $user3->id] + ]); + + $result = array_map(function($result) { + if ($result) { + return $result->id; + } else { + return $result; + } + }, $results); + + $this->assertEquals( + [$conversation1->id, null, $conversation2->id], + $result + ); + } + + /** + * Test all conversations are returned if each set has a conversation. + */ + public function test_get_individual_conversations_between_users_all_conversations() { + $generator = $this->getDataGenerator(); + $user1 = $generator->create_user(); + $user2 = $generator->create_user(); + $user3 = $generator->create_user(); + $type = \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL; + + $conversation1 = \core_message\api::create_conversation($type, [$user1->id, $user2->id]); + $conversation2 = \core_message\api::create_conversation($type, [$user2->id, $user3->id]); + $conversation3 = \core_message\api::create_conversation($type, [$user1->id, $user3->id]); + + $results = \core_message\api::get_individual_conversations_between_users([ + [$user1->id, $user2->id], + [$user2->id, $user3->id], + [$user1->id, $user3->id] + ]); + + $result = array_map(function($result) { + if ($result) { + return $result->id; + } else { + return $result; + } + }, $results); + + $this->assertEquals( + [$conversation1->id, $conversation2->id, $conversation3->id], + $result + ); + } + + /** + * Test that the results are ordered to match the order of the parameters. + */ + public function test_get_individual_conversations_between_users_ordering() { + $generator = $this->getDataGenerator(); + $user1 = $generator->create_user(); + $user2 = $generator->create_user(); + $user3 = $generator->create_user(); + $type = \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL; + + $conversation1 = \core_message\api::create_conversation($type, [$user1->id, $user2->id]); + $conversation2 = \core_message\api::create_conversation($type, [$user2->id, $user3->id]); + $conversation3 = \core_message\api::create_conversation($type, [$user1->id, $user3->id]); + + $results = \core_message\api::get_individual_conversations_between_users([ + [$user1->id, $user2->id], + [$user2->id, $user3->id], + [$user1->id, $user3->id] + ]); + + $result = array_map(function($result) { + if ($result) { + return $result->id; + } else { + return $result; + } + }, $results); + + $this->assertEquals( + [$conversation1->id, $conversation2->id, $conversation3->id], + $result + ); + + $results = \core_message\api::get_individual_conversations_between_users([ + [$user2->id, $user3->id], + [$user1->id, $user2->id], + [$user1->id, $user3->id] + ]); + + $result = array_map(function($result) { + if ($result) { + return $result->id; + } else { + return $result; + } + }, $results); + + $this->assertEquals( + [$conversation2->id, $conversation1->id, $conversation3->id], + $result + ); + } + /** * Test returning members in a conversation with no contact requests. */ diff --git a/message/tests/externallib_test.php b/message/tests/externallib_test.php index 9d2f0add5ab..42aed082022 100644 --- a/message/tests/externallib_test.php +++ b/message/tests/externallib_test.php @@ -552,6 +552,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { * Test getting contact requests. */ public function test_get_contact_requests() { + global $PAGE; + $this->resetAfterTest(); $user1 = self::getDataGenerator()->create_user(); @@ -572,6 +574,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { $this->assertCount(1, $requests); $request = reset($requests); + $userpicture = new \user_picture($user2); + $profileimageurl = $userpicture->get_url($PAGE)->out(false); $this->assertEquals($user2->id, $request['id']); $this->assertEquals(fullname($user2), $request['fullname']); @@ -677,7 +681,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { $return = core_message_external::create_contact_request($user1->id, $user2->id); $return = external_api::clean_returnvalue(core_message_external::create_contact_request_returns(), $return); - $this->assertEquals(array(), $return); + $this->assertEquals([], $return['warnings']); $request = $DB->get_records('message_contact_requests'); @@ -685,8 +689,10 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { $request = reset($request); - $this->assertEquals($user1->id, $request->userid); - $this->assertEquals($user2->id, $request->requesteduserid); + $this->assertEquals($request->id, $return['request']['id']); + $this->assertEquals($request->userid, $return['request']['userid']); + $this->assertEquals($request->requesteduserid, $return['request']['requesteduserid']); + $this->assertEquals($request->timecreated, $return['request']['timecreated']); } /** @@ -707,7 +713,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { $return = core_message_external::create_contact_request($user1->id, $user2->id); $return = external_api::clean_returnvalue(core_message_external::create_contact_request_returns(), $return); - $warning = reset($return); + $warning = reset($return['warnings']); $this->assertEquals('user', $warning['item']); $this->assertEquals($user2->id, $warning['itemid']); diff --git a/message/tests/privacy_provider_test.php b/message/tests/privacy_provider_test.php index 101adbba016..5005483f69b 100644 --- a/message/tests/privacy_provider_test.php +++ b/message/tests/privacy_provider_test.php @@ -693,8 +693,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide // There should be 4 conversation members. $this->assertEquals(4, $DB->count_records('message_conversation_members')); - // There should be 3 notifications + 2 for the contact request. - $this->assertEquals(5, $DB->count_records('notifications')); + // There should be 3 notifications. + $this->assertEquals(3, $DB->count_records('notifications')); provider::delete_data_for_all_users_in_context($user1context); @@ -732,8 +732,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide // And user1 is not in any conversation. $this->assertEquals(0, $DB->count_records('message_conversation_members', ['userid' => $user1->id])); - // Confirm there is only 1 notification + 1 for the contact request. - $this->assertEquals(2, $DB->count_records('notifications')); + // Confirm there is only 1 notification. + $this->assertEquals(1, $DB->count_records('notifications')); // And it is not related to user1. $this->assertEquals(0, $DB->count_records_select('notifications', 'useridfrom = ? OR useridto = ? ', [$user1->id, $user1->id])); @@ -800,8 +800,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide // There should be two conversation members. $this->assertEquals(2, $DB->count_records('message_conversation_members')); - // There should be three notifications + two for the contact requests. - $this->assertEquals(5, $DB->count_records('notifications')); + // There should be three notifications. + $this->assertEquals(3, $DB->count_records('notifications')); $user1context = context_user::instance($user1->id); $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_message', @@ -846,13 +846,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide $mcm = reset($mcms); $this->assertEquals($user2->id, $mcm->userid); - $this->assertCount(2, $notifications); + $this->assertCount(1, $notifications); ksort($notifications); - $notification = array_shift($notifications); - $this->assertEquals($user2->id, $notification->useridfrom); - $this->assertEquals($user4->id, $notification->useridto); - $notification = array_shift($notifications); $this->assertEquals($user2->id, $notification->useridfrom); $this->assertEquals($user3->id, $notification->useridto); @@ -1120,7 +1116,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide $this->assertEquals(2, $DB->count_records('message_conversation_members')); // There should be three notifications + two for the contact requests. - $this->assertEquals(5, $DB->count_records('notifications')); + $this->assertEquals(3, $DB->count_records('notifications')); $user1context = context_user::instance($user1->id); $approveduserlist = new \core_privacy\local\request\approved_userlist($user1context, 'core_message', @@ -1167,13 +1163,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide $mcm = reset($mcms); $this->assertEquals($user2->id, $mcm->userid); - $this->assertCount(2, $notifications); + $this->assertCount(1, $notifications); ksort($notifications); - $notification = array_shift($notifications); - $this->assertEquals($user2->id, $notification->useridfrom); - $this->assertEquals($user4->id, $notification->useridto); - $notification = array_shift($notifications); $this->assertEquals($user2->id, $notification->useridfrom); $this->assertEquals($user3->id, $notification->useridto); From 886b01783b8b3adeef89e5675a08b09a1feea341 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:44:02 +0800 Subject: [PATCH 13/31] MDL-63303 message: add fields to send_instant_message --- message/externallib.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/message/externallib.php b/message/externallib.php index 42c4491055f..d9b555eacf9 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -164,6 +164,7 @@ class core_message_external extends external_api { $tousers = $DB->get_records_select("user", "id " . $sqluserids . " AND deleted = 0", $sqlparams); $resultmessages = array(); + $messageids = array(); foreach ($params['messages'] as $message) { $resultmsg = array(); //the infos about the success of the operation @@ -197,6 +198,12 @@ class core_message_external extends external_api { } if ($success) { $resultmsg['msgid'] = $success; + $resultmsg['text'] = message_format_message_text((object) [ + 'smallmessage' => $message['text'], + 'fullmessageformat' => external_validate_format($message['textformat']) + ]); + $resultmsg['timecreated'] = time(); + $messageids[] = $success; } else { // WARNINGS: for backward compatibility we return this errormessage. // We should have thrown exceptions as these errors prevent results to be returned. @@ -208,6 +215,16 @@ class core_message_external extends external_api { $resultmessages[] = $resultmsg; } + if (!empty($messageids)) { + $messagerecords = $DB->get_records_list('messages', 'id', $messageids, '', 'id, conversationid'); + $resultmessages = array_map(function($resultmessage) use ($messagerecords, $USER) { + $id = $resultmessage['msgid']; + $resultmessage['conversationid'] = isset($messagerecords[$id]) ? $messagerecords[$id]->conversationid : null; + $resultmessage['useridfrom'] = $USER->id; + return $resultmessage; + }, $resultmessages); + } + return $resultmessages; } @@ -223,7 +240,11 @@ class core_message_external extends external_api { array( 'msgid' => new external_value(PARAM_INT, 'test this to know if it succeeds: id of the created message if it succeeded, -1 when failed'), 'clientmsgid' => new external_value(PARAM_ALPHANUMEXT, 'your own id for the message', VALUE_OPTIONAL), - 'errormessage' => new external_value(PARAM_TEXT, 'error message - if it failed', VALUE_OPTIONAL) + 'errormessage' => new external_value(PARAM_TEXT, 'error message - if it failed', VALUE_OPTIONAL), + 'text' => new external_value(PARAM_RAW, 'The text of the message', VALUE_OPTIONAL), + 'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the message', VALUE_OPTIONAL), + 'conversationid' => new external_value(PARAM_INT, 'The conversation id for this message', VALUE_OPTIONAL), + 'useridfrom' => new external_value(PARAM_INT, 'The user id who sent the message', VALUE_OPTIONAL), ) ) ); From 6399c7ef144bc7e480bc7539fb7d6dbf9c0e4ec2 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:44:55 +0800 Subject: [PATCH 14/31] MDL-63303 message: add count_conversations to api --- message/classes/api.php | 78 ++++++++++++ message/tests/api_test.php | 235 +++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) diff --git a/message/classes/api.php b/message/classes/api.php index 9788b21ced2..650d3e51975 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -1297,6 +1297,84 @@ class api { return false; } + /** + * Returns the count of conversations (collection of messages from a single user) for + * the given user. + * + * @param \stdClass $user The user who's conversations should be counted + * @param int $type The conversation type + * @param bool $excludefavourites Exclude favourite conversations + * @return int the count of the user's unread conversations + */ + public static function count_conversations($user, int $type = null, bool $excludefavourites = false) { + global $DB; + + $params = []; + $favouritessql = ''; + + if ($excludefavourites) { + $favouritessql = "AND m.conversationid NOT IN ( + SELECT itemid + FROM {favourite} + WHERE component = 'core_message' + AND itemtype = 'message_conversations' + AND userid = ? + )"; + $params[] = $user->id; + } + + switch($type) { + case null: + $params = array_merge([$user->id, self::MESSAGE_ACTION_DELETED, $user->id], $params); + $sql = "SELECT COUNT(DISTINCT(m.conversationid)) + FROM {messages} m + LEFT JOIN {message_conversations} c + ON m.conversationid = c.id + LEFT JOIN {message_user_actions} ma + ON ma.messageid = m.id + LEFT JOIN {message_conversation_members} mcm + ON m.conversationid = mcm.conversationid + WHERE mcm.userid = ? + AND ( + c.type != " . self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL . " + OR + ( + (ma.action IS NULL OR ma.action != ? OR ma.userid != ?) + AND c.type = " . self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL . " + ) + ) + ${favouritessql}"; + break; + case self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL: + $params = array_merge([self::MESSAGE_ACTION_DELETED, $user->id, $user->id], $params); + $sql = "SELECT COUNT(DISTINCT(m.conversationid)) + FROM {messages} m + LEFT JOIN {message_conversations} c + ON m.conversationid = c.id + LEFT JOIN {message_user_actions} ma + ON ma.messageid = m.id + LEFT JOIN {message_conversation_members} mcm + ON m.conversationid = mcm.conversationid + WHERE (ma.action IS NULL OR ma.action != ? OR ma.userid != ?) + AND mcm.userid = ? + AND c.type = " . self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL . " + ${favouritessql}"; + break; + default: + $params = array_merge([$user->id, $type], $params); + $sql = "SELECT COUNT(m.conversationid) + FROM {message_conversation_members} m + LEFT JOIN {message_conversations} c + ON m.conversationid = c.id + WHERE m.userid = ? + AND c.type = ? + ${favouritessql}"; + + } + + return $DB->count_records_sql($sql, $params); + } + /** * Marks all messages being sent to a user in a particular conversation. * diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 61fe9b04cba..6efdb1b63c3 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -5598,6 +5598,241 @@ class core_message_api_testcase extends core_message_messagelib_testcase { \core_message\api::send_message_to_conversation($user2->id, $ic1->id, 'test', FORMAT_MOODLE); } + /** + * Data provider for test_count_conversations(). + */ + public function test_count_conversations_test_cases() { + $typeindividual = \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL; + $typegroup = \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP; + list($user1, $user2, $user3, $user4, $user5) = [0, 1, 2, 3, 4]; + $conversations = [ + [ + 'type' => $typeindividual, + 'users' => [$user1, $user2], + 'messages' => [$user1, $user2], + 'favourites' => [$user1] + ], + [ + 'type' => $typeindividual, + 'users' => [$user1, $user3], + 'messages' => [$user1, $user1], + 'favourites' => [] + ], + [ + 'type' => $typegroup, + 'users' => [$user1, $user2, $user3, $user4], + 'messages' => [$user1, $user2, $user3, $user4], + 'favourites' => [] + ], + ]; + + return [ + 'No conversations' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user5], + 'expected' => 0 + ], + 'No individual conversations' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user4, $typeindividual], + 'expected' => 0 + ], + 'No individual conversations, 1 group conversation' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user4], + 'expected' => 1 + ], + '1 - Multiple individual conversations, 1 group conversation' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user1, $typegroup], + 'expected' => 1 + ], + '2 - Multiple individual conversations, 1 group conversation' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user2, $typegroup], + 'expected' => 1 + ], + '3 - Multiple individual conversations, 1 group conversation' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user3, $typegroup], + 'expected' => 1 + ], + '4 - Multiple individual conversations, 1 group conversation' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user4, $typegroup], + 'expected' => 1 + ], + 'Individual exclude favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user1, $typeindividual, true], + 'expected' => 1 + ], + 'Individual include favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user1, $typeindividual, false], + 'expected' => 2 + ], + 'All exclude favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user1, null, true], + 'expected' => 2 + ], + 'All include favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => null, + 'delete' => [], + 'arguments' => [$user1, null, false], + 'expected' => 3 + ], + 'Delete single message individual' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [1], + 'arguments' => [$user1, $typeindividual], + 'expected' => 2 + ], + 'Delete single message all' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [1], + 'arguments' => [$user1, null], + 'expected' => 3 + ], + 'Delete all message individual conversation include favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [2, 3], + 'arguments' => [$user1, $typeindividual, false], + 'expected' => 1 + ], + 'Delete all message individual conversation exclude favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [2, 3], + 'arguments' => [$user1, $typeindividual, true], + 'expected' => 0 + ], + 'Delete all message individual conversation include favourites diff user' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [2, 3], + 'arguments' => [$user2, $typeindividual, false], + 'expected' => 1 + ], + 'Delete all message individual conversation exclude favourites diff user' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [2, 3], + 'arguments' => [$user2, $typeindividual, true], + 'expected' => 1 + ], + 'Delete all message group conversation include favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [4, 5, 6, 7], + 'arguments' => [$user1, $typegroup, false], + 'expected' => 1 + ], + 'Delete all message group conversation include favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [4, 5, 6, 7], + 'arguments' => [$user1, null, false], + 'expected' => 3 + ], + 'Delete all message group conversation exclude favourites' => [ + 'conversationConfigs' => $conversations, + 'deleteuser' => $user1, + 'delete' => [4, 5, 6, 7], + 'arguments' => [$user1, null, true], + 'expected' => 2 + ] + ]; + } + + /** + * Test the count_conversations() function. + * + * @dataProvider test_count_conversations_test_cases() + * @param array $conversationconfigs Conversations to create + * @param int $deleteuser The user who is deleting the messages + * @param array $delete The list of messages to delete (by index) + * @param array $arguments Arguments for the count conversations function + * @param int $expected The expected result + */ + public function test_count_conversations( + $conversationconfigs, + $deleteuser, + $delete, + $arguments, + $expected + ) { + $generator = $this->getDataGenerator(); + $users = [ + $generator->create_user(), + $generator->create_user(), + $generator->create_user(), + $generator->create_user(), + $generator->create_user() + ]; + + $user = $users[$arguments[0]]; + $deleteuser = !is_null($deleteuser) ? $users[$deleteuser] : null; + $arguments[0] = $user; + $systemcontext = \context_system::instance(); + $conversations = []; + $messageids = []; + + foreach ($conversationconfigs as $config) { + $conversation = \core_message\api::create_conversation( + $config['type'], + array_map(function($userindex) use ($users) { + return $users[$userindex]->id; + }, $config['users']) + ); + + foreach ($config['messages'] as $userfromindex) { + $userfrom = $users[$userfromindex]; + $messageids[] = testhelper::send_fake_message_to_conversation($userfrom, $conversation->id); + } + + foreach ($config['favourites'] as $userfromindex) { + $userfrom = $users[$userfromindex]; + $usercontext = \context_user::instance($userfrom->id); + $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); + $ufservice->create_favourite('core_message', 'message_conversations', $conversation->id, $systemcontext); + } + + $conversations[] = $conversation; + } + + foreach ($delete as $messageindex) { + \core_message\api::delete_message($deleteuser->id, $messageids[$messageindex]); + } + + $this->assertEquals($expected, \core_message\api::count_conversations(...$arguments)); + } + /** * Comparison function for sorting contacts. * From 4e3130269cbbe61387495a7a9228478263e26890 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 5 Nov 2018 16:11:26 +0800 Subject: [PATCH 15/31] MDL-63303 message: add get_conversation to message api --- lib/db/services.php | 9 ++ message/classes/api.php | 115 ++++++++++++++++++ message/externallib.php | 101 +++++++++++++++- message/tests/externallib_test.php | 181 +++++++++++++++++++++++++++++ version.php | 2 +- 5 files changed, 406 insertions(+), 2 deletions(-) diff --git a/lib/db/services.php b/lib/db/services.php index 6aecd22850e..e1a87831be1 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1087,6 +1087,15 @@ $functions = array( 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_message_get_conversation' => array( + 'classname' => 'core_message_external', + 'methodname' => 'get_conversation', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve a conversation for a user', + 'type' => 'read', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + 'ajax' => true + ), 'core_message_get_messages' => array( 'classname' => 'core_message_external', 'methodname' => 'get_messages', diff --git a/message/classes/api.php b/message/classes/api.php index 650d3e51975..4039f757a26 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -801,6 +801,121 @@ class api { return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum); } + /** + * Return a conversation. + * + * @param int $userid The user id to get the conversation for + * @param int $conversationid The id of the conversation to fetch + * @param bool $includecontactrequests Should contact requests be included between members + * @param bool $includeprivacyinfo Should privacy info be included between members + * @param int $memberlimit Limit number of members to load + * @param int $memberoffset Offset members by this amount + * @param int $messagelimit Limit number of messages to load + * @param int $messageoffset Offset the messages + * @param bool $newestmessagesfirst Order messages by newest first + * @return \stdClass + */ + public static function get_conversation( + int $userid, + int $conversationid, + bool $includecontactrequests = false, + bool $includeprivacyinfo = false, + int $memberlimit = 0, + int $memberoffset = 0, + int $messagelimit = 0, + int $messageoffset = 0, + bool $newestmessagesfirst = true + ) { + global $USER, $DB; + + $systemcontext = \context_system::instance(); + $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext); + if (($USER->id != $userid) && !$canreadallmessages) { + throw new \moodle_exception('You do not have permission to perform this action.'); + } + + $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]); + if (!$conversation) { + return null; + } + + $isconversationmember = $DB->record_exists( + 'message_conversation_members', + [ + 'conversationid' => $conversationid, + 'userid' => $userid + ] + ); + + if (!$isconversationmember && !$canreadallmessages) { + throw new \moodle_exception('You do not have permission to view this conversation.'); + } + + $members = self::get_conversation_members( + $userid, + $conversationid, + $includecontactrequests, + $memberoffset, + $memberlimit + ); + // Strip out the requesting user to match what get_conversations does. + $members = array_filter($members, function($member) use ($userid) { + return $member->id != $userid; + }); + + $messages = self::get_conversation_messages( + $userid, + $conversationid, + $messageoffset, + $messagelimit, + $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC' + ); + + $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); + $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $systemcontext); + + $convextrafields = self::get_linked_conversation_extra_fields([$conversation]); + $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null; + $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null; + + $unreadcountssql = 'SELECT count(m.id) + FROM {messages} m + INNER JOIN {message_conversations} mc + ON mc.id = m.conversationid + LEFT JOIN {message_user_actions} mua + ON (mua.messageid = m.id AND mua.userid = ? AND + (mua.action = ? OR mua.action = ?)) + WHERE m.conversationid = ? + AND m.useridfrom != ? + AND mua.id is NULL'; + $unreadcount = $DB->count_records_sql( + $unreadcountssql, + [ + $userid, + self::MESSAGE_ACTION_READ, + self::MESSAGE_ACTION_DELETED, + $conversationid, + $userid + ] + ); + + $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]); + + return (object) [ + 'id' => $conversation->id, + 'name' => $conversation->name, + 'subname' => $subname, + 'imageurl' => $imageurl, + 'type' => $conversation->type, + 'membercount' => $membercount, + 'isfavourite' => $isfavourite, + 'isread' => empty($unreadcount), + 'unreadcount' => $unreadcount, + 'members' => $members, + 'messages' => $messages['messages'] + ]; + } + /** * Mark a conversation as a favourite for the given user. * diff --git a/message/externallib.php b/message/externallib.php index d9b555eacf9..bdd1fcac262 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -1050,7 +1050,6 @@ class core_message_external extends external_api { * @return external_single_structure * @since Moodle 3.6 */ - private static function get_conversation_structure() { return new external_single_structure( array( @@ -1616,6 +1615,106 @@ class core_message_external extends external_api { ); } + /** + * Get conversation parameters. + * + * @return external_function_parameters + */ + public static function get_conversation_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing conversations for'), + 'conversationid' => new external_value(PARAM_INT, 'The id of the conversation to fetch'), + 'includecontactrequests' => new external_value(PARAM_BOOL, 'Include contact requests in the members'), + 'includeprivacyinfo' => new external_value(PARAM_BOOL, 'Include privacy info in the members'), + 'memberlimit' => new external_value(PARAM_INT, 'Limit for number of members', VALUE_DEFAULT, 0), + 'memberoffset' => new external_value(PARAM_INT, 'Offset for member list', VALUE_DEFAULT, 0), + 'messagelimit' => new external_value(PARAM_INT, 'Limit for number of messages', VALUE_DEFAULT, 100), + 'messageoffset' => new external_value(PARAM_INT, 'Offset for messages list', VALUE_DEFAULT, 0), + 'newestmessagesfirst' => new external_value(PARAM_BOOL, 'Order messages by newest first', VALUE_DEFAULT, true) + ) + ); + } + + /** + * Get a single conversation. + * + * @param int $userid The user id to get the conversation for + * @param int $conversationid The id of the conversation to fetch + * @param bool $includecontactrequests Should contact requests be included between members + * @param bool $includeprivacyinfo Should privacy info be included between members + * @param int $memberlimit Limit number of members to load + * @param int $memberoffset Offset members by this amount + * @param int $messagelimit Limit number of messages to load + * @param int $messageoffset Offset the messages + * @param bool $newestmessagesfirst Order messages by newest first + * @return stdClass + * @throws \moodle_exception if the messaging feature is disabled on the site. + */ + public static function get_conversation( + int $userid, + int $conversationid, + bool $includecontactrequests = false, + bool $includeprivacyinfo = false, + int $memberlimit = 0, + int $memberoffset = 0, + int $messagelimit = 0, + int $messageoffset = 0, + bool $newestmessagesfirst = true + ) { + global $CFG, $DB, $USER; + + // All the standard BL checks. + if (empty($CFG->messaging)) { + throw new moodle_exception('disabled', 'message'); + } + + $params = [ + 'userid' => $userid, + 'conversationid' => $conversationid, + 'includecontactrequests' => $includecontactrequests, + 'includeprivacyinfo' => $includeprivacyinfo, + 'memberlimit' => $memberlimit, + 'memberoffset' => $memberoffset, + 'messagelimit' => $messagelimit, + 'messageoffset' => $messageoffset, + 'newestmessagesfirst' => $newestmessagesfirst + ]; + self::validate_parameters(self::get_conversation_parameters(), $params); + + $systemcontext = context_system::instance(); + self::validate_context($systemcontext); + + $conversation = \core_message\api::get_conversation( + $params['userid'], + $params['conversationid'], + $params['includecontactrequests'], + $params['includeprivacyinfo'], + $params['memberlimit'], + $params['memberoffset'], + $params['messagelimit'], + $params['messageoffset'], + $params['newestmessagesfirst'] + ); + + if ($conversation) { + return $conversation; + } else { + // We have to throw an exception here because the external functions annoyingly + // don't accept null to be returned for a single structure. + throw new \moodle_exception('Conversation does not exist'); + } + } + + /** + * Get conversation returns. + * + * @return external_single_structure + */ + public static function get_conversation_returns() { + return self::get_conversation_structure(); + } + /** * The messagearea conversations parameters. * diff --git a/message/tests/externallib_test.php b/message/tests/externallib_test.php index 42aed082022..93777b621a1 100644 --- a/message/tests/externallib_test.php +++ b/message/tests/externallib_test.php @@ -5632,4 +5632,185 @@ class core_message_externallib_testcase extends externallib_advanced_testcase { $this->expectException(\moodle_exception::class); $writtenmessages = core_message_external::send_messages_to_conversation($gc1->id, $messages); } + + /** + * Test getting a conversation that doesn't exist. + */ + public function test_get_conversation_no_conversation() { + $this->resetAfterTest(); + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + $name = 'lol conversation'; + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ], + $name + ); + $conversationid = $conversation->id; + + $this->setUser($user1); + + $this->expectException('moodle_exception'); + $conv = core_message_external::get_conversation($user1->id, $conversationid + 1); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + } + + /** + * Test getting a conversation with no messages. + */ + public function test_get_conversation_no_messages() { + $this->resetAfterTest(); + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + $name = 'lol conversation'; + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ], + $name + ); + $conversationid = $conversation->id; + + $this->setUser($user1); + + $conv = core_message_external::get_conversation($user1->id, $conversationid); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertEquals($conversationid, $conv['id']); + $this->assertEquals($name, $conv['name']); + $this->assertArrayHasKey('subname', $conv); + $this->assertEquals(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $conv['type']); + $this->assertEquals(2, $conv['membercount']); + $this->assertEquals(false, $conv['isfavourite']); + $this->assertEquals(true, $conv['isread']); + $this->assertEquals(0, $conv['unreadcount']); + $this->assertCount(1, $conv['members']); + foreach ($conv['members'] as $member) { + $member = (array) $member; + $this->assertArrayHasKey('id', $member); + $this->assertArrayHasKey('fullname', $member); + $this->assertArrayHasKey('profileimageurl', $member); + $this->assertArrayHasKey('profileimageurlsmall', $member); + $this->assertArrayHasKey('isonline', $member); + $this->assertArrayHasKey('showonlinestatus', $member); + $this->assertArrayHasKey('isblocked', $member); + $this->assertArrayHasKey('iscontact', $member); + } + $this->assertEmpty($conv['messages']); + } + + /** + * Test getting a conversation with messages. + */ + public function test_get_conversation_with_messages() { + $this->resetAfterTest(); + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + // Some random conversation. + $otherconversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ] + ); + + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id, + ] + ); + $conversationid = $conversation->id; + + $time = time(); + $message1id = testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'A', $time - 10); + $message2id = testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'B', $time - 5); + $message3id = testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'C', $time); + + // Add some messages to the other convo to make sure they aren't included. + testhelper::send_fake_message_to_conversation($user1, $otherconversation->id, 'foo'); + + $this->setUser($user1); + + // Test newest first. + $conv = core_message_external::get_conversation( + $user1->id, + $conversationid, + false, + false, + 0, + 0, + 0, + 0, + true + ); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertEquals(false, $conv['isread']); + $this->assertEquals(1, $conv['unreadcount']); + $this->assertCount(3, $conv['messages']); + $this->assertEquals($message3id, $conv['messages'][0]->id); + $this->assertEquals($user1->id, $conv['messages'][0]->useridfrom); + $this->assertEquals($message2id, $conv['messages'][1]->id); + $this->assertEquals($user2->id, $conv['messages'][1]->useridfrom); + $this->assertEquals($message1id, $conv['messages'][2]->id); + $this->assertEquals($user1->id, $conv['messages'][2]->useridfrom); + + // Test newest last. + $conv = core_message_external::get_conversation( + $user1->id, + $conversationid, + false, + false, + 0, + 0, + 0, + 0, + false + ); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertCount(3, $conv['messages']); + $this->assertEquals($message3id, $conv['messages'][2]->id); + $this->assertEquals($user1->id, $conv['messages'][2]->useridfrom); + $this->assertEquals($message2id, $conv['messages'][1]->id); + $this->assertEquals($user2->id, $conv['messages'][1]->useridfrom); + $this->assertEquals($message1id, $conv['messages'][0]->id); + $this->assertEquals($user1->id, $conv['messages'][0]->useridfrom); + + // Test message offest and limit. + $conv = core_message_external::get_conversation( + $user1->id, + $conversationid, + false, + false, + 0, + 0, + 1, + 1, + true + ); + external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv); + + $conv = (array) $conv; + $this->assertCount(1, $conv['messages']); + $this->assertEquals($message2id, $conv['messages'][0]->id); + $this->assertEquals($user2->id, $conv['messages'][0]->useridfrom); + } } diff --git a/version.php b/version.php index 49d5cac851e..f99c6d23859 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018111301.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018111301.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 569c0bae9c68df4cd047d338117a560f6225ce00 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 6 Nov 2018 12:05:11 +0800 Subject: [PATCH 16/31] MDL-63303 message: add get_conversation_between_users external func --- lib/db/services.php | 9 ++++ message/externallib.php | 105 ++++++++++++++++++++++++++++++++++++++++ version.php | 2 +- 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/lib/db/services.php b/lib/db/services.php index e1a87831be1..e21b9574737 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1096,6 +1096,15 @@ $functions = array( 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), 'ajax' => true ), + 'core_message_get_conversation_between_users' => array( + 'classname' => 'core_message_external', + 'methodname' => 'get_conversation_between_users', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve a conversation for a user between another user', + 'type' => 'read', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + 'ajax' => true + ), 'core_message_get_messages' => array( 'classname' => 'core_message_external', 'methodname' => 'get_messages', diff --git a/message/externallib.php b/message/externallib.php index bdd1fcac262..a5ec428edf7 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -1715,6 +1715,111 @@ class core_message_external extends external_api { return self::get_conversation_structure(); } + /** + * Get conversation parameters. + * + * @return external_function_parameters + */ + public static function get_conversation_between_users_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing conversations for'), + 'otheruserid' => new external_value(PARAM_INT, 'The other user id'), + 'includecontactrequests' => new external_value(PARAM_BOOL, 'Include contact requests in the members'), + 'includeprivacyinfo' => new external_value(PARAM_BOOL, 'Include privacy info in the members'), + 'memberlimit' => new external_value(PARAM_INT, 'Limit for number of members', VALUE_DEFAULT, 0), + 'memberoffset' => new external_value(PARAM_INT, 'Offset for member list', VALUE_DEFAULT, 0), + 'messagelimit' => new external_value(PARAM_INT, 'Limit for number of messages', VALUE_DEFAULT, 100), + 'messageoffset' => new external_value(PARAM_INT, 'Offset for messages list', VALUE_DEFAULT, 0), + 'newestmessagesfirst' => new external_value(PARAM_BOOL, 'Order messages by newest first', VALUE_DEFAULT, true) + ) + ); + } + + /** + * Get a single conversation between users. + * + * @param int $userid The user id to get the conversation for + * @param int $otheruserid The other user id + * @param bool $includecontactrequests Should contact requests be included between members + * @param bool $includeprivacyinfo Should privacy info be included between members + * @param int $memberlimit Limit number of members to load + * @param int $memberoffset Offset members by this amount + * @param int $messagelimit Limit number of messages to load + * @param int $messageoffset Offset the messages + * @param bool $newestmessagesfirst Order messages by newest first + * @return stdClass + * @throws \moodle_exception if the messaging feature is disabled on the site. + */ + public static function get_conversation_between_users( + int $userid, + int $otheruserid, + bool $includecontactrequests = false, + bool $includeprivacyinfo = false, + int $memberlimit = 0, + int $memberoffset = 0, + int $messagelimit = 0, + int $messageoffset = 0, + bool $newestmessagesfirst = true + ) { + global $CFG, $DB, $USER; + + // All the standard BL checks. + if (empty($CFG->messaging)) { + throw new moodle_exception('disabled', 'message'); + } + + $params = [ + 'userid' => $userid, + 'otheruserid' => $otheruserid, + 'includecontactrequests' => $includecontactrequests, + 'includeprivacyinfo' => $includeprivacyinfo, + 'memberlimit' => $memberlimit, + 'memberoffset' => $memberoffset, + 'messagelimit' => $messagelimit, + 'messageoffset' => $messageoffset, + 'newestmessagesfirst' => $newestmessagesfirst + ]; + self::validate_parameters(self::get_conversation_between_users_parameters(), $params); + + $systemcontext = context_system::instance(); + self::validate_context($systemcontext); + + $conversationid = \core_message\api::get_conversation_between_users([$params['userid'], $params['otheruserid']]); + $conversation = null; + + if ($conversationid) { + $conversation = \core_message\api::get_conversation( + $params['userid'], + $conversationid, + $params['includecontactrequests'], + $params['includeprivacyinfo'], + $params['memberlimit'], + $params['memberoffset'], + $params['messagelimit'], + $params['messageoffset'], + $params['newestmessagesfirst'] + ); + } + + if ($conversation) { + return $conversation; + } else { + // We have to throw an exception here because the external functions annoyingly + // don't accept null to be returned for a single structure. + throw new \moodle_exception('Conversation does not exist'); + } + } + + /** + * Get conversation returns. + * + * @return external_single_structure + */ + public static function get_conversation_between_users_returns() { + return self::get_conversation_structure(true); + } + /** * The messagearea conversations parameters. * diff --git a/version.php b/version.php index f99c6d23859..42fb311532e 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018111301.02; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018111301.03; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From b3bbd4a0e1a6943fcd046a214e0067ad2da3e940 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 5 Nov 2018 14:12:25 +0800 Subject: [PATCH 17/31] MDL-63303 message: fix get_conversations external func --- lib/db/services.php | 1 + message/externallib.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/db/services.php b/lib/db/services.php index e21b9574737..e0b5596c968 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1086,6 +1086,7 @@ $functions = array( 'description' => 'Retrieve a list of conversations for a user', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + 'ajax' => true ), 'core_message_get_conversation' => array( 'classname' => 'core_message_external', diff --git a/message/externallib.php b/message/externallib.php index a5ec428edf7..07d01d58005 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -1609,7 +1609,7 @@ class core_message_external extends external_api { return new external_single_structure( [ 'conversations' => new external_multiple_structure( - self::get_conversation_structure() + self::get_conversation_structure(true) ) ] ); From ecb4755c917bf5b0b6ef3fb7c5b4eed753704f0f Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Fri, 9 Nov 2018 13:20:04 +0800 Subject: [PATCH 18/31] MDL-63303 message: add count_contacts api function --- message/classes/api.php | 15 +++++++++++++++ message/tests/api_test.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/message/classes/api.php b/message/classes/api.php index 4039f757a26..e21059bd882 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -1001,6 +1001,21 @@ class api { return []; } + /** + * Returns the contacts count. + * + * @param int $userid The user id + * @return array + */ + public static function count_contacts(int $userid) : int { + global $DB; + + $sql = "SELECT COUNT(id) + FROM {message_contacts} + WHERE userid = ? OR contactid = ?"; + return $DB->count_records_sql($sql, [$userid, $userid]); + } + /** * Returns the an array of the users the given user is in a conversation * with who are a contact and the number of unread messages. diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 6efdb1b63c3..f7446ab6ce6 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -5833,6 +5833,35 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $this->assertEquals($expected, \core_message\api::count_conversations(...$arguments)); } + /** + * Test the count_contacts() function. + */ + public function test_count_contacts() { + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + $this->assertEquals(0, \core_message\api::count_contacts($user1->id)); + + \core_message\api::create_contact_request($user1->id, $user2->id); + + // Still zero until the request is confirmed. + $this->assertEquals(0, \core_message\api::count_contacts($user1->id)); + + \core_message\api::confirm_contact_request($user1->id, $user2->id); + + $this->assertEquals(1, \core_message\api::count_contacts($user1->id)); + + \core_message\api::create_contact_request($user3->id, $user1->id); + + // Still one until the request is confirmed. + $this->assertEquals(1, \core_message\api::count_contacts($user1->id)); + + \core_message\api::confirm_contact_request($user3->id, $user1->id); + + $this->assertEquals(2, \core_message\api::count_contacts($user1->id)); + } + /** * Comparison function for sorting contacts. * From 0802c38a2fca2494940e8d2e732162b4f9af930c Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Fri, 9 Nov 2018 14:18:15 +0800 Subject: [PATCH 19/31] MDL-63303 message: add lastmessagedate to messagearea contact --- message/classes/helper.php | 3 +++ message/classes/output/messagearea/contact.php | 7 +++++++ message/externallib.php | 1 + 3 files changed, 11 insertions(+) diff --git a/message/classes/helper.php b/message/classes/helper.php index 38b4e554030..8eef5fe7685 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -306,11 +306,13 @@ class helper { // Store the message if we have it. $data->ismessaging = false; $data->lastmessage = null; + $data->lastmessagedate = null; $data->messageid = null; if (isset($contact->smallmessage)) { $data->ismessaging = true; // Strip the HTML tags from the message for displaying in the contact area. $data->lastmessage = clean_param($contact->smallmessage, PARAM_NOTAGS); + $data->lastmessagedate = $contact->timecreated; $data->useridfrom = $contact->useridfrom; if (isset($contact->messageid)) { $data->messageid = $contact->messageid; @@ -608,6 +610,7 @@ class helper { $data->profileimageurlsmall = $conv->members[$otheruser->id]->profileimageurlsmall; $data->ismessaging = isset($conv->messages[0]->text) ? true : false; $data->lastmessage = $conv->messages[0]->text ? clean_param($conv->messages[0]->text, PARAM_NOTAGS) : null; + $data->lastmessagedate = $conv->messages[0]->timecreated ?? null; $data->messageid = $conv->messages[0]->id ?? null; $data->isonline = $conv->members[$otheruser->id]->isonline ?? null; $data->isblocked = $conv->members[$otheruser->id]->isblocked ?? null; diff --git a/message/classes/output/messagearea/contact.php b/message/classes/output/messagearea/contact.php index 2f0b504a23a..3a138c1250b 100644 --- a/message/classes/output/messagearea/contact.php +++ b/message/classes/output/messagearea/contact.php @@ -83,6 +83,11 @@ class contact implements templatable, renderable { */ public $lastmessage; + /** + * @var int The last message sent timestamp. + */ + public $lastmessagedate; + /** * @var bool Is the user online? */ @@ -117,6 +122,7 @@ class contact implements templatable, renderable { $this->messageid = $contact->messageid; $this->ismessaging = $contact->ismessaging; $this->lastmessage = $contact->lastmessage; + $this->lastmessagedate = $contact->lastmessagedate; $this->isonline = $contact->isonline; $this->isblocked = $contact->isblocked; $this->isread = $contact->isread; @@ -140,6 +146,7 @@ class contact implements templatable, renderable { } else { $contact->lastmessage = null; } + $contact->lastmessagedate = $this->lastmessagedate; $contact->showonlinestatus = is_null($this->isonline) ? false : true; $contact->isonline = $this->isonline; $contact->isblocked = $this->isblocked; diff --git a/message/externallib.php b/message/externallib.php index 07d01d58005..ddc5e12d0c0 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -1033,6 +1033,7 @@ class core_message_external extends external_api { 'ismessaging' => new external_value(PARAM_BOOL, 'If we are messaging the user'), 'sentfromcurrentuser' => new external_value(PARAM_BOOL, 'Was the last message sent from the current user?'), 'lastmessage' => new external_value(PARAM_NOTAGS, 'The user\'s last message'), + 'lastmessagedate' => new external_value(PARAM_INT, 'Timestamp for last message', VALUE_DEFAULT, null), 'messageid' => new external_value(PARAM_INT, 'The unique search message id', VALUE_DEFAULT, null), 'showonlinestatus' => new external_value(PARAM_BOOL, 'Show the user\'s online status?'), 'isonline' => new external_value(PARAM_BOOL, 'The user\'s online status'), From 3ea46c8e312072780f189861ee3b2f338dc78f17 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:45:59 +0800 Subject: [PATCH 20/31] MDL-63303 message: add functions to message_repository.js --- message/amd/build/message_repository.min.js | 2 +- message/amd/src/message_repository.js | 878 ++++++++++++++++++++ 2 files changed, 879 insertions(+), 1 deletion(-) diff --git a/message/amd/build/message_repository.min.js b/message/amd/build/message_repository.min.js index 5f20161d194..4e55810c9a1 100644 --- a/message/amd/build/message_repository.min.js +++ b/message/amd/build/message_repository.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/notification"],function(a,b,c){var d=function(a){"undefined"==typeof a.limit&&(a.limit=0),"undefined"==typeof a.offset&&(a.offset=0),a.limitfrom=a.offset,a.limitnum=a.limit,delete a.limit,delete a.offset;var d={methodname:"core_message_data_for_messagearea_conversations",args:a},e=b.call([d])[0];return e.fail(c.exception),e},e=function(a){var d={methodname:"core_message_get_unread_conversations_count",args:a},e=b.call([d])[0];return e.fail(c.exception),e},f=function(a){var d={methodname:"core_message_mark_all_messages_as_read",args:a},e=b.call([d])[0];return e.fail(c.exception),e};return{query:d,countUnreadConversations:e,markAllAsRead:f}}); \ No newline at end of file +define(["jquery","core/ajax","core/notification"],function(a,b,c){var d={PRIVATE:1,PUBLIC:2},e=function(a){"undefined"==typeof a.limit&&(a.limit=0),"undefined"==typeof a.offset&&(a.offset=0),"undefined"==typeof a.type&&(a.type=null),"undefined"==typeof a.favouritesonly&&(a.favouritesonly=!1),a.limitfrom=a.offset,a.limitnum=a.limit,delete a.limit,delete a.offset;var d={methodname:"core_message_data_for_messagearea_conversations",args:a},e=b.call([d])[0];return e.fail(c.exception),e},f=function(a){var d={methodname:"core_message_get_unread_conversations_count",args:a},e=b.call([d])[0];return e.fail(c.exception),e},g=function(a){var d={methodname:"core_message_mark_all_messages_as_read",args:a},e=b.call([d])[0];return e.fail(c.exception),e},h=function(a,c,d){var e={userid:a};"undefined"!=typeof c&&(e.limitnum=c),"undefined"!=typeof d&&(e.limitfrom=d);var f={methodname:"core_message_data_for_messagearea_contacts",args:e};return b.call([f])[0]},i=function(a,c){var d={methodname:"core_message_data_for_messagearea_get_profile",args:{currentuserid:a,otheruserid:c}};return b.call([d])[0]},j=function(c,d){var e=[{methodname:"core_message_block_user",args:{userid:c,blockeduserid:d}},{methodname:"core_message_get_member_info",args:{referenceuserid:c,userids:[d],includecontactrequests:!0,includeprivacyinfo:!0}}];return a.when.apply(null,b.call(e)).then(function(a,b){return b.length?b[0]:{}})},k=function(c,d){var e=[{methodname:"core_message_unblock_user",args:{userid:c,unblockeduserid:d}},{methodname:"core_message_get_member_info",args:{referenceuserid:c,userids:[d],includecontactrequests:!0,includeprivacyinfo:!0}}];return a.when.apply(null,b.call(e)).then(function(a,b){return b.length?b[0]:{}})},l=function(a,c){var d={methodname:"core_message_create_contact_request",args:{userid:a,requesteduserid:c}};return b.call([d])[0]},m=function(c,d){var e=[{methodname:"core_message_delete_contacts",args:{userid:c,userids:d}},{methodname:"core_message_get_member_info",args:{referenceuserid:c,userids:d,includecontactrequests:!0,includeprivacyinfo:!0}}];return a.when.apply(null,b.call(e)).then(function(a,b){return b})},n=function(a,c,d,e,f,g){var h={currentuserid:a,convid:c,newest:!!f};"undefined"!=typeof d&&(h.limitnum=d),"undefined"!=typeof e&&(h.limitfrom=e),"undefined"!=typeof g&&(h.timefrom=g);var i={methodname:"core_message_get_conversation_messages",args:h};return b.call([i])[0]},o=function(a,c,d,e){var f={userid:a,search:c};"undefined"!=typeof d&&(f.limitnum=d),"undefined"!=typeof e&&(f.limitfrom=e);var g={methodname:"core_message_message_search_users",args:f};return b.call([g])[0]},p=function(a,c,d,e){var f={userid:a,search:c};"undefined"!=typeof d&&(f.limitnum=d),"undefined"!=typeof e&&(f.limitfrom=e);var g={methodname:"core_message_data_for_messagearea_search_messages",args:f};return b.call([g])[0]},q=function(a,c){var d=c.map(function(b){return{touserid:a,text:b}}),e={methodname:"core_message_send_instant_messages",args:{messages:d}};return b.call([e])[0].then(function(a){var b=a.reduce(function(a,b){return b.errormessage&&a.push(b.errormessage),a},[]);if(b.length)throw new Error(b.join("\n"));return a}).then(function(a){return a.map(function(a){return{id:a.msgid,text:a.text,timecreated:a.timecreated,useridfrom:a.useridfrom,conversationid:a.conversationid}})})},r=function(a,b){return q(a,[b]).then(function(a){return a[0]})},s=function(a,c){var d=c.map(function(a){return{text:a}}),e={methodname:"core_message_send_messages_to_conversation",args:{conversationid:a,messages:d}};return b.call([e])[0]},t=function(a,b){return s(a,[b]).then(function(a){return a[0]})},u=function(a,c){var d={methodname:"core_user_update_user_preferences",args:{userid:a,preferences:c}};return b.call([d])[0]},v=function(a){var c={methodname:"core_user_get_user_preferences",args:{userid:a}};return b.call([c])[0]},w=function(a,c){return b.call(c.map(function(b){return{methodname:"core_message_delete_message",args:{messageid:b,userid:a}}}))},x=function(a,c){var d={methodname:"core_message_delete_conversation",args:{userid:a,otheruserid:c}};return b.call([d])[0]},y=function(a){var c={methodname:"core_message_get_contact_requests",args:{userid:a}};return b.call([c])[0]},z=function(c,d){var e=[{methodname:"core_message_confirm_contact_request",args:{userid:c,requesteduserid:d}},{methodname:"core_message_get_member_info",args:{referenceuserid:d,userids:[c],includecontactrequests:!0,includeprivacyinfo:!0}}];return a.when.apply(null,b.call(e)).then(function(a,b){return b.length?b[0]:{}})},A=function(c,d){var e=[{methodname:"core_message_decline_contact_request",args:{userid:c,requesteduserid:d}},{methodname:"core_message_get_member_info",args:{referenceuserid:d,userids:[c],includecontactrequests:!0,includeprivacyinfo:!0}}];return a.when.apply(null,b.call(e)).then(function(a,b){return b.length?b[0]:{}})},B=function(a,c,d,e,f,g,h,i,j){var k={userid:a,conversationid:c};"undefined"!=typeof d&&null!==d&&(k.includecontactrequests=d),"undefined"!=typeof e&&null!==e&&(k.includeprivacyinfo=e),"undefined"!=typeof f&&null!==f&&(k.memberlimit=f),"undefined"!=typeof g&&null!==g&&(k.memberoffset=g),"undefined"!=typeof h&&null!==h&&(k.messagelimit=h),"undefined"!=typeof i&&null!==i&&(k.messageoffset=i),"undefined"!=typeof j&&null!==j&&(k.newestmessagesfirst=j);var l={methodname:"core_message_get_conversation",args:k};return b.call([l])[0]},C=function(a,c,d,e,f,g,h,i,j){var k={userid:a,otheruserid:c};"undefined"!=typeof d&&null!==d&&(k.includecontactrequests=d),"undefined"!=typeof e&&null!==e&&(k.includeprivacyinfo=e),"undefined"!=typeof f&&null!==f&&(k.memberlimit=f),"undefined"!=typeof g&&null!==g&&(k.memberoffset=g),"undefined"!=typeof h&&null!==h&&(k.messagelimit=h),"undefined"!=typeof i&&null!==i&&(k.messageoffset=i),"undefined"!=typeof j&&null!==j&&(k.newestmessagesfirst=j);var l={methodname:"core_message_get_conversation_between_users",args:k};return b.call([l])[0]},D=function(a,c,e,f,g){var h={userid:a,type:c};"undefined"!=typeof e&&null!==e&&(h.limitnum=e),"undefined"!=typeof f&&null!==f&&(h.limitfrom=f),"undefined"!=typeof g&&null!==g&&(h.favourites=g);var i={methodname:"core_message_get_conversations",args:h};return b.call([i])[0].then(function(a){return a.conversations.length&&(a.conversations=a.conversations.map(function(a){if(a.type==d.PRIVATE){var b=a.members.length?a.members[0]:null;b&&(a.name=a.name?a.name:b.fullname,a.imageurl=a.imageurl?a.imageurl:b.profileimageurl)}return a})),a})},E=function(a,c,d,e,f){var g={userid:c,conversationid:a};"undefined"!=typeof d&&null!==d&&(g.limitnum=d),"undefined"!=typeof e&&null!==e&&(g.limitfrom=e),"undefined"!=typeof f&&null!==f&&(g.includecontactrequests=f);var h={methodname:"core_message_get_conversation_members",args:g};return b.call([h])[0]},F=function(a,c){var d={methodname:"core_message_set_favourite_conversations",args:{userid:a,conversations:c}};return b.call([d])[0]},G=function(a,c){var d={methodname:"core_message_unset_favourite_conversations",args:{userid:a,conversations:c}};return b.call([d])[0]},H=function(a,c,d,e){var f={referenceuserid:a,userids:c};"undefined"!=typeof d&&(f.includecontactrequests=d),"undefined"!=typeof e&&(f.includeprivacyinfo=e);var g={methodname:"core_message_get_member_info",args:f};return b.call([g])[0]},I=function(a,c){var d={methodname:"core_message_mark_all_conversation_messages_as_read",args:{userid:a,conversationid:c}};return b.call([d])[0]};return{query:e,countUnreadConversations:f,markAllAsRead:g,getContacts:h,getProfile:i,blockUser:j,unblockUser:k,createContactRequest:l,deleteContacts:m,getMessages:n,searchUsers:o,searchMessages:p,sendMessagesToUser:q,sendMessageToUser:r,sendMessagesToConversation:s,sendMessageToConversation:t,savePreferences:u,getPreferences:v,deleteMessages:w,deleteCoversation:x,getContactRequests:y,acceptContactRequest:z,declineContactRequest:A,getConversation:B,getConversationBetweenUsers:C,getConversations:D,getConversationMembers:E,setFavouriteConversations:F,unsetFavouriteConversations:G,getMemberInfo:H,markAllConversationMessagesAsRead:I}}); \ No newline at end of file diff --git a/message/amd/src/message_repository.js b/message/amd/src/message_repository.js index 6c0ee94f039..3eb4d470d37 100644 --- a/message/amd/src/message_repository.js +++ b/message/amd/src/message_repository.js @@ -23,6 +23,12 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) { + + var CONVERSATION_TYPES = { + PRIVATE: 1, + PUBLIC: 2 + }; + /** * Retrieve a list of messages from the server. * @@ -39,6 +45,14 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat args.offset = 0; } + if (typeof args.type === 'undefined') { + args.type = null; + } + + if (typeof args.favouritesonly === 'undefined') { + args.favouritesonly = false; + } + args.limitfrom = args.offset; args.limitnum = args.limit; @@ -96,9 +110,873 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat return promise; }; + /** + * Get contacts for given user. + * + * @param {int} userId The user id + * @param {int} limit Limit for results + * @param {int} offset Offset for results + * @return {object} jQuery promise + */ + var getContacts = function(userId, limit, offset) { + var args = { + userid: userId + }; + + if (typeof limit !== 'undefined') { + args.limitnum = limit; + } + + if (typeof offset !== 'undefined') { + args.limitfrom = offset; + } + + var request = { + methodname: 'core_message_data_for_messagearea_contacts', + args: args + }; + + return Ajax.call([request])[0]; + }; + + /** + * Request profile information as a user for a given user. + * + * @param {int} userId The requesting user + * @param {int} profileUserId The id of the user who's profile is being requested + * @return {object} jQuery promise + */ + var getProfile = function(userId, profileUserId) { + var request = { + methodname: 'core_message_data_for_messagearea_get_profile', + args: { + currentuserid: userId, + otheruserid: profileUserId + } + }; + + return Ajax.call([request])[0]; + }; + + /** + * Block a user. + * + * @param {int} userId The requesting user + * @param {int} blockedUserId Id of user to block + * @return {object} jQuery promise + */ + var blockUser = function(userId, blockedUserId) { + var requests = [ + { + methodname: 'core_message_block_user', + args: { + userid: userId, + blockeduserid: blockedUserId + } + }, + { + methodname: 'core_message_get_member_info', + args: { + referenceuserid: userId, + userids: [blockedUserId], + includecontactrequests: true, + includeprivacyinfo: true + } + } + ]; + + // Wrap both requests in a single promise so that we can catch an error + // from either request. + return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) { + // Only return the profile. + return profiles.length ? profiles[0] : {}; + }); + }; + + /** + * Unblock a user. + * + * @param {int} userId The requesting user + * @param {int} unblockedUserId Id of user to unblock + * @return {object} jQuery promise + */ + var unblockUser = function(userId, unblockedUserId) { + var requests = [ + { + methodname: 'core_message_unblock_user', + args: { + userid: userId, + unblockeduserid: unblockedUserId + } + }, + { + methodname: 'core_message_get_member_info', + args: { + referenceuserid: userId, + userids: [unblockedUserId], + includecontactrequests: true, + includeprivacyinfo: true + } + } + ]; + + // Wrap both requests in a single promise so that we can catch an error + // from either request. + return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) { + // Only return the profile. + return profiles.length ? profiles[0] : {}; + }); + }; + + /** + * Create a request to add a user as a contact. + * + * @param {int} userId The requesting user + * @param {int[]} requestUserIds List of user ids to add + * @return {object} jQuery promise + */ + var createContactRequest = function(userId, requestUserIds) { + var request = { + methodname: 'core_message_create_contact_request', + args: { + userid: userId, + requesteduserid: requestUserIds + } + }; + + return Ajax.call([request])[0]; + }; + + /** + * Remove a list of users as contacts. + * + * @param {int} userId The requesting user + * @param {int[]} contactUserIds List of user ids to add + * @return {object} jQuery promise + */ + var deleteContacts = function(userId, contactUserIds) { + var requests = [ + { + methodname: 'core_message_delete_contacts', + args: { + userid: userId, + userids: contactUserIds + } + }, + { + methodname: 'core_message_get_member_info', + args: { + referenceuserid: userId, + userids: contactUserIds, + includecontactrequests: true, + includeprivacyinfo: true + } + } + ]; + + return $.when.apply(null, Ajax.call(requests)).then(function(response1, profiles) { + // Return all of the profiles as an array. + return profiles; + }); + }; + + /** + * Get messages between two users. + * + * @param {int} currentUserId The requesting user + * @param {int} conversationId Other user in the conversation + * @param {int} limit Limit for results + * @param {int} offset Offset for results + * @param {bool} newestFirst Order results by newest first + * @param {int} timeFrom Only return messages after this timestamp + * @return {object} jQuery promise + */ + var getMessages = function(currentUserId, conversationId, limit, offset, newestFirst, timeFrom) { + var args = { + currentuserid: currentUserId, + convid: conversationId, + newest: newestFirst ? true : false + }; + + if (typeof limit !== 'undefined') { + args.limitnum = limit; + } + + if (typeof offset !== 'undefined') { + args.limitfrom = offset; + } + + if (typeof timeFrom !== 'undefined') { + args.timefrom = timeFrom; + } + + var request = { + methodname: 'core_message_get_conversation_messages', + args: args + }; + return Ajax.call([request])[0]; + }; + + /** + * Search for users. + * + * @param {int} userId The requesting user + * @param {string} searchString Search string + * @param {int} limit Limit for results + * @param {int} offset Offset for results + * @return {object} jQuery promise + */ + var searchUsers = function(userId, searchString, limit, offset) { + var args = { + userid: userId, + search: searchString + }; + + if (typeof limit !== 'undefined') { + args.limitnum = limit; + } + + if (typeof offset !== 'undefined') { + args.limitfrom = offset; + } + + var request = { + methodname: 'core_message_message_search_users', + args: args + }; + + return Ajax.call([request])[0]; + }; + + /** + * Search for messages. + * + * @param {int} userId The requesting user + * @param {string} searchString Search string + * @param {int} limit Limit for results + * @param {int} offset Offset for results + * @return {object} jQuery promise + */ + var searchMessages = function(userId, searchString, limit, offset) { + var args = { + userid: userId, + search: searchString + }; + + if (typeof limit !== 'undefined') { + args.limitnum = limit; + } + + if (typeof offset !== 'undefined') { + args.limitfrom = offset; + } + + var request = { + methodname: 'core_message_data_for_messagearea_search_messages', + args: args + }; + + return Ajax.call([request])[0]; + }; + + /** + * Send a list of messages to a user. + * + * @param {int} toUserId The recipient user id + * @param {string[]} messages List of messages to send + * @return {object} jQuery promise + */ + var sendMessagesToUser = function(toUserId, messages) { + var formattedMessages = messages.map(function(message) { + return { + touserid: toUserId, + text: message + }; + }); + var request = { + methodname: 'core_message_send_instant_messages', + args: { + messages: formattedMessages + } + }; + + return Ajax.call([request])[0] + .then(function(results) { + // Error handling for the weird way the old function works. + var errors = results.reduce(function(carry, result) { + if (result.errormessage) { + carry.push(result.errormessage); + } + + return carry; + }, []); + if (errors.length) { + throw new Error(errors.join("\n")); + } + + return results; + }) + .then(function(results) { + // Format the results to match the other send message function. + return results.map(function(result) { + return { + id: result.msgid, + text: result.text, + timecreated: result.timecreated, + useridfrom: result.useridfrom, + conversationid: result.conversationid + }; + }); + }); + }; + + /** + * Send a single message to a user. + * + * @param {int} toUserId The recipient user id + * @param {string} text The message text + * @return {object} jQuery promise + */ + var sendMessageToUser = function(toUserId, text) { + return sendMessagesToUser(toUserId, [text]) + .then(function(results) { + return results[0]; + }); + }; + + /** + * Send messages to a conversation. + */ + var sendMessagesToConversation = function(conversationId, messages) { + var formattedMessages = messages.map(function(message) { + return { + text: message + }; + }); + var request = { + methodname: 'core_message_send_messages_to_conversation', + args: { + conversationid: conversationId, + messages: formattedMessages + } + }; + + return Ajax.call([request])[0]; + }; + + /** + * Send a message to a conversation. + */ + var sendMessageToConversation = function(conversationId, text) { + return sendMessagesToConversation(conversationId, [text]) + .then(function(result) { + return result[0]; + }); + }; + + /** + * Save message preferences. + * + * @param {int} userId The owner of the preferences + * @param {object[]} preferences New preferences values + * @return {object} jQuery promise + */ + var savePreferences = function(userId, preferences) { + var request = { + methodname: 'core_user_update_user_preferences', + args: { + userid: userId, + preferences: preferences + } + }; + return Ajax.call([request])[0]; + }; + + /** + * Get the user's preferences. + * + * @param {int} userId The target user + * @return {object} jQuery promise + */ + var getPreferences = function(userId) { + var request = { + methodname: 'core_user_get_user_preferences', + args: { + userid: userId + } + }; + return Ajax.call([request])[0]; + }; + + /** + * Delete a list of messages. + * + * @param {int} userId The user to delete messages for + * @param {int[]} messageIds List of message ids to delete + * @return {object} jQuery promise + */ + var deleteMessages = function(userId, messageIds) { + return Ajax.call(messageIds.map(function(messageId) { + return { + methodname: 'core_message_delete_message', + args: { + messageid: messageId, + userid: userId + } + }; + })); + }; + + /** + * Delete a conversation between two users. + * + * @param {int} userId The user to delete messages for + * @param {int} otherUserId The other member of the conversation + * @return {object} jQuery promise + */ + var deleteCoversation = function(userId, otherUserId) { + var request = { + methodname: 'core_message_delete_conversation', + args: { + userid: userId, + otheruserid: otherUserId + } + }; + return Ajax.call([request])[0]; + }; + + /** + * Get the list of contact requests for a user. + * + * @param {int} userId The user id + * @return {object} jQuery promise + */ + var getContactRequests = function(userId) { + var request = { + methodname: 'core_message_get_contact_requests', + args: { + userid: userId + } + }; + return Ajax.call([request])[0]; + }; + + /** + * Accept a contact request. + * + * @param {int} sendingUserId The user that sent the request + * @param {int} recipientUserId The user that received the request + * @return {object} jQuery promise + */ + var acceptContactRequest = function(sendingUserId, recipientUserId) { + var requests = [ + { + methodname: 'core_message_confirm_contact_request', + args: { + userid: sendingUserId, + requesteduserid: recipientUserId + } + }, + { + methodname: 'core_message_get_member_info', + args: { + referenceuserid: recipientUserId, + userids: [sendingUserId], + includecontactrequests: true, + includeprivacyinfo: true + } + } + ]; + + // Wrap both requests in a single promise so that we can catch an error + // from either request. + return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) { + // Only return the profile. + return profiles.length ? profiles[0] : {}; + }); + }; + + /** + * Decline a contact request. + * + * @param {int} sendingUserId The user that sent the request + * @param {int} recipientUserId The user that received the request + * @return {object} jQuery promise + */ + var declineContactRequest = function(sendingUserId, recipientUserId) { + var requests = [ + { + methodname: 'core_message_decline_contact_request', + args: { + userid: sendingUserId, + requesteduserid: recipientUserId + } + }, + { + methodname: 'core_message_get_member_info', + args: { + referenceuserid: recipientUserId, + userids: [sendingUserId], + includecontactrequests: true, + includeprivacyinfo: true + } + } + ]; + + // Wrap both requests in a single promise so that we can catch an error + // from either request. + return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) { + // Only return the profile. + return profiles.length ? profiles[0] : {}; + }); + }; + + /** + * Get a conversation. + * + * @param {int} loggedInUserId The logged in user + * @param {int} conversationId The conversation id + * @param {bool} includeContactRequests Incldue contact requests between members + * @param {bool} includePrivacyInfo Include privacy info for members + * @param {int} memberLimit Limit for members + * @param {int} memberOffset Offset for members + * @param {int} messageLimit Limit for messages + * @param {int} messageOffset Offset for messages + * @param {bool} newestMessagesFirst Order the messages by newest first + * @return {object} jQuery promise + */ + var getConversation = function( + loggedInUserId, + conversationId, + includeContactRequests, + includePrivacyInfo, + memberLimit, + memberOffset, + messageLimit, + messageOffset, + newestMessagesFirst + ) { + var args = { + userid: loggedInUserId, + conversationid: conversationId + }; + + if (typeof includeContactRequests != 'undefined' && includeContactRequests !== null) { + args.includecontactrequests = includeContactRequests; + } + + if (typeof includePrivacyInfo != 'undefined' && includePrivacyInfo !== null) { + args.includeprivacyinfo = includePrivacyInfo; + } + + if (typeof memberLimit != 'undefined' && memberLimit !== null) { + args.memberlimit = memberLimit; + } + + if (typeof memberOffset != 'undefined' && memberOffset !== null) { + args.memberoffset = memberOffset; + } + + if (typeof messageLimit != 'undefined' && messageLimit !== null) { + args.messagelimit = messageLimit; + } + + if (typeof messageOffset != 'undefined' && messageOffset !== null) { + args.messageoffset = messageOffset; + } + + if (typeof newestMessagesFirst != 'undefined' && newestMessagesFirst !== null) { + args.newestmessagesfirst = newestMessagesFirst; + } + + var request = { + methodname: 'core_message_get_conversation', + args: args + }; + + return Ajax.call([request])[0]; + }; + + /** + * Get a conversation between users. + * + * @param {int} loggedInUserId The logged in user + * @param {int} otherUserId The other user id + * @param {bool} includeContactRequests Incldue contact requests between members + * @param {bool} includePrivacyInfo Include privacy info for members + * @param {int} memberLimit Limit for members + * @param {int} memberOffset Offset for members + * @param {int} messageLimit Limit for messages + * @param {int} messageOffset Offset for messages + * @param {bool} newestMessagesFirst Order the messages by newest first + * @return {object} jQuery promise + */ + var getConversationBetweenUsers = function( + loggedInUserId, + otherUserId, + includeContactRequests, + includePrivacyInfo, + memberLimit, + memberOffset, + messageLimit, + messageOffset, + newestMessagesFirst + ) { + var args = { + userid: loggedInUserId, + otheruserid: otherUserId + }; + + if (typeof includeContactRequests != 'undefined' && includeContactRequests !== null) { + args.includecontactrequests = includeContactRequests; + } + + if (typeof includePrivacyInfo != 'undefined' && includePrivacyInfo !== null) { + args.includeprivacyinfo = includePrivacyInfo; + } + + if (typeof memberLimit != 'undefined' && memberLimit !== null) { + args.memberlimit = memberLimit; + } + + if (typeof memberOffset != 'undefined' && memberOffset !== null) { + args.memberoffset = memberOffset; + } + + if (typeof messageLimit != 'undefined' && messageLimit !== null) { + args.messagelimit = messageLimit; + } + + if (typeof messageOffset != 'undefined' && messageOffset !== null) { + args.messageoffset = messageOffset; + } + + if (typeof newestMessagesFirst != 'undefined' && newestMessagesFirst !== null) { + args.newestmessagesfirst = newestMessagesFirst; + } + + var request = { + methodname: 'core_message_get_conversation_between_users', + args: args + }; + + return Ajax.call([request])[0]; + }; + + /** + * Get the conversations for a user. + * + * @param {int} userId The logged in user + * @param {int|null} type The type of conversation to get + * @param {int} limit Limit for results + * @param {int} offset Offset for results + * @param {bool|null} favourites If favourites should be included or not + * @return {object} jQuery promise + */ + var getConversations = function( + userId, + type, + limit, + offset, + favourites + ) { + var args = { + userid: userId, + type: type + }; + + if (typeof limit != 'undefined' && limit !== null) { + args.limitnum = limit; + } + + if (typeof offset != 'undefined' && offset !== null) { + args.limitfrom = offset; + } + + if (typeof favourites != 'undefined' && favourites !== null) { + args.favourites = favourites; + } + + var request = { + methodname: 'core_message_get_conversations', + args: args + }; + + return Ajax.call([request])[0] + .then(function(result) { + if (result.conversations.length) { + result.conversations = result.conversations.map(function(conversation) { + if (conversation.type == CONVERSATION_TYPES.PRIVATE) { + var otherUser = conversation.members.length ? conversation.members[0] : null; + + if (otherUser) { + conversation.name = conversation.name ? conversation.name : otherUser.fullname; + conversation.imageurl = conversation.imageurl ? conversation.imageurl : otherUser.profileimageurl; + } + } + + return conversation; + }); + } + + return result; + }); + }; + + /** + * Get the conversations for a user. + * + * @param {int} conversationId The conversation id + * @param {int} loggedInUserId The logged in user + * @param {int} limit Limit for results + * @param {int} offset Offset for results + * @param {bool} includeContactRequests If contact requests should be included in result + * @return {object} jQuery promise + */ + var getConversationMembers = function(conversationId, loggedInUserId, limit, offset, includeContactRequests) { + var args = { + userid: loggedInUserId, + conversationid: conversationId + }; + + if (typeof limit != 'undefined' && limit !== null) { + args.limitnum = limit; + } + + if (typeof offset != 'undefined' && offset !== null) { + args.limitfrom = offset; + } + + if (typeof includeContactRequests != 'undefined' && includeContactRequests !== null) { + args.includecontactrequests = includeContactRequests; + } + + var request = { + methodname: 'core_message_get_conversation_members', + args: args + }; + + return Ajax.call([request])[0]; + }; + + /** + * Set a list of conversations to set as favourites for the given user. + * + * @param {int} userId The user id + * @param {array} conversationIds List of conversation ids to set as favourite + * @return {object} jQuery promise + */ + var setFavouriteConversations = function(userId, conversationIds) { + + var request = { + methodname: 'core_message_set_favourite_conversations', + args: { + userid: userId, + conversations: conversationIds + } + }; + return Ajax.call([request])[0]; + }; + + /** + * Set a list of conversations to unset as favourites for the given user. + * + * @param {int} userId The user id + * @param {array} conversationIds List of conversation ids to unset as favourite + * @return {object} jQuery promise + */ + var unsetFavouriteConversations = function(userId, conversationIds) { + + var request = { + methodname: 'core_message_unset_favourite_conversations', + args: { + userid: userId, + conversations: conversationIds + } + }; + return Ajax.call([request])[0]; + }; + + /** + * Get a list of user's member info. + * + * @param {int} referenceUserId The user id + * @param {array} userIds List of user ids to get + * @param {bool} includeContactRequests Include contact requests between users in response + * @param {bool} includePrivacyInfo Include privacy info for reference user in response + * @return {object} jQuery promise + */ + var getMemberInfo = function(referenceUserId, userIds, includeContactRequests, includePrivacyInfo) { + var args = { + referenceuserid: referenceUserId, + userids: userIds + }; + + if (typeof includeContactRequests != 'undefined') { + args.includecontactrequests = includeContactRequests; + } + + if (typeof includePrivacyInfo != 'undefined') { + args.includeprivacyinfo = includePrivacyInfo; + } + + var request = { + methodname: 'core_message_get_member_info', + args: args + }; + return Ajax.call([request])[0]; + }; + + /** + * Get a list of user's member info. + * + * @param {int} userId The user id to mark as read for + * @param {int} conversationId The conversation to mark as read + * @return {object} jQuery promise + */ + var markAllConversationMessagesAsRead = function(userId, conversationId) { + + var request = { + methodname: 'core_message_mark_all_conversation_messages_as_read', + args: { + userid: userId, + conversationid: conversationId + } + }; + return Ajax.call([request])[0]; + }; + return { query: query, countUnreadConversations: countUnreadConversations, markAllAsRead: markAllAsRead, + getContacts: getContacts, + getProfile: getProfile, + blockUser: blockUser, + unblockUser: unblockUser, + createContactRequest: createContactRequest, + deleteContacts: deleteContacts, + getMessages: getMessages, + searchUsers: searchUsers, + searchMessages: searchMessages, + sendMessagesToUser: sendMessagesToUser, + sendMessageToUser: sendMessageToUser, + sendMessagesToConversation: sendMessagesToConversation, + sendMessageToConversation: sendMessageToConversation, + savePreferences: savePreferences, + getPreferences: getPreferences, + deleteMessages: deleteMessages, + deleteCoversation: deleteCoversation, + getContactRequests: getContactRequests, + acceptContactRequest: acceptContactRequest, + declineContactRequest: declineContactRequest, + getConversation: getConversation, + getConversationBetweenUsers: getConversationBetweenUsers, + getConversations: getConversations, + getConversationMembers: getConversationMembers, + setFavouriteConversations: setFavouriteConversations, + unsetFavouriteConversations: unsetFavouriteConversations, + getMemberInfo: getMemberInfo, + markAllConversationMessagesAsRead: markAllConversationMessagesAsRead }; }); From 5005d8cfb43b73dd7481e5db9445f1be060b1276 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 5 Nov 2018 14:17:18 +0800 Subject: [PATCH 21/31] MDL-63303 message: add message drawer (boost only) --- lang/en/message.php | 52 + lang/en/moodle.php | 2 + lib/amd/build/key_codes.min.js | 2 +- lib/amd/src/key_codes.js | 3 + lib/db/services.php | 1 + lib/templates/loading.mustache | 2 +- message/amd/build/message_drawer.min.js | 1 + .../amd/build/message_drawer_events.min.js | 1 + .../amd/build/message_drawer_helper.min.js | 1 + .../message_drawer_lazy_load_list.min.js | 1 + .../amd/build/message_drawer_router.min.js | 1 + .../amd/build/message_drawer_routes.min.js | 1 + .../build/message_drawer_view_contact.min.js | 1 + .../build/message_drawer_view_contacts.min.js | 1 + ...essage_drawer_view_contacts_section.min.js | 1 + ...awer_view_contacts_section_contacts.min.js | 1 + ...awer_view_contacts_section_requests.min.js | 1 + .../message_drawer_view_conversation.min.js | 1 + ..._drawer_view_conversation_constants.min.js | 1 + ...ge_drawer_view_conversation_patcher.min.js | 1 + ...e_drawer_view_conversation_renderer.min.js | 1 + ...wer_view_conversation_state_manager.min.js | 1 + .../message_drawer_view_group_info.min.js | 1 + .../build/message_drawer_view_overview.min.js | 1 + ...essage_drawer_view_overview_section.min.js | 1 + ...er_view_overview_section_favourites.min.js | 1 + ...iew_overview_section_group_messages.min.js | 1 + ...awer_view_overview_section_messages.min.js | 1 + .../build/message_drawer_view_search.min.js | 1 + .../build/message_drawer_view_settings.min.js | 1 + message/amd/src/message_drawer.js | 243 +++ message/amd/src/message_drawer_events.js | 45 + message/amd/src/message_drawer_helper.js | 62 + .../amd/src/message_drawer_lazy_load_list.js | 327 ++++ message/amd/src/message_drawer_router.js | 225 +++ message/amd/src/message_drawer_routes.js | 33 + .../amd/src/message_drawer_view_contact.js | 97 + .../amd/src/message_drawer_view_contacts.js | 163 ++ .../message_drawer_view_contacts_section.js | 317 ++++ ...e_drawer_view_contacts_section_contacts.js | 200 +++ ...e_drawer_view_contacts_section_requests.js | 139 ++ .../src/message_drawer_view_conversation.js | 1564 +++++++++++++++++ ...sage_drawer_view_conversation_constants.js | 106 ++ ...essage_drawer_view_conversation_patcher.js | 1160 ++++++++++++ ...ssage_drawer_view_conversation_renderer.js | 1518 ++++++++++++++++ ..._drawer_view_conversation_state_manager.js | 651 +++++++ .../amd/src/message_drawer_view_group_info.js | 170 ++ .../amd/src/message_drawer_view_overview.js | 145 ++ .../message_drawer_view_overview_section.js | 529 ++++++ ...drawer_view_overview_section_favourites.js | 48 + ...er_view_overview_section_group_messages.js | 49 + ...e_drawer_view_overview_section_messages.js | 48 + message/amd/src/message_drawer_view_search.js | 837 +++++++++ .../amd/src/message_drawer_view_settings.js | 142 ++ message/lib.php | 106 ++ message/templates/message_drawer.mustache | 70 + .../message_drawer_contacts_list.mustache | 71 + ...er_contacts_list_item_placeholder.mustache | 48 + ...message_drawer_conversations_list.mustache | 90 + ...nversations_list_item_placeholder.mustache | 65 + .../message_drawer_icon_back.mustache | 37 + .../message_drawer_icon_forward.mustache | 37 + .../message_drawer_lazy_load_list.mustache | 58 + .../message_drawer_messages_list.mustache | 97 + ...er_messages_list_item_placeholder.mustache | 65 + .../message_drawer_non_contacts_list.mustache | 65 + .../message_drawer_view_contact_body.mustache | 44 + ..._drawer_view_contact_body_content.mustache | 95 + ...message_drawer_view_contacts_body.mustache | 96 + ...ew_contacts_body_section_contacts.mustache | 48 + ...ew_contacts_body_section_requests.mustache | 81 + ...ntacts_body_section_requests_list.mustache | 71 + ...ssage_drawer_view_contacts_header.mustache | 53 + ...sage_drawer_view_contacts_section.mustache | 56 + ...age_drawer_view_conversation_body.mustache | 60 + ...onversation_body_confirm_dialogue.mustache | 78 + ...drawer_view_conversation_body_day.mustache | 40 + ...conversation_body_day_placeholder.mustache | 83 + ...er_view_conversation_body_message.mustache | 70 + ...r_view_conversation_body_messages.mustache | 41 + ...iew_conversation_body_placeholder.mustache | 48 + ...e_drawer_view_conversation_footer.mustache | 66 + ..._view_conversation_footer_content.mustache | 59 + ...iew_conversation_footer_edit_mode.mustache | 50 + ...w_conversation_footer_placeholder.mustache | 40 + ...nversation_footer_require_contact.mustache | 45 + ...nversation_footer_require_unblock.mustache | 44 + ...ersation_footer_unable_to_message.mustache | 40 + ...e_drawer_view_conversation_header.mustache | 54 + ...ation_header_content_type_private.mustache | 104 ++ ..._content_type_private_no_controls.mustache | 71 + ...sation_header_content_type_public.mustache | 87 + ...iew_conversation_header_edit_mode.mustache | 44 + ...w_conversation_header_placeholder.mustache | 60 + ...ssage_drawer_view_group_info_body.mustache | 48 + ...awer_view_group_info_body_content.mustache | 70 + ...view_group_info_participants_list.mustache | 71 + ...articipants_list_item_placeholder.mustache | 48 + ...message_drawer_view_overview_body.mustache | 48 + ...ssage_drawer_view_overview_header.mustache | 71 + ...sage_drawer_view_overview_section.mustache | 73 + ..._view_overview_section_favourites.mustache | 49 + ...w_overview_section_group_messages.mustache | 49 + ...er_view_overview_section_messages.mustache | 49 + .../message_drawer_view_search_body.mustache | 55 + ...message_drawer_view_search_header.mustache | 71 + ...rawer_view_search_results_content.mustache | 76 + ...earch_results_content_placeholder.mustache | 37 + ...message_drawer_view_settings_body.mustache | 80 + ...ssage_drawer_view_settings_header.mustache | 48 + theme/boost/scss/moodle/core.scss | 11 +- theme/boost/scss/moodle/message.scss | 183 ++ theme/boost/style/moodle.css | 150 +- 113 files changed, 12711 insertions(+), 17 deletions(-) create mode 100644 message/amd/build/message_drawer.min.js create mode 100644 message/amd/build/message_drawer_events.min.js create mode 100644 message/amd/build/message_drawer_helper.min.js create mode 100644 message/amd/build/message_drawer_lazy_load_list.min.js create mode 100644 message/amd/build/message_drawer_router.min.js create mode 100644 message/amd/build/message_drawer_routes.min.js create mode 100644 message/amd/build/message_drawer_view_contact.min.js create mode 100644 message/amd/build/message_drawer_view_contacts.min.js create mode 100644 message/amd/build/message_drawer_view_contacts_section.min.js create mode 100644 message/amd/build/message_drawer_view_contacts_section_contacts.min.js create mode 100644 message/amd/build/message_drawer_view_contacts_section_requests.min.js create mode 100644 message/amd/build/message_drawer_view_conversation.min.js create mode 100644 message/amd/build/message_drawer_view_conversation_constants.min.js create mode 100644 message/amd/build/message_drawer_view_conversation_patcher.min.js create mode 100644 message/amd/build/message_drawer_view_conversation_renderer.min.js create mode 100644 message/amd/build/message_drawer_view_conversation_state_manager.min.js create mode 100644 message/amd/build/message_drawer_view_group_info.min.js create mode 100644 message/amd/build/message_drawer_view_overview.min.js create mode 100644 message/amd/build/message_drawer_view_overview_section.min.js create mode 100644 message/amd/build/message_drawer_view_overview_section_favourites.min.js create mode 100644 message/amd/build/message_drawer_view_overview_section_group_messages.min.js create mode 100644 message/amd/build/message_drawer_view_overview_section_messages.min.js create mode 100644 message/amd/build/message_drawer_view_search.min.js create mode 100644 message/amd/build/message_drawer_view_settings.min.js create mode 100644 message/amd/src/message_drawer.js create mode 100644 message/amd/src/message_drawer_events.js create mode 100644 message/amd/src/message_drawer_helper.js create mode 100644 message/amd/src/message_drawer_lazy_load_list.js create mode 100644 message/amd/src/message_drawer_router.js create mode 100644 message/amd/src/message_drawer_routes.js create mode 100644 message/amd/src/message_drawer_view_contact.js create mode 100644 message/amd/src/message_drawer_view_contacts.js create mode 100644 message/amd/src/message_drawer_view_contacts_section.js create mode 100644 message/amd/src/message_drawer_view_contacts_section_contacts.js create mode 100644 message/amd/src/message_drawer_view_contacts_section_requests.js create mode 100644 message/amd/src/message_drawer_view_conversation.js create mode 100644 message/amd/src/message_drawer_view_conversation_constants.js create mode 100644 message/amd/src/message_drawer_view_conversation_patcher.js create mode 100644 message/amd/src/message_drawer_view_conversation_renderer.js create mode 100644 message/amd/src/message_drawer_view_conversation_state_manager.js create mode 100644 message/amd/src/message_drawer_view_group_info.js create mode 100644 message/amd/src/message_drawer_view_overview.js create mode 100644 message/amd/src/message_drawer_view_overview_section.js create mode 100644 message/amd/src/message_drawer_view_overview_section_favourites.js create mode 100644 message/amd/src/message_drawer_view_overview_section_group_messages.js create mode 100644 message/amd/src/message_drawer_view_overview_section_messages.js create mode 100644 message/amd/src/message_drawer_view_search.js create mode 100644 message/amd/src/message_drawer_view_settings.js create mode 100644 message/templates/message_drawer.mustache create mode 100644 message/templates/message_drawer_contacts_list.mustache create mode 100644 message/templates/message_drawer_contacts_list_item_placeholder.mustache create mode 100644 message/templates/message_drawer_conversations_list.mustache create mode 100644 message/templates/message_drawer_conversations_list_item_placeholder.mustache create mode 100644 message/templates/message_drawer_icon_back.mustache create mode 100644 message/templates/message_drawer_icon_forward.mustache create mode 100644 message/templates/message_drawer_lazy_load_list.mustache create mode 100644 message/templates/message_drawer_messages_list.mustache create mode 100644 message/templates/message_drawer_messages_list_item_placeholder.mustache create mode 100644 message/templates/message_drawer_non_contacts_list.mustache create mode 100644 message/templates/message_drawer_view_contact_body.mustache create mode 100644 message/templates/message_drawer_view_contact_body_content.mustache create mode 100644 message/templates/message_drawer_view_contacts_body.mustache create mode 100644 message/templates/message_drawer_view_contacts_body_section_contacts.mustache create mode 100644 message/templates/message_drawer_view_contacts_body_section_requests.mustache create mode 100644 message/templates/message_drawer_view_contacts_body_section_requests_list.mustache create mode 100644 message/templates/message_drawer_view_contacts_header.mustache create mode 100644 message/templates/message_drawer_view_contacts_section.mustache create mode 100644 message/templates/message_drawer_view_conversation_body.mustache create mode 100644 message/templates/message_drawer_view_conversation_body_confirm_dialogue.mustache create mode 100644 message/templates/message_drawer_view_conversation_body_day.mustache create mode 100644 message/templates/message_drawer_view_conversation_body_day_placeholder.mustache create mode 100644 message/templates/message_drawer_view_conversation_body_message.mustache create mode 100644 message/templates/message_drawer_view_conversation_body_messages.mustache create mode 100644 message/templates/message_drawer_view_conversation_body_placeholder.mustache create mode 100644 message/templates/message_drawer_view_conversation_footer.mustache create mode 100644 message/templates/message_drawer_view_conversation_footer_content.mustache create mode 100644 message/templates/message_drawer_view_conversation_footer_edit_mode.mustache create mode 100644 message/templates/message_drawer_view_conversation_footer_placeholder.mustache create mode 100644 message/templates/message_drawer_view_conversation_footer_require_contact.mustache create mode 100644 message/templates/message_drawer_view_conversation_footer_require_unblock.mustache create mode 100644 message/templates/message_drawer_view_conversation_footer_unable_to_message.mustache create mode 100644 message/templates/message_drawer_view_conversation_header.mustache create mode 100644 message/templates/message_drawer_view_conversation_header_content_type_private.mustache create mode 100644 message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache create mode 100644 message/templates/message_drawer_view_conversation_header_content_type_public.mustache create mode 100644 message/templates/message_drawer_view_conversation_header_edit_mode.mustache create mode 100644 message/templates/message_drawer_view_conversation_header_placeholder.mustache create mode 100644 message/templates/message_drawer_view_group_info_body.mustache create mode 100644 message/templates/message_drawer_view_group_info_body_content.mustache create mode 100644 message/templates/message_drawer_view_group_info_participants_list.mustache create mode 100644 message/templates/message_drawer_view_group_info_participants_list_item_placeholder.mustache create mode 100644 message/templates/message_drawer_view_overview_body.mustache create mode 100644 message/templates/message_drawer_view_overview_header.mustache create mode 100644 message/templates/message_drawer_view_overview_section.mustache create mode 100644 message/templates/message_drawer_view_overview_section_favourites.mustache create mode 100644 message/templates/message_drawer_view_overview_section_group_messages.mustache create mode 100644 message/templates/message_drawer_view_overview_section_messages.mustache create mode 100644 message/templates/message_drawer_view_search_body.mustache create mode 100644 message/templates/message_drawer_view_search_header.mustache create mode 100644 message/templates/message_drawer_view_search_results_content.mustache create mode 100644 message/templates/message_drawer_view_search_results_content_placeholder.mustache create mode 100644 message/templates/message_drawer_view_settings_body.mustache create mode 100644 message/templates/message_drawer_view_settings_header.mustache diff --git a/lang/en/message.php b/lang/en/message.php index 1c1f5baf55e..66798146dba 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -22,13 +22,20 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['acceptandaddcontact'] = 'Accept and add to contacts'; $string['addcontact'] = 'Add contact'; +$string['addcontactconfirm'] = 'Are you sure you want to add {$a} to your contacts?'; $string['addtoyourcontacts'] = 'Add to your contacts'; +$string['addtoyourcontactsandmessage'] = 'Add to your contacts and message'; +$string['addtofavourites'] = 'Add to favourites'; $string['ago'] = '{$a} ago'; $string['allusers'] = 'All messages from all users'; +$string['backto'] = 'Back to {$a}'; $string['backupmessageshelp'] = 'If enabled, then instant messages will be included in SITE automated backups'; $string['blockcontact'] = 'Block contact'; $string['blockedusers'] = 'Blocked users'; +$string['blockuser'] = 'Block user'; +$string['blockuserconfirm'] = 'Are you sure you want to block {$a}?'; $string['blocknoncontacts'] = 'Prevent non-contacts from messaging me'; $string['canceledit'] = 'Cancel editing messages'; $string['contactableprivacy'] = 'Accept messages from:'; @@ -37,12 +44,16 @@ $string['contactableprivacy_coursemember'] = 'My contacts and anyone in my cours $string['contactableprivacy_site'] = 'Anyone on the site'; $string['contactblocked'] = 'Contact blocked'; $string['contactrequests'] = 'Contact requests'; +$string['contactrequestsent'] = 'Contact request sent'; $string['contacts'] = 'Contacts'; +$string['decline'] = 'Decline'; $string['defaultmessageoutputs'] = 'Default message outputs'; $string['defaults'] = 'Defaults'; $string['deleteallconfirm'] = "Are you sure you would like to delete this entire conversation?"; $string['deleteallmessages'] = "Delete all messages"; +$string['deleteconversation'] = "Delete conversation"; $string['deleteselectedmessages'] = 'Delete selected messages'; +$string['deleteselectedmessagesconfirm'] = 'Are you sure you would like to delete the selected messages?'; $string['disableall'] = 'Disable notifications'; $string['disabled'] = 'Messaging is disabled on this site'; $string['disallowed'] = 'Disallowed'; @@ -64,8 +75,13 @@ $string['eventmessagesent'] = 'Message sent'; $string['forced'] = 'Locked'; $string['guestnoeditmessage'] = 'Guest user can not edit messaging options'; $string['guestnoeditmessageother'] = 'Guest user can not edit other user messaging options'; +$string['groupinfo'] = 'Group info'; +$string['groupmessages'] = 'Group messages'; $string['hidemessagewindow'] = 'Hide message window'; $string['hidenotificationwindow'] = 'Hide notification window'; +$string['info'] = 'Info'; +$string['isnotinyourcontacts'] = '{$a} is not in your contacts'; +$string['loadmore'] = 'Load more'; $string['loggedin'] = 'Online'; $string['loggedin_help'] = 'Configure how you would like to receive notifications when you are logged into Moodle'; $string['loggedindescription'] = 'When you are logged into Moodle'; @@ -78,24 +94,40 @@ $string['messagepreferences'] = 'Message preferences'; $string['message'] = 'Message'; $string['messagecontactrequestsnotification'] = '{$a} wants to be added as a contact'; $string['messagecontactrequestsnotificationsubject'] = '{$a} wants to be added as a contact'; +$string['messagedrawerviewcontact'] = 'User details for {$a}'; +$string['messagedrawerviewcontacts'] = 'Message contacts'; +$string['messagedrawerviewconversation'] = 'Conversation with {$a}'; +$string['messagedrawerviewgroupinfo'] = 'Group details for {$a}'; +$string['messagedrawerviewoverview'] = 'Messages overview'; +$string['messagedrawerviewsearch'] = 'Messages search results for {$a}'; +$string['messagedrawerviewsettings'] = 'Message settings'; $string['messagepreferences'] = 'Message preferences'; $string['messages'] = 'Messages'; +$string['messagesselected:'] = 'Messages selected:'; $string['messagingdatahasnotbeenmigrated'] = 'Your messages are temporarily unavailable due to upgrades in the messaging infrastructure. Please wait for them to be migrated.'; $string['newonlymsg'] = 'Show only new'; $string['newmessage'] = 'New message'; $string['newmessagesearch'] = 'Select or search for a contact to send a new message.'; +$string['nofavourites'] = 'No favourites'; $string['noframesjs'] = 'Use more accessible interface'; +$string['nocontactrequests'] = 'No contact requests'; $string['nocontacts'] = 'No contacts'; +$string['nocontactsgetstarted'] = "Try searching for someone to add them as a contact"; +$string['nogroupmessages'] = 'No group messages'; $string['nomessages'] = 'No messages'; $string['nomessagesfound'] = 'No messages were found'; $string['noreply'] = 'Do not reply to this message'; $string['noncontacts'] = 'Non-contacts'; $string['nonotifications'] = 'You have no notifications'; +$string['noparticipants'] = 'No participants'; $string['notificationdatahasnotbeenmigrated'] = 'Your notifications are temporarily unavailable due to upgrades in the notification infrastructure. Please wait for them to be migrated.'; $string['notificationwindow'] = 'Notification window'; $string['notificationpreferences'] = 'Notification preferences'; $string['notificationimage'] = 'Notification image'; $string['notifications'] = 'Notifications'; +$string['notincontactsheading'] = '{$a} is not in your contacts'; +$string['notincontacts'] = 'You need to add {$a} to your contacts to be able to send them messages.'; +$string['numparticipants'] = '{$a} participants'; $string['off'] = 'Off'; $string['offline'] = 'Offline'; $string['on'] = 'On'; @@ -105,7 +137,10 @@ $string['outputdoesnotexist'] = 'Message output does not exists'; $string['outputenabled'] = 'Output enabled'; $string['outputnotavailable'] = 'Not available'; $string['outputnotconfigured'] = 'Not configured'; +$string['participants'] = 'Participants'; $string['permitted'] = 'Permitted'; +$string['privacy'] = 'Privacy'; +$string['privacy_desc'] = 'You can restrict who can message you'; $string['privacy:metadata:messages'] = 'Messages'; $string['privacy:metadata:messages:conversationid'] = 'The ID of the conversation'; $string['privacy:metadata:messages:fullmessage'] = 'The full message'; @@ -154,12 +189,18 @@ $string['privacy:metadata:preference:core_message_settings'] = 'Settings related $string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\''; $string['processorsettings'] = 'Processor settings'; $string['removecontact'] = 'Remove contact'; +$string['removecontactconfirm'] = 'Are you sure you want to remove {$a} from your contacts?'; $string['removecoursefilter'] = 'Remove filter for course {$a}'; $string['removefromyourcontacts'] = 'Remove from your contacts'; +$string['removefromfavourites'] = 'Remove from favourites'; +$string['requirecontacttomessage'] = 'You need to request {$a} to add you as a contact to be able to message'; $string['requiresconfiguration'] = 'Requires configuration'; $string['searchforuser'] = 'Search for a user'; $string['searchforuserorcourse'] = 'Search for a user or course'; $string['searchmessages'] = 'Search messages'; +$string['searchnocontactsfound'] = 'No contacts found'; +$string['searchnomessagesfound'] = 'No messages found'; +$string['searchnononcontactsfound'] = 'No non contacts found'; $string['searchcombined'] = 'Search people and messages'; $string['seeall'] = 'See all'; $string['selectmessagestodelete'] = 'Select messages to delete'; @@ -167,6 +208,7 @@ $string['selectnotificationtoview'] = 'Select from the list of notifications on $string['send'] = 'Send'; $string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"'; $string['sendingviawhen'] = 'Sending "{$a->provider}" via "{$a->processor}" when {$a->state}'; +$string['sendcontactrequest'] = 'Send contact request'; $string['sendmessage'] = 'Send message'; $string['sendbulkmessage'] = 'Send message to {$a} people'; $string['sendbulkmessagesingle'] = 'Send message to 1 person'; @@ -182,19 +224,29 @@ $string['shownotificationwindowwithcount'] = 'Show notification window with {$a} $string['togglenotificationmenu'] = 'Toggle notifications menu'; $string['togglemessagemenu'] = 'Toggle messages menu'; $string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) that doesn\'t exist'; +$string['unabletomessage'] = 'You are unable to message this user'; +$string['unblock'] = 'Unblock'; $string['unblockcontact'] = 'Unblock contact'; +$string['unblockuser'] = 'Unblock user'; +$string['unblockuserconfirm'] = 'Are you sure you want to unblock {$a}?'; $string['unknownuser'] = 'Unknown user'; $string['unreadnotification'] = 'Unread notification: {$a}'; $string['unreadnewgroupconversationmessage'] = 'New message from {$a->name} in {$a->conversationname}'; $string['unreadnewmessage'] = 'New message from {$a}'; $string['usercantbemessaged'] = 'You can\'t message {$a} due to their message preferences. Try adding them as a contact.'; +$string['userisblockingyou'] = 'This user has blocked you from sending messages to them'; +$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.'; +$string['userwouldliketocontactyou'] = '{$a} would like to contact you'; $string['viewfullnotification'] = 'View full notification'; $string['viewinganotherusersmessagearea'] = 'You are viewing another user\'s message area.'; $string['viewmessageswith'] = 'View messages with {$a}'; $string['viewnotificationresource'] = 'Go to: {$a}'; $string['viewunreadmessageswith'] = 'View unread messages with {$a}'; $string['writeamessage'] = 'Write a message...'; +$string['wouldliketocontactyou'] = 'Would like to contact you'; $string['you'] = 'You:'; +$string['youhaveblockeduser'] = 'You have blocked this user in the past'; +$string['yourcontactrequestpending'] = 'Your contact request is pending with {$a}'; // Deprecated since Moodle 3.6. $string['eventmessagecontactblocked'] = 'Message contact blocked'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 163e3d89671..93ebebeee21 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -808,6 +808,7 @@ $string['expandcategory'] = 'Expand {$a}'; $string['explanation'] = 'Explanation'; $string['explanationdigitalminor'] = 'This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.'; $string['extendperiod'] = 'Extended period'; +$string['favourites'] = 'Favourites'; $string['failedloginattempts'] = '{$a->attempts} failed logins since your last login'; $string['feedback'] = 'Feedback'; $string['file'] = 'File'; @@ -1656,6 +1657,7 @@ $string['requestcourse'] = 'Request a course'; $string['requestedby'] = 'Requested by'; $string['requestedcourses'] = 'Requested courses'; $string['requestreason'] = 'Reason for course request'; +$string['requests'] = 'Requests'; $string['required'] = 'Required'; $string['requirespayment'] = 'This course requires payment for access'; $string['resendemail'] = 'Resend email'; diff --git a/lib/amd/build/key_codes.min.js b/lib/amd/build/key_codes.min.js index 04bace5c914..18a820abbba 100644 --- a/lib/amd/build/key_codes.min.js +++ b/lib/amd/build/key_codes.min.js @@ -1 +1 @@ -define(function(){return{tab:9,enter:13,escape:27,space:32,end:35,home:36,arrowLeft:37,arrowUp:38,arrowRight:39,arrowDown:40,8:56,asterix:106,pageUp:33,pageDown:34}}); \ No newline at end of file +define(function(){return{tab:9,enter:13,shift:16,ctrl:17,alt:18,escape:27,space:32,end:35,home:36,arrowLeft:37,arrowUp:38,arrowRight:39,arrowDown:40,8:56,asterix:106,pageUp:33,pageDown:34}}); \ No newline at end of file diff --git a/lib/amd/src/key_codes.js b/lib/amd/src/key_codes.js index b64529b1cc4..aaaf296c02e 100644 --- a/lib/amd/src/key_codes.js +++ b/lib/amd/src/key_codes.js @@ -28,6 +28,9 @@ define(function() { return /** @alias module:core/key_codes */ { 'tab': 9, 'enter': 13, + 'shift': 16, + 'ctrl': 17, + 'alt': 18, 'escape': 27, 'space': 32, 'end': 35, diff --git a/lib/db/services.php b/lib/db/services.php index e0b5596c968..9f7392362d9 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1571,6 +1571,7 @@ $functions = array( 'classpath' => 'user/externallib.php', 'description' => 'Return user preferences.', 'type' => 'read', + 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'core_user_update_picture' => array( diff --git a/lib/templates/loading.mustache b/lib/templates/loading.mustache index c5fe4ee389a..98d9cf9f2d3 100644 --- a/lib/templates/loading.mustache +++ b/lib/templates/loading.mustache @@ -33,4 +33,4 @@ Example context (json): {} }} -{{#pix}} i/loading, core, {{#str}} loading {{/str}} {{/pix}} +{{#pix}} i/loading, core, {{#str}} loading {{/str}} {{/pix}} diff --git a/message/amd/build/message_drawer.min.js b/message/amd/build/message_drawer.min.js new file mode 100644 index 00000000000..edc08680a5e --- /dev/null +++ b/message/amd/build/message_drawer.min.js @@ -0,0 +1 @@ +define(["jquery","core/custom_interaction_events","core/pubsub","core_message/message_drawer_view_contact","core_message/message_drawer_view_contacts","core_message/message_drawer_view_conversation","core_message/message_drawer_view_group_info","core_message/message_drawer_view_overview","core_message/message_drawer_view_search","core_message/message_drawer_view_settings","core_message/message_drawer_router","core_message/message_drawer_routes","core_message/message_drawer_events"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={VIEW_CONTACT:'[data-region="view-contact"]',VIEW_CONTACTS:'[data-region="view-contacts"]',VIEW_CONVERSATION:'[data-region="view-conversation"]',VIEW_GROUP_INFO:'[data-region="view-group-info"]',VIEW_OVERVIEW:'[data-region="view-overview"]',VIEW_SEARCH:'[data-region="view-search"]',VIEW_SETTINGS:'[data-region="view-settings"]',ROUTES:"[data-route]",ROUTES_BACK:"[data-route-back]",HEADER_CONTAINER:'[data-region="header-container"]',BODY_CONTAINER:'[data-region="body-container"]',FOOTER_CONTAINER:'[data-region="footer-container"]'},o=function(a,b){var c=a.children(),d=c.filter(n.HEADER_CONTAINER).find(b),e=c.filter(n.BODY_CONTAINER).find(b),f=c.filter(n.FOOTER_CONTAINER).find(b),g=[d,e,f].filter(function(a){return a.length});return g},p=[[l.VIEW_CONTACT,n.VIEW_CONTACT,d.show,d.description],[l.VIEW_CONTACTS,n.VIEW_CONTACTS,e.show,e.description],[l.VIEW_CONVERSATION,n.VIEW_CONVERSATION,f.show,f.description],[l.VIEW_GROUP_INFO,n.VIEW_GROUP_INFO,g.show,g.description],[l.VIEW_OVERVIEW,n.VIEW_OVERVIEW,h.show,h.description],[l.VIEW_SEARCH,n.VIEW_SEARCH,i.show,i.description],[l.VIEW_SETTINGS,n.VIEW_SETTINGS,j.show,j.description]],q=function(a){p.forEach(function(b){k.add(b[0],o(a,b[1]),b[2],b[3])})},r=function(a){a.attr("data-shown")||(k.go(l.VIEW_OVERVIEW),a.attr("data-shown",!0)),a.removeClass("hidden"),a.attr("aria-expanded",!0),a.attr("aria-hidden",!1)},s=function(a){a.addClass("hidden"),a.attr("aria-expanded",!1),a.attr("aria-hidden",!0)},t=function(a){return!a.hasClass("hidden")},u=function(d){b.define(d,[b.events.activate]);var e=/^data-route-param-?(\d*)$/;d.on(b.events.activate,n.ROUTES,function(b,c){for(var d=a(b.target).closest(n.ROUTES),f=d.attr("data-route"),g=[],h=0;h1?c[1]:0,g=d.length>1?d[1]:0;return f0){var d=h(a);return c(d,b,f).then(function(){return b})}return b}).then(function(b){return e(a),a.attr("data-seen",!0),b.length||s(a,!0),b})["catch"](function(){e(a),a.attr("data-seen",!0)})},v=function(a,b,c){return h(a).empty(),n(a),q(a),u(a,b,c).then(function(b){o(a),b.length?p(a):l(a)})["catch"](function(){o(a),p(a)})},w=function(a,c,d){b.define(a,[b.events.scrollBottom]),a.on(b.events.scrollBottom,function(){t(a)&&(j(a),u(a,c,d).then(function(){return k(a)})["catch"](function(){return k(a)}))})},x=function(b,c,d){b=a(b),b.attr("data-init")||(w(b,c,d),v(b,c,d),b.attr("data-init",!0))};return{show:x,getContentContainer:h,getRoot:i,setLoadedAll:s,showEmptyMessage:l,hideEmptyMessage:m,showContent:p,hideContent:q}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_router.min.js b/message/amd/build/message_drawer_router.min.js new file mode 100644 index 00000000000..c5aeb95060b --- /dev/null +++ b/message/amd/build/message_drawer_router.min.js @@ -0,0 +1 @@ +define(["jquery","core/pubsub","core/str","core_message/message_drawer_events"],function(a,b,c,d){var e={},f=[],g={CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',ROUTES_BACK:"[data-route-back]"},h=function(a,b,c,d){e[a]={elements:b,onGo:c,getDescription:d}},i=function(c){var f,h=[].slice.call(arguments,1),i=a.Deferred().resolve().promise();if(Object.keys(e).forEach(function(a){var b=e[a],d=a===c;d&&(f=b),b.elements.forEach(function(a){a.removeClass("previous"),d?(a.removeClass("hidden"),a.attr("aria-hidden",!1)):(a.addClass("hidden"),a.attr("aria-hidden",!0))})}),f&&f.onGo){i=f.onGo.apply(void 0,f.elements.concat(h));for(var j=a(document.activeElement),k=!1,l=0;lh?a.pop():k=!0,a}).then(function(b){return 0==j&&0==b.length&&(t(a),s(a)),i+=b.length,j+=h,b.length>0?x(a,b):b})},z=function(a,b){v(a,b).remove()},A=function(a,b){var c=v(a,b);c.length&&c.find(m.BLOCK_ICON_CONTAINER).removeClass("hidden")},B=function(a,b){var c=v(a,b);c.length&&c.find(m.BLOCK_ICON_CONTAINER).addClass("hidden")},C=function(a){c.subscribe(g.CONTACT_ADDED,function(){j=0,k=!1,q(a).empty(),y(a)}),c.subscribe(g.CONTACT_REMOVED,function(b){z(a,b)}),c.subscribe(g.CONTACT_BLOCKED,function(b){A(a,b)}),c.subscribe(g.CONTACT_UNBLOCKED,function(b){B(a,b)});var d=r(a);e.define(d,[e.events.scrollBottom,e.events.scrollLock]),d.on(e.events.scrollBottom,function(c,d){var e=i>1;k||!e||l||(l=!0,o(a),y(a).then(function(){p(a),l=!1})["catch"](function(c){p(a),l=!1,b.exception(c)})),d.originalEvent.preventDefault()})},D=function(b,c){c=a(c),j=0,c.attr("data-contacts-init")||(C(c),c.attr("data-contacts-init",!0)),k||y(c)};return{show:D}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_contacts_section_contacts.min.js b/message/amd/build/message_drawer_view_contacts_section_contacts.min.js new file mode 100644 index 00000000000..9b0b825f9e0 --- /dev/null +++ b/message/amd/build/message_drawer_view_contacts_section_contacts.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/pubsub","core/templates","core_message/message_repository","core_message/message_drawer_events","core_message/message_drawer_lazy_load_list"],function(a,b,c,d,e,f,g){var h=100,i=0,j={BLOCK_ICON_CONTAINER:'[data-region="block-icon-container"]',CONTACT:'[data-region="contact"]',CONTENT_CONTAINER:'[data-region="contacts-content-container"]'},k={CONTACTS_LIST:"core_message/message_drawer_contacts_list"},l=function(a,b){return a.find('[data-contact-user-id="'+b+'"]')},m=function(c,e){var f=e.map(function(b){return a.extend(b,{id:b.userid})});return d.render(k.CONTACTS_LIST,{contacts:f}).then(function(a){return c.append(a),a})["catch"](b.exception)},n=function(a,c){return e.getContacts(c,h+1,i).then(function(a){return a.contacts}).then(function(b){return b.length>h?b.pop():g.setLoadedAll(a,!0),b}).then(function(a){return i+=h,a})["catch"](b.exception)},o=function(a,b){l(a,b).remove()},p=function(a,b){var c=l(a,b);c.length&&c.find(j.BLOCK_ICON_CONTAINER).removeClass("hidden")},q=function(a,b){var c=l(a,b);c.length&&c.find(j.BLOCK_ICON_CONTAINER).addClass("hidden")},r=function(a){c.subscribe(f.CONTACT_ADDED,function(b){var c=g.getContentContainer(a);m(c,[b]),g.hideEmptyMessage(a),g.showContent(a)}),c.subscribe(f.CONTACT_REMOVED,function(b){o(a,b);var c=a.find(j.CONTACT);c.length||(g.hideContent(a),g.showEmptyMessage(a))}),c.subscribe(f.CONTACT_BLOCKED,function(b){p(a,b)}),c.subscribe(f.CONTACT_UNBLOCKED,function(b){q(a,b)})},s=function(a){a.attr("data-contacts-init")||(r(a),a.attr("data-contacts-init",!0)),g.show(a,n,m)};return{show:s}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_contacts_section_requests.min.js b/message/amd/build/message_drawer_view_contacts_section_requests.min.js new file mode 100644 index 00000000000..cf9b67e0972 --- /dev/null +++ b/message/amd/build/message_drawer_view_contacts_section_requests.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/pubsub","core/templates","core_message/message_repository","core_message/message_drawer_events","core_message/message_drawer_lazy_load_list"],function(a,b,c,d,e,f,g){var h={CONTACT_REQUEST:'[data-region="contact-request"]'},i={REQUESTS_LIST:"core_message/message_drawer_view_contacts_body_section_requests_list"},j=function(a,c){var e=c.map(function(a){return{id:a.id,profileimageurl:a.profileimageurl,fullname:a.fullname}});return d.render(i.REQUESTS_LIST,{requests:e}).then(function(b){return a.append(b),b})["catch"](b.exception)},k=function(a,c){return e.getContactRequests(c).then(function(b){return g.setLoadedAll(a,!0),b})["catch"](b.exception)},l=function(a){return function(b){a.find('[data-request-id="'+b.userid+'"]').remove();var c=a.find(h.CONTACT_REQUEST);c.length||(g.showEmptyMessage(a),g.hideContent(a))}},m=function(a){c.subscribe(f.CONTACT_REQUEST_ACCEPTED,l(a)),c.subscribe(f.CONTACT_REQUEST_DECLINED,l(a))},n=function(a){a.attr("data-contacts-init")||(m(a),a.attr("data-contacts-init",!0)),g.show(a,k,j)};return{show:n}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_conversation.min.js b/message/amd/build/message_drawer_view_conversation.min.js new file mode 100644 index 00000000000..6e19ab79fbb --- /dev/null +++ b/message/amd/build/message_drawer_view_conversation.min.js @@ -0,0 +1 @@ +define(["jquery","core/auto_rows","core/backoff_timer","core/custom_interaction_events","core/notification","core/pubsub","core/str","core_message/message_repository","core_message/message_drawer_events","core_message/message_drawer_view_conversation_constants","core_message/message_drawer_view_conversation_patcher","core_message/message_drawer_view_conversation_renderer","core_message/message_drawer_view_conversation_state_manager","core_message/message_drawer_router","core_message/message_drawer_routes"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p={},q=null,r=!1,s=0,t=null,u=null,v=j.NEWEST_MESSAGES_FIRST,w=j.LOAD_MESSAGE_LIMIT,x=j.INITIAL_NEW_MESSAGE_POLL_TIMEOUT,y=j.SELECTORS,z=j.CONVERSATION_TYPES,A=function(){if(!q||q.type!=z.PRIVATE)return null;var a=q.loggedInUserId,b=Object.keys(q.members).filter(function(b){return a!=b});return b.length?b[0]:null},B=function(a){return Object.keys(p).reduce(function(b,c){if(!b){var d=p[c].state;d.type==z.PRIVATE&&a in d.members&&(b=d.id)}return b},null)},C=function(a){return{id:parseInt(a.attr("data-user-id"),10),contactrequests:[]}},D=function(){return s},E=function(a){s=a,p[q.id].messagesOffset=a},F=function(){return r},G=function(a){r=a,p[q.id].loadedAllMessages=a},H=function(a){return a.find(y.MESSAGES_CONTAINER)},I=function(b){return{id:b.id,name:b.name,subname:b.subname,imageUrl:b.imageUrl,isFavourite:b.isFavourite,type:b.type,totalMemberCount:b.totalMemberCount,loggedInUserId:b.loggedInUserId,messages:b.messages.map(function(b){return a.extend({},b)}),members:Object.keys(b.members).reduce(function(c,d){return c[d]=a.extend({},b.members[d]),c[d].contactrequests=b.members[d].contactrequests.map(function(b){return a.extend({},b)}),c},{})}},J=function(a,b){var c=a.id,d=m.setLoadingMembers(q,!0);return d=m.setLoadingMessages(d,!0),u(d).then(function(){return h.getMemberInfo(c,[b],!0,!0)}).then(function(a){if(a.length)return a[0];throw new Error("Unable to load other user profile")}).then(function(b){var c=m.addMembers(q,[b,a]);return c=m.setLoadingMembers(c,!1),c=m.setLoadingMessages(c,!1),c=m.setName(c,b.fullname),c=m.setType(c,1),c=m.setImageUrl(c,b.profileimageurl),c=m.setTotalMemberCount(c,2),u(c).then(function(){return b})})["catch"](function(a){var b=m.setLoadingMembers(q,!1);u(b),e.exception(a)})},K=function(a,b){var c=a.members.filter(function(a){return a.id!=b}),d=c.length?c[0]:null,e=a.name,f=a.imageurl;a.type==z.PRIVATE&&(e=e||d?d.fullname:"",f=f||d?d.profileimageurl:"");var g=m.addMembers(q,a.members);return g=m.setName(g,e),g=m.setSubname(g,a.subname),g=m.setType(g,a.type),g=m.setImageUrl(g,f),g=m.setTotalMemberCount(g,a.membercount),g=m.setIsFavourite(g,a.isfavourite),g=m.addMessages(g,a.messages)},L=function(a,b,c,d,f){var g=b.id,i=m.setLoadingMembers(q,!0);return i=m.setLoadingMessages(i,!0),u(i).then(function(){return h.getConversation(g,a,!0,!0,0,0,c+1,d,f)}).then(function(a){return a.messages.length>c?a.messages=a.messages.slice(1):G(!0),E(d+c),a}).then(function(a){var c=a.members.filter(function(a){return a.id==b.id});c.length<1&&(a.members=a.members.concat([b]));var d=K(a,b.id);return d=m.setLoadingMembers(d,!1),d=m.setLoadingMessages(d,!1),u(d).then(function(){return a})}).then(function(){return P(a)})["catch"](function(a){var b=m.setLoadingMembers(q,!1);b=m.setLoadingMessages(b,!1),u(b),e.exception(a)})},M=function(a,b,c,d){var f=a.members.filter(function(a){return a.id==b.id});f.length<1&&(a.members=a.members.concat([b]));var g=K(a,b.id);g=m.setLoadingMembers(g,!1),g=m.setLoadingMessages(g,!0);var h=a.messages.length;return u(g).then(function(){if(hb?a.messages=a.messages.slice(1):G(!0),a):a}).then(function(a){var b=a.members.filter(function(a){return!(a.id in q.members)}),c=m.addMembers(q,b);return c=m.addMessages(c,a.messages),c=m.setLoadingMessages(c,!1),u(c).then(function(){return a})})["catch"](function(a){var b=m.setLoadingMessages(q,!1);throw u(b),a})},O=function(a,b){return function(){var c=q.messages,d=c.length?c[c.length-1]:null;if(d){for(var e=[],f=c.length-1;f>=0;f--){var g=c[f];if(g.timeCreated!==d.timeCreated)break;e.push(g.id)}return N(a,0,0,b,e,d.timeCreated).then(function(b){return b.messages.length?(t.restart(),P(a)):b})}}},P=function(a){var b=q.loggedInUserId;return h.markAllConversationMessagesAsRead(b,a).then(function(){var b=m.markMessagesAsRead(q,q.messages);return f.publish(i.CONVERSATION_READ,a),u(b)})},Q=function(a){return ca(a).then(function(){var b=m.addPendingBlockUsersById(q,[a]);return u(b)})},R=function(a){var b=m.setLoadingConfirmAction(q,!0);return u(b).then(function(){return h.blockUser(q.loggedInUserId,a)}).then(function(b){var c=m.addMembers(q,[b]);return c=m.removePendingBlockUsersById(c,[a]),c=m.setLoadingConfirmAction(c,!1),f.publish(i.CONTACT_BLOCKED,c.id),u(c)})},S=function(a){return ca(a).then(function(){var b=m.addPendingUnblockUsersById(q,[a]);return u(b)})},T=function(a){var b=m.setLoadingConfirmAction(q,!0);return u(b).then(function(){return h.unblockUser(q.loggedInUserId,a)}).then(function(b){var c=m.addMembers(q,[b]);return c=m.removePendingUnblockUsersById(c,[a]),c=m.setLoadingConfirmAction(c,!1),f.publish(i.CONTACT_UNBLOCKED,c.id),u(c)})},U=function(a){return ca(a).then(function(){var b=m.addPendingRemoveContactsById(q,[a]);return u(b)})},V=function(a){var b=m.setLoadingConfirmAction(q,!0);return u(b).then(function(){return h.deleteContacts(q.loggedInUserId,[a])}).then(function(b){var c=m.addMembers(q,b);return c=m.removePendingRemoveContactsById(c,[a]),c=m.setLoadingConfirmAction(c,!1),f.publish(i.CONTACT_REMOVED,a),u(c)})},W=function(a){return ca(a).then(function(){var b=m.addPendingAddContactsById(q,[a]);return u(b)})},X=function(a){var b=m.setLoadingConfirmAction(q,!0);return u(b).then(function(){return h.createContactRequest(q.loggedInUserId,a)}).then(function(a){if(!a.request)throw new Error(a.warnings[0].message);return a.request}).then(function(b){var c=m.removePendingAddContactsById(q,[a]);return c=m.addContactRequests(c,[b]),c=m.setLoadingConfirmAction(c,!1),u(c)})},Y=function(){var a=q.loggedInUserId,b=q.id;return h.setFavouriteConversations(a,[b]).then(function(){var a=m.setIsFavourite(q,!0);return u(a)}).then(function(){return f.publish(i.CONVERSATION_SET_FAVOURITE,I(q))})},Z=function(){var a=q.loggedInUserId,b=q.id;return h.unsetFavouriteConversations(a,[b]).then(function(){var a=m.setIsFavourite(q,!1);return u(a)}).then(function(){return f.publish(i.CONVERSATION_UNSET_FAVOURITE,I(q))})},$=function(a){var b=q.selectedMessageIds;return ca(a).then(function(){var a=m.addPendingDeleteMessagesById(q,b);return u(a)})},_=function(){var a=q.pendingDeleteMessageIds,b=m.setLoadingConfirmAction(q,!0);return u(b).then(function(){return h.deleteMessages(q.loggedInUserId,a)}).then(function(){var b=m.removeMessagesById(q,a);b=m.removePendingDeleteMessagesById(b,a),b=m.removeSelectedMessagesById(b,a),b=m.setLoadingConfirmAction(b,!1);var c=q.messages[q.messages.length-1],d=b.messages.length?b.messages[b.messages.length-1]:null;if(d&&d.id!=c.id){var e=I(b);f.publish(i.CONVERSATION_NEW_LAST_MESSAGE,e)}else b.messages.length||f.publish(i.CONVERSATION_DELETED,b.id);return u(b)})},aa=function(a){return ca(a).then(function(){var a=m.setPendingDeleteConversation(q,!0);return u(a)})},ba=function(){var a=m.setLoadingConfirmAction(q,!0);return u(a).then(function(){return h.deleteCoversation(q.loggedInUserId,A())}).then(function(){var a=m.removeMessages(q,q.messages);return a=m.removeSelectedMessagesById(a,q.selectedMessageIds),a=m.setPendingDeleteConversation(a,!1),a=m.setLoadingConfirmAction(a,!1),f.publish(i.CONVERSATION_DELETED,a.id),u(a)})},ca=function(a){var b=q.pendingDeleteMessageIds,c=m.removePendingAddContactsById(q,[a]);return c=m.removePendingRemoveContactsById(c,[a]),c=m.removePendingUnblockUsersById(c,[a]),c=m.removePendingBlockUsersById(c,[a]),c=m.removePendingDeleteMessagesById(c,b),c=m.setPendingDeleteConversation(c,!1),u(c)},da=function(a){var b=q.loggedInUserId,c=q.members[a].contactrequests.filter(function(a){return a.requesteduserid==b}),d=c[0],e=m.setLoadingConfirmAction(q,!0);return u(e).then(function(){return h.acceptContactRequest(a,b)}).then(function(a){var b=m.removeContactRequests(q,[d]);return b=m.addMembers(q,[a]),b=m.setLoadingConfirmAction(b,!1),u(b)}).then(function(){f.publish(i.CONTACT_ADDED,q.members[a]),f.publish(i.CONTACT_REQUEST_ACCEPTED,d)})},ea=function(a){var b=q.loggedInUserId,c=q.members[b].contactrequests.filter(function(b){return b.userid==a}),d=c[0],e=m.setLoadingConfirmAction(q,!0);return u(e).then(function(){return h.declineContactRequest(a,b)}).then(function(a){var b=m.removeContactRequests(q,[d]);return b=m.addMembers(q,[a]),b=m.setLoadingConfirmAction(b,!1),u(b)}).then(function(){f.publish(i.CONTACT_REQUEST_DECLINED,d)})},fa=function(a,b){var c=m.setSendingMessage(q,!0),d=null;return u(c).then(function(){if(a||q.type!=z.PRIVATE)return h.sendMessageToConversation(a,b);var c=A();return h.sendMessageToUser(c,b).then(function(a){return d=parseInt(a.conversationid,10),a})}).then(function(a){var b=m.addMessages(q,[a]);b=m.setSendingMessage(b,!1);var c=I(b);return b.id||(b=m.setId(b,d),c.id=d,va(d),f.publish(i.CONVERSATION_CREATED,c)),u(b).then(function(){f.publish(i.CONVERSATION_NEW_LAST_MESSAGE,c)})})["catch"](function(a){var b=m.setSendingMessage(q,!1);u(b),e.exception(a)})},ga=function(a){var b=q;return b=q.selectedMessageIds.indexOf(a)>-1?m.removeSelectedMessagesById(q,[a]):m.addSelectedMessagesById(q,[a]),u(b)},ha=function(){return ca(A()).then(function(){var a=m.removeSelectedMessagesById(q,q.selectedMessageIds);return u(a)})},ia=function(a,b,c){return function(d){var e=k.buildPatch(q,d);return l.render(a,b,c,e).then(function(){q=d,d.id&&(p[d.id]={state:d,messagesOffset:D(),loadedAllMessages:F()})})}},ja=function(a){return function(b,c){q.loadingConfirmAction||a(A())["catch"](function(a){var b=m.setLoadingConfirmAction(q,!1);u(b),e.exception(a)}),c.originalEvent.preventDefault()}},ka=function(b,c){var d=a(b.target),e=d.closest(y.FOOTER_CONTAINER),f=e.find(y.MESSAGE_TEXT_AREA),g=f.val().trim();""!==g&&fa(q.id,g),c.originalEvent.preventDefault()},la=function(b,c){var d=window.getSelection(),f=a(b.target);if(""==d.toString()&&!f.is("a")){var g=f.closest(y.MESSAGE),h=parseInt(g.attr("data-message-id"),10);ga(h)["catch"](e.exception),c.originalEvent.preventDefault()}},ma=function(a,b){ha()["catch"](e.exception),b.originalEvent.preventDefault()},na=function(a,b){var c=A(),d=q.members[c];n.go(o.VIEW_CONTACT,d),b.originalEvent.preventDefault()},oa=function(a,b){Y()["catch"](e.exception),b.originalEvent.preventDefault()},pa=function(a,b){Z()["catch"](e.exception),b.originalEvent.preventDefault()},qa=function(a,b){n.go(o.VIEW_GROUP_INFO,{id:q.id,name:q.name,subname:q.subname,imageUrl:q.imageUrl,totalMemberCount:q.totalMemberCount},q.loggedInUserId),b.originalEvent.preventDefault()},ra=[[y.ACTION_REQUEST_BLOCK,ja(Q)],[y.ACTION_REQUEST_UNBLOCK,ja(S)],[y.ACTION_REQUEST_ADD_CONTACT,ja(W)],[y.ACTION_REQUEST_REMOVE_CONTACT,ja(U)],[y.ACTION_REQUEST_DELETE_CONVERSATION,ja(aa)],[y.ACTION_CANCEL_EDIT_MODE,ma],[y.ACTION_VIEW_CONTACT,na],[y.ACTION_VIEW_GROUP_INFO,qa],[y.ACTION_CONFIRM_FAVOURITE,oa],[y.ACTION_CONFIRM_UNFAVOURITE,pa]],sa=[[y.ACTION_CANCEL_CONFIRM,ja(ca)],[y.ACTION_CONFIRM_BLOCK,ja(R)],[y.ACTION_CONFIRM_UNBLOCK,ja(T)],[y.ACTION_CONFIRM_ADD_CONTACT,ja(X)],[y.ACTION_CONFIRM_REMOVE_CONTACT,ja(V)],[y.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES,ja(_)],[y.ACTION_CONFIRM_DELETE_CONVERSATION,ja(ba)],[y.ACTION_REQUEST_ADD_CONTACT,ja(W)],[y.ACTION_ACCEPT_CONTACT_REQUEST,ja(da)],[y.ACTION_DECLINE_CONTACT_REQUEST,ja(ea)],[y.MESSAGE,la]],ta=[[y.SEND_MESSAGE_BUTTON,ka],[y.ACTION_REQUEST_DELETE_SELECTED_MESSAGES,ja($)],[y.ACTION_REQUEST_ADD_CONTACT,ja(W)],[y.ACTION_REQUEST_UNBLOCK,ja(S)]],ua=function(a,c,g){var h=!1,j=H(c);b.init(g),d.define(a,[d.events.activate]),d.define(c,[d.events.activate]),d.define(g,[d.events.activate]),d.define(j,[d.events.scrollTop,d.events.scrollLock]),j.on(d.events.scrollTop,function(a,b){var c=Object.keys(q.members).length>1;if(!h&&!F()&&c){var d=m.setLoadingMessages(q,!0);u(d).then(function(){return N(q.id,w,D(),v,[])}).then(function(){h=!1,E(D()+w)})["catch"](function(a){h=!1,e.exception(a)})}b.originalEvent.preventDefault()}),ra.forEach(function(b){var c=b[0],e=b[1];a.on(d.events.activate,c,e)}),sa.forEach(function(a){var b=a[0],e=a[1];c.on(d.events.activate,b,e)}),ta.forEach(function(a){var b=a[0],c=a[1];g.on(d.events.activate,b,c)}),f.subscribe(i.ROUTE_CHANGED,function(a){t&&(a.route==o.VIEW_CONVERSATION?t.restart():t.stop())})},va=function(a){t&&t.stop(),t=new c(O(a,v),function(a){return a?2*a:x}),t.start()},wa=function(a,b,c){var d=c.id,e=parseInt(a.attr("data-midnight"),10),f=m.buildInitialState(e,d,b);return q||(q=f),t&&t.stop(),u(f)},xa=function(a,b,c){return wa(a,null,b).then(function(){return h.getConversationBetweenUsers(b.id,c,!0,!0,0,0,w,0,v).then(function(c){return za(a,c,b)})["catch"](function(){return J(b,c)})})},ya=function(a,b,c){var d=null;return b in p&&(d=p[b]),wa(a,b,c).then(function(){if(d){var a=d.state;return a=m.setLoadingMessages(a,!1),a=m.setLoadingMembers(a,!1),E(d.messagesOffset),G(d.loadedAllMessages),u(a)}return L(b,c,w,0,v)}).then(function(){return va(b)})},za=function(a,b,c){var d=null;return b.id in p&&(d=p[b.id]),wa(a,b.id,c).then(function(){if(d){var a=d.state;return a=m.setLoadingMessages(a,!1),a=m.setLoadingMembers(a,!1),E(d.messagesOffset),G(d.loadedAllMessages),u(a)}return M(b,c,w,v)}).then(function(){return va(b.id)})},Aa=function(b,c,d,f,g,h){var i=null,k=null;"object"==typeof f?(i=f,k=parseInt(i.id,10)):(i=null,k=parseInt(f,10),k=isNaN(k)?null:k),!k&&g&&h&&(k=B(h)),c.attr("data-init")||(u=ia(b,c,d),ua(b,c,d),c.attr("data-init",!0));var l=!q||q.id!=k||h&&h!=A();if(l){var m=null,n=C(c);return m=i?za(c,i,n,h):k?ya(c,k,n,h):xa(c,n,h),m.then(function(){b.find(j.SELECTORS.CAN_RECEIVE_FOCUS).first().focus()})["catch"](e.exception)}if(q.type==z.PRIVATE&&g){var o=A();switch(g){case"block":return Q(o);case"unblock":return S(o);case"add-contact":return W(o);case"remove-contact":return U(o)}}return a.Deferred().resolve().promise()},Ba=function(){return g.get_string("messagedrawerviewconversation","core_message",q.name)};return{show:Aa,description:Ba}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_conversation_constants.min.js b/message/amd/build/message_drawer_view_conversation_constants.min.js new file mode 100644 index 00000000000..afd8fe09422 --- /dev/null +++ b/message/amd/build/message_drawer_view_conversation_constants.min.js @@ -0,0 +1 @@ +define([],function(){var a={ACTION_ACCEPT_CONTACT_REQUEST:'[data-action="accept-contact-request"]',ACTION_CANCEL_CONFIRM:'[data-action="cancel-confirm"]',ACTION_CANCEL_EDIT_MODE:'[data-action="cancel-edit-mode"]',ACTION_CONFIRM_ADD_CONTACT:'[data-action="confirm-add-contact"]',ACTION_CONFIRM_BLOCK:'[data-action="confirm-block"]',ACTION_CONFIRM_DELETE_SELECTED_MESSAGES:'[data-action="confirm-delete-selected-messages"]',ACTION_CONFIRM_DELETE_CONVERSATION:'[data-action="confirm-delete-conversation"]',ACTION_CONFIRM_FAVOURITE:'[data-action="confirm-favourite"]',ACTION_CONFIRM_UNFAVOURITE:'[data-action="confirm-unfavourite"]',ACTION_CONFIRM_REMOVE_CONTACT:'[data-action="confirm-remove-contact"]',ACTION_CONFIRM_UNBLOCK:'[data-action="confirm-unblock"]',ACTION_DECLINE_CONTACT_REQUEST:'[data-action="decline-contact-request"]',ACTION_REQUEST_ADD_CONTACT:'[data-action="request-add-contact"]',ACTION_REQUEST_BLOCK:'[data-action="request-block"]',ACTION_REQUEST_DELETE_CONVERSATION:'[data-action="request-delete-conversation"]',ACTION_REQUEST_DELETE_SELECTED_MESSAGES:'[data-action="delete-selected-messages"]',ACTION_REQUEST_REMOVE_CONTACT:'[data-action="request-remove-contact"]',ACTION_REQUEST_UNBLOCK:'[data-action="request-unblock"]',ACTION_VIEW_CONTACT:'[data-action="view-contact"]',ACTION_VIEW_GROUP_INFO:'[data-action="view-group-info"]',CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',CONFIRM_DIALOGUE_BUTTON_TEXT:'[data-region="dialogue-button-text"]',CONFIRM_DIALOGUE_CANCEL_BUTTON:'[data-action="cancel-confirm"]',CONFIRM_DIALOGUE_CONTAINER:'[data-region="confirm-dialogue-container"]',CONFIRM_DIALOGUE_HEADER:'[data-region="dialogue-header"]',CONFIRM_DIALOGUE_TEXT:'[data-region="dialogue-text"]',CONTACT_REQUEST_SENT_MESSAGE_CONTAINER:'[data-region="contact-request-sent-message-container"]',CONTENT_PLACEHOLDER_CONTAINER:'[data-region="content-placeholder"]',CONTENT_CONTAINER:'[data-region="content-container"]',CONTENT_MESSAGES_CONTAINER:'[data-region="content-message-container"]',CONTENT_MESSAGES_FOOTER_CONTAINER:'[data-region="content-messages-footer-container"]',CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER:'[data-region="content-messages-footer-edit-mode-container"]',CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER:'[data-region="content-messages-footer-require-contact-container"]',CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER:'[data-region="content-messages-footer-require-unblock-container"]',CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER:'[data-region="content-messages-footer-unable-to-message"]',DAY_MESSAGES_CONTAINER:'[data-region="day-messages-container"]',FAVOURITE_ICON_CONTAINER:'[data-region="favourite-icon-container"]',FOOTER_CONTAINER:'[data-region="content-messages-footer-container"]',HEADER:'[data-region="header-content"]',HEADER_EDIT_MODE:'[data-region="header-edit-mode"]',HEADER_PLACEHOLDER_CONTAINER:'[data-region="header-placeholder"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',MESSAGE:'[data-region="message"]',MESSAGE_NOT_SELECTED:'[data-region="message"][aria-checked="false"]',MESSAGE_NOT_SELECTED_ICON:'[data-region="not-selected-icon"]',MESSAGE_SELECTED_ICON:'[data-region="selected-icon"]',MESSAGES:'[data-region="content-message-container"]',MESSAGES_CONTAINER:'[data-region="content-message-container"]',MESSAGES_SELECTED_COUNT:'[data-region="message-selected-court"]',MESSAGE_TEXT_AREA:'[data-region="send-message-txt"]',MORE_MESSAGES_LOADING_ICON_CONTAINER:'[data-region="more-messages-loading-icon-container"]',PLACEHOLDER_CONTAINER:'[data-region="placeholder-container"]',SEND_MESSAGE_BUTTON:'[data-action="send-message"]',SEND_MESSAGE_ICON_CONTAINER:'[data-region="send-icon-container"]',TEXT:'[data-region="text"]',TITLE:'[data-region="title"]'},b={HEADER_PRIVATE:"core_message/message_drawer_view_conversation_header_content_type_private",HEADER_PRIVATE_NO_CONTROLS:"core_message/message_drawer_view_conversation_header_content_type_private_no_controls",HEADER_PUBLIC:"core_message/message_drawer_view_conversation_header_content_type_public",DAY:"core_message/message_drawer_view_conversation_body_day",MESSAGE:"core_message/message_drawer_view_conversation_body_message",MESSAGES:"core_message/message_drawer_view_conversation_body_messages"},c={PRIVATE:1,PUBLIC:2};return{SELECTORS:a,TEMPLATES:b,CONVERSATION_TYPES:c,NEWEST_MESSAGES_FIRST:!0,LOAD_MESSAGE_LIMIT:100,INITIAL_NEW_MESSAGE_POLL_TIMEOUT:1e3}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_conversation_patcher.min.js b/message/amd/build/message_drawer_view_conversation_patcher.min.js new file mode 100644 index 00000000000..103c32c3443 --- /dev/null +++ b/message/amd/build/message_drawer_view_conversation_patcher.min.js @@ -0,0 +1 @@ +define(["jquery","core/user_date","core_message/message_drawer_view_conversation_constants"],function(a,b,c){var d=function(a,c){var d=a.reduce(function(a,d){var e=b.getUserMidnightForTimestamp(d.timeCreated,c);return a.hasOwnProperty(e)?a[e].push(d):a[e]=[d],a},{});return Object.keys(d).map(function(a){return{timestamp:a,messages:d[a]}})},e=function(a,b,c){b=b.slice();var d=[],e=[],f=[];return a.forEach(function(a){for(var d=!1,g=0;g0,i=g.length>0;return d||e?h&&i?null:h||!i||e.iscontact?!d&&e?e.iscontact?"contact":null:!e&&d?d.iscontact?"non-contact":null:d.iscontact&&!e.iscontact?i?"pending-contact":"non-contact":!d.iscontact&&e.iscontact?"contact":null:"pending-contact":null},B=function(a,b){return!(a.loadingConfirmAction||!b.loadingConfirmAction)||!(a.loadingConfirmAction&&!b.loadingConfirmAction)&&null},C=function(a,b){var c=a.selectedMessageIds.length>0,d=b.selectedMessageIds.length>0,e=a.messages.length!=b.messages.length;return!(c||!d)||!(c&&!d)&&(!(!c||!e)||null)},D=function(a,b){var c=a.selectedMessageIds,d=b.selectedMessageIds;if(g(c,d))return null;var f=e(c,d,function(a,b){return a==b});return{count:d.length,add:f.missingFromA,remove:f.missingFromB}},E=function(a){return Object.keys(a.members).reduce(function(b,c){return c==a.loggedInUserId||b||(b=a.members[c]),b},null)},F=function(a,b){var c=b.contactrequests.filter(function(b){return b.userid==a||b.requesteduserid}),d=c.length>0;return b.requirescontact&&!b.iscontact&&!d},G=function(a,b){var c=E(a),d=E(b),e=a.messages.length>0,f=b.messages.length>0,g=b.loggedInUserId,h=c&&F(g,c),i=d&&F(g,d),j=t(a,b),k=j===!1;if(!a.hasTriedToLoadMessages&&!b.hasTriedToLoadMessages)return null;if(!c&&!d)return null;if(!c&&i)return{show:!0,hasMessages:f,user:d};if(k&&i)return{show:!0,hasMessages:f,user:d};if(a.hasTriedToLoadMessages&&b.hasTriedToLoadMessages){if(!h&&i)return{show:!0,hasMessages:f,user:d};if(h&&!i)return{show:!1,hasMessages:f}}return!a.hasTriedToLoadMessages&&b.hasTriedToLoadMessages&&i?{show:!0,hasMessages:f,user:d}:a.hasTriedToLoadMessages&&!b.hasTriedToLoadMessages&&h?{show:!1,hasMessages:e}:null},H=function(a,b){var c=E(a),d=E(b);return c||d?c&&!d?!c.isblocked&&null:!c&&d?!!d.isblocked||null:!(c.isblocked||!d.isblocked)||!(c.isblocked&&!d.isblocked)&&null:null},I=function(a,b){var c=E(a),d=E(b);return c||d?c&&!d?!c.canmessage||null:!c&&d?!d.canmessage||null:!(!c.canmessage&&d.canmessage)&&(!(!c.canmessage||d.canmessage)||null):null},J=function(a,b){var c=o(a,b),d=C(a,b),e=G(a,b),f=H(a,b),g=I(a,b),h=null!==e?e.show&&e.hasMessages:null,i=E(b),j=function(a,c){if(a)return c;if(null!==a&&!a){if(!i)return{type:"content"};if(i.isblocked)return{type:"unblock"};if(b.messages.length&&F(b.loggedInUserId,i))return{type:"add-contact",user:i};if(!i.canmessage||i.requirescontact&&!i.iscontact)return{type:"unable-to-message"}}return null};if(null===c&&null===d&&null===e&&null===f)return null;for(var k=[[c,{type:"placeholder"}],[d,{type:"edit-mode"}],[g,{type:"unable-to-message"}],[f,{type:"unblock"}],[h,{type:"add-contact",user:i}]],l=0;l0,i=g.length>0,j=a.messages.length>0,k=a.messages.length>0;return h||!i||e.iscontact||k?!(d&&!d.iscontact&&i&&e.iscontact)&&(!(h&&!i)&&(!(!j&&k)&&null)):e.fullname},N=function(b,d){var e={all:{reset:L,conversation:j,scrollToMessage:m,loadingMembers:n,loadingFirstMessages:o,loadingMessages:p,sendingMessage:q,confirmDeleteSelectedMessages:v,inEditMode:C,selectedMessages:D,isFavourite:z}};e[c.CONVERSATION_TYPES.PRIVATE]={header:k,footer:J,confirmBlockUser:r,confirmUnblockUser:s,confirmAddContact:t,confirmRemoveContact:u,confirmContactRequest:x,confirmDeleteConversation:w,isBlocked:y,isContact:A,loadingConfirmAction:B,requireAddContact:G,contactRequestSent:M},e[c.CONVERSATION_TYPES.PUBLIC]={header:l,footer:K};var f=a.extend({},e.all);return d.type&&d.type in e&&(f=a.extend(f,e[d.type])),Object.keys(f).reduce(function(a,c){var e=f[c],g=e(b,d);return null!==g&&(a[c]=g),a},{})};return{buildPatch:N}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_conversation_renderer.min.js b/message/amd/build/message_drawer_view_conversation_renderer.min.js new file mode 100644 index 00000000000..8fd856c43b0 --- /dev/null +++ b/message/amd/build/message_drawer_view_conversation_renderer.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/str","core/templates","core/user_date","core_message/message_drawer_view_conversation_constants"],function(a,b,c,d,e,f){var g=f.SELECTORS,h=f.TEMPLATES,i=f.CONVERSATION_TYPES,j=function(a){return a.find(g.CONTENT_MESSAGES_CONTAINER)},k=function(a){j(a).removeClass("hidden")},l=function(a){j(a).addClass("hidden")},m=function(a){return a.find(g.CONTACT_REQUEST_SENT_MESSAGE_CONTAINER)},n=function(a){return m(a).addClass("hidden")},o=function(a){return a.find(g.CONTENT_MESSAGES_FOOTER_CONTAINER)},p=function(a){o(a).removeClass("hidden")},q=function(a){o(a).addClass("hidden")},r=function(a){return a.find(g.CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER)},s=function(a){r(a).removeClass("hidden")},t=function(a){r(a).addClass("hidden")},u=function(a){return a.find(g.PLACEHOLDER_CONTAINER)},v=function(a){u(a).removeClass("hidden")},w=function(a){u(a).addClass("hidden")},x=function(a){return a.find(g.CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER)},y=function(a){x(a).removeClass("hidden")},z=function(a){x(a).addClass("hidden")},A=function(a){return a.find(g.CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER)},B=function(a){A(a).removeClass("hidden")},C=function(a){A(a).addClass("hidden")},D=function(a){return a.find(g.CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER)},E=function(a){D(a).removeClass("hidden")},F=function(a){D(a).addClass("hidden")},G=function(a){N(a),Q(a),T(a)},H=function(a){q(a),t(a),w(a),z(a),C(a),F(a)},I=function(a){return a.find(g.CONTENT_PLACEHOLDER_CONTAINER)},J=function(a){I(a).removeClass("hidden")},K=function(a){I(a).addClass("hidden")},L=function(a){return a.find(g.HEADER)},M=function(a){L(a).removeClass("hidden")},N=function(a){L(a).addClass("hidden")},O=function(a){return a.find(g.HEADER_EDIT_MODE)},P=function(a){O(a).removeClass("hidden")},Q=function(a){O(a).addClass("hidden")},R=function(a){return a.find(g.HEADER_PLACEHOLDER_CONTAINER)},S=function(a){R(a).removeClass("hidden")},T=function(a){R(a).addClass("hidden")},U=function(a){return a.find(g.MESSAGE_TEXT_AREA)},V=function(a,b){var c=j(a);return c.find('[data-message-id="'+b+'"]')},W=function(a,b){var c=j(a);return c.find('[data-day-id="'+b+'"]')},X=function(a){return a.find(g.MORE_MESSAGES_LOADING_ICON_CONTAINER)},Y=function(a){X(a).removeClass("hidden")},Z=function(a){X(a).addClass("hidden")},$=function(a){a.find(g.SEND_MESSAGE_BUTTON).prop("disabled",!0),U(a).prop("disabled",!0)},_=function(a){a.find(g.SEND_MESSAGE_BUTTON).prop("disabled",!1),U(a).prop("disabled",!1)},aa=function(a){$(a),a.find(g.SEND_MESSAGE_ICON_CONTAINER).addClass("hidden"),a.find(g.LOADING_ICON_CONTAINER).removeClass("hidden")},ba=function(a){_(a),a.find(g.SEND_MESSAGE_ICON_CONTAINER).removeClass("hidden"),a.find(g.LOADING_ICON_CONTAINER).addClass("hidden")},ca=function(a){var b=U(a);b.val(""),b.focus()},da=function(a){return a.find(g.CONFIRM_DIALOGUE_CONTAINER)},ea=function(a){var b=da(a),c=b.siblings(":not(.hidden)");c.attr("aria-hidden",!0),c.attr("tabindex",-1),c.attr("data-confirm-dialogue-hidden",!0),b.removeClass("hidden")},fa=function(a){var b=da(a),c=b.siblings('[data-confirm-dialogue-hidden="true"]');c.removeAttr("aria-hidden"),c.removeAttr("tabindex"),c.removeAttr("data-confirm-dialogue-hidden"),b.addClass("hidden")},ga=function(a,b){O(a).find(g.MESSAGES_SELECTED_COUNT).text(b)},ha=function(a,b){return a.map(function(a){return{id:a.id,isread:a.isRead,fromloggedinuser:a.fromLoggedInUser,userfrom:a.userFrom,text:a.text,formattedtime:b[a.timeCreated]}})},ia=function(b,c,e,f,g){var i=j(c),k=f.map(function(a){return d.render(h.DAY,{timestamp:a.value.timestamp,messages:ha(a.value.messages,g)})});return a.when.apply(a,k).then(function(){f.forEach(function(b,d){k[d].then(function(d){if(b.before){var e=W(c,b.before.timestamp);return a(d).insertBefore(e)}return i.append(d)})["catch"](function(){})})})},ja=function(b,c,e,f,i){var j=f.map(function(a){var b=ha([a.value],i);return d.render(h.MESSAGE,b[0])});return a.when.apply(a,j).then(function(){f.forEach(function(b,d){j[d].then(function(d){if(b.before){var e=V(c,b.before.id);return a(d).insertBefore(e)}var f=W(c,b.day.timestamp),h=f.find(g.DAY_MESSAGES_CONTAINER);return h.append(d)})["catch"](function(){})})})},ka=function(a,b){b.forEach(function(b){W(a,b.timestamp).remove()})},la=function(a,b){b.forEach(function(b){V(a,b.id).remove()})},ma=function(b,d,f,g){var h=[],i=g.days.add.length>0,j=g.messages.add.length>0,k=[],l=a.Deferred().resolve({}).promise();return i&&(k=k.concat(g.days.add.reduce(function(a,b){return a.concat(b.value.messages.map(function(a){return a.timeCreated}))},[]))),j&&(k=k.concat(g.messages.add.map(function(a){return a.value.timeCreated}))),k.length&&(l=c.get_string("strftimetime24","core_langconfig").then(function(a){var b=k.map(function(b){return{timestamp:b,format:a}});return e.get(b)}).then(function(a){return k.reduce(function(b,c,d){return b[c]=a[d],b},{})})),i&&h.push(l.then(function(a){return ia(b,d,f,g.days.add,a)})),j&&h.push(l.then(function(a){return ja(b,d,f,g.messages.add,a)})),g.days.remove.length>0&&ka(d,g.days.remove),g.messages.remove.length>0&&la(d,g.messages.remove),a.when.apply(a,h)},na=function(a,b,c,e){var f=L(a),g=h.HEADER_PUBLIC;return e.type==i.PRIVATE&&(g=e.showControls?h.HEADER_PRIVATE:h.HEADER_PRIVATE_NO_CONTROLS),d.render(g,e.context).then(function(a,b){d.replaceNodeContents(f,a,b)})},oa=function(a,b,d,e){switch(H(d),e.type){case"placeholder":return v(d);case"add-contact":return c.get_strings([{key:"requirecontacttomessage",component:"core_message",param:e.user.fullname},{key:"isnotinyourcontacts",component:"core_message",param:e.user.fullname}]).then(function(a){var b=a[1],c=a[0],e=x(d);return e.find(g.TITLE).text(b),e.find(g.TEXT).text(c),y(d),a});case"edit-mode":return s(d);case"content":return p(d);case"unblock":return B(d);case"unable-to-message":return E(d)}return!0},pa=function(a,b,c,d){var e=j(b),f=V(b,d),g=f.position();if(g){var h=e.scrollTop()+g.top;e.scrollTop(h)}},qa=function(a,b,c,d){d?(N(a),S(a)):(M(a),T(a))},ra=function(a,b,c,d){d?(l(b),J(b)):(k(b),K(b))},sa=function(a,b,c,d){d?Y(b):Z(b)},ta=function(a,b,c,d){d?aa(c):(ba(c),ca(c))},ua=function(a,b,c,d,e,f,h,i){var j=da(b),k=d.map(function(a){return j.find(a)}),l=j.find(g.CONFIRM_DIALOGUE_CANCEL_BUTTON),m=j.find(g.CONFIRM_DIALOGUE_TEXT),n=j.find(g.CONFIRM_DIALOGUE_HEADER);j.find("button").addClass("hidden"),h?l.removeClass("hidden"):l.addClass("hidden"),f?(n.removeClass("hidden"),n.text(f)):(n.addClass("hidden"),n.text("")),k.forEach(function(a){a.removeClass("hidden")}),m.text(e),ea(c),ea(b),i||ea(a),j.find(g.CAN_RECEIVE_FOCUS).first().focus()},va=function(a,b,c){var d=da(b),e=d.find(g.CONFIRM_DIALOGUE_CANCEL_BUTTON),f=d.find(g.CONFIRM_DIALOGUE_TEXT),h=d.find(g.CONFIRM_DIALOGUE_HEADER);return fa(b),fa(c),fa(a),d.find("button").addClass("hidden"),e.removeClass("hidden"),f.text(""),h.addClass("hidden"),h.text(""),a.find(g.CAN_RECEIVE_FOCUS).first().focus(),!0},wa=function(a,b,d,e){return e?c.get_string("blockuserconfirm","core_message",e.fullname).then(function(c){return ua(a,b,d,[g.ACTION_CONFIRM_BLOCK],c,"",!0,!1)}):va(a,b,d)},xa=function(a,b,d,e){return e?c.get_string("unblockuserconfirm","core_message",e.fullname).then(function(c){return ua(a,b,d,[g.ACTION_CONFIRM_UNBLOCK],c,"",!0,!1)}):va(a,b,d)},ya=function(a,b,d,e){return e?c.get_string("addcontactconfirm","core_message",e.fullname).then(function(c){return ua(a,b,d,[g.ACTION_CONFIRM_ADD_CONTACT],c,"",!0,!1)}):va(a,b,d)},za=function(a,b,d,e){return e?c.get_string("removecontactconfirm","core_message",e.fullname).then(function(c){return ua(a,b,d,[g.ACTION_CONFIRM_REMOVE_CONTACT],c,"",!0,!1)}):va(a,b,d)},Aa=function(a,b,d,e){return e?c.get_string("deleteselectedmessagesconfirm","core_message").then(function(c){return ua(a,b,d,[g.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES],c,"",!0,!1)}):va(a,b,d)},Ba=function(a,b,d,e){return e?c.get_string("deleteallconfirm","core_message").then(function(c){return ua(a,b,d,[g.ACTION_CONFIRM_DELETE_CONVERSATION],c,"",!0,!1)}):va(a,b,d)},Ca=function(a,b,d,e){return e?c.get_string("userwouldliketocontactyou","core_message",e.fullname).then(function(c){var e=[g.ACTION_ACCEPT_CONTACT_REQUEST,g.ACTION_DECLINE_CONTACT_REQUEST];return ua(a,b,d,e,c,"",!1,!0)}):va(a,b,d)},Da=function(a,b,c,d){d?(a.find(g.ACTION_REQUEST_BLOCK).addClass("hidden"),a.find(g.ACTION_REQUEST_UNBLOCK).removeClass("hidden")):(a.find(g.ACTION_REQUEST_BLOCK).removeClass("hidden"),a.find(g.ACTION_REQUEST_UNBLOCK).addClass("hidden"))},Ea=function(a,b,c,d){d?(a.find(g.FAVOURITE_ICON_CONTAINER).removeClass("hidden"),a.find(g.ACTION_CONFIRM_FAVOURITE).addClass("hidden"),a.find(g.ACTION_CONFIRM_UNFAVOURITE).removeClass("hidden")):(a.find(g.FAVOURITE_ICON_CONTAINER).addClass("hidden"),a.find(g.ACTION_CONFIRM_FAVOURITE).removeClass("hidden"),a.find(g.ACTION_CONFIRM_UNFAVOURITE).addClass("hidden"))},Fa=function(a,b,c,d){var e=a.find(g.ACTION_REQUEST_ADD_CONTACT),f=a.find(g.ACTION_REQUEST_REMOVE_CONTACT);switch(d){case"pending-contact":e.addClass("hidden"),f.addClass("hidden");break;case"contact":e.addClass("hidden"),f.removeClass("hidden");break;case"non-contact":e.removeClass("hidden"),f.addClass("hidden")}},Ga=function(a,b,c,d){var e=da(b),f=e.find("button"),h=e.find(g.CONFIRM_DIALOGUE_BUTTON_TEXT),i=e.find(g.LOADING_ICON_CONTAINER);d?(f.prop("disabled",!0),h.addClass("hidden"),i.removeClass("hidden")):(f.prop("disabled",!1),h.removeClass("hidden"),i.addClass("hidden"))},Ha=function(a,b,c,d){var e=null;d?(e=b.find(g.MESSAGE_NOT_SELECTED),e.find(g.MESSAGE_NOT_SELECTED_ICON).removeClass("hidden"),N(a),P(a)):(e=j(b),e.find(g.MESSAGE_NOT_SELECTED_ICON).addClass("hidden"),e.find(g.MESSAGE_SELECTED_ICON).addClass("hidden"),M(a),Q(a))},Ia=function(a,b,c,d){var e=d.count>0;d.add.length&&d.add.forEach(function(a){var c=V(b,a);c.find(g.MESSAGE_NOT_SELECTED_ICON).addClass("hidden"),c.find(g.MESSAGE_SELECTED_ICON).removeClass("hidden"),c.attr("aria-checked",!0)}),d.remove.length&&d.remove.forEach(function(a){var c=V(b,a);e&&c.find(g.MESSAGE_NOT_SELECTED_ICON).removeClass("hidden"),c.find(g.MESSAGE_SELECTED_ICON).addClass("hidden"),c.attr("aria-checked",!1)}),ga(a,d.count)},Ja=function(a,b,d,e){return e.show&&!e.hasMessages?c.get_strings([{key:"requirecontacttomessage",component:"core_message",param:e.user.fullname},{key:"isnotinyourcontacts",component:"core_message",param:e.user.fullname}]).then(function(c){var e=c[1],f=c[0];return ua(a,b,d,[g.ACTION_REQUEST_ADD_CONTACT],f,e,!1,!0)}):va(a,b,d)},Ka=function(a,b,d,e){var f=m(b);return e?c.get_string("yourcontactrequestpending","core_message",e).then(function(a){return f.find(g.TEXT).text(a),f.removeClass("hidden"),a}):(f.addClass("hidden"),!0)},La=function(a,b,c){return va(a,b,c),n(b),G(a),S(a),H(c),v(c),!0},Ma=function(c,d,e,f){var g=[{reset:La},{conversation:ma,header:na,footer:oa,confirmBlockUser:wa,confirmUnblockUser:xa,confirmAddContact:ya,confirmRemoveContact:za,confirmDeleteSelectedMessages:Aa,confirmDeleteConversation:Ba,confirmContactRequest:Ca,requireAddContact:Ja,contactRequestSent:Ka},{loadingMembers:qa,loadingFirstMessages:ra,loadingMessages:sa,sendingMessage:ta,isBlocked:Da,isContact:Fa,isFavourite:Ea,loadingConfirmAction:Ga,inEditMode:Ha},{scrollToMessage:pa,selectedMessages:Ia}],h=function(a){var b=[];for(var g in f)if(a.hasOwnProperty(g)){var h=a[g],i=f[g];b.push(h(c,d,e,i))}return b},i=h(g[0]);return i=i.concat(h(g[1])),a.when.apply(a,i).then(function(){for(var a=2;ab.timeCreated?1:0}),e.messages=g.filter(function(a,b,c){return!b||a.id!==c[b-1].id}),e},f=function(a,c){var d=b(a),e=c.map(function(a){return a.id});return d.messages=d.messages.filter(function(a){return e.indexOf(a.id)<0}),d},g=function(a,c){var d=b(a);return d.messages=d.messages.filter(function(a){return c.indexOf(a.id)<0}),d},h=function(a,c){var d=b(a);return c.forEach(function(a){d.members[a.id]=a}),d},i=function(a,c){var d=b(a);return c.forEach(function(a){delete d.members[a.id]}),d},j=function(a,c){var d=b(a);return d.loadingMessages=c,a.loadingMessages&&!c&&(d.hasTriedToLoadMessages=!0),d},k=function(a,c){var d=b(a);return d.sendingMessage=c,d},l=function(a,c){var d=b(a);return d.loadingMembers=c,d},m=function(a,c){var d=b(a);return d.id=c,d},n=function(a,c){var d=b(a);return d.name=c,d},o=function(a,c){var d=b(a);return d.subname=c,d},p=function(a,c){var d=b(a);return d.type=c,d},q=function(a,c){var d=b(a);return d.isFavourite=c,d},r=function(a,c){var d=b(a);return d.totalMemberCount=c,d},s=function(a,c){var d=b(a);return d.imageUrl=c,d},t=function(a,c){var d=b(a);return d.loadingConfirmAction=c,d},u=function(a,c){var d=b(a);return d.pendingDeleteConversation=c,d},v=function(a,c){var d=b(a);return c.forEach(function(a){d.pendingBlockUserIds.push(a)}),d},w=function(a,c){var d=b(a);return c.forEach(function(a){d.pendingRemoveContactIds.push(a)}),d},x=function(a,c){var d=b(a);return c.forEach(function(a){d.pendingUnblockUserIds.push(a)}),d},y=function(a,c){var d=b(a);return c.forEach(function(a){d.pendingAddContactIds.push(a)}),d},z=function(a,c){var d=b(a);return c.forEach(function(a){d.pendingDeleteMessageIds.push(a)}),d},A=function(a,c){var d=b(a);return d.pendingBlockUserIds=d.pendingBlockUserIds.filter(function(a){return c.indexOf(a)<0}),d},B=function(a,c){var d=b(a);return d.pendingRemoveContactIds=d.pendingRemoveContactIds.filter(function(a){return c.indexOf(a)<0}),d},C=function(a,c){var d=b(a);return d.pendingUnblockUserIds=d.pendingUnblockUserIds.filter(function(a){return c.indexOf(a)<0}),d},D=function(a,c){var d=b(a);return d.pendingAddContactIds=d.pendingAddContactIds.filter(function(a){return c.indexOf(a)<0}),d},E=function(a,c){var d=b(a);return d.pendingDeleteMessageIds=d.pendingDeleteMessageIds.filter(function(a){return c.indexOf(a)<0}),d},F=function(a,c){var d=b(a);return d.selectedMessageIds=d.selectedMessageIds.concat(c),d},G=function(a,c){var d=b(a);return d.selectedMessageIds=d.selectedMessageIds.filter(function(a){return c.indexOf(a)<0}),d},H=function(a,c){var d=b(a),e=c.map(function(a){return a.id});return d.messages=d.messages.map(function(a){return e.indexOf(a.id)>=0&&(a.isRead=!0),a}),d},I=function(a,c){var d=b(a);return c.forEach(function(a){var b=a.userid,c=a.requesteduserid;d.members[b].contactrequests.push(a),d.members[c].contactrequests.push(a)}),d},J=function(a,c){var d=b(a);return c.forEach(function(a){var b=a.userid,c=a.requesteduserid;d.members[b].contactrequests=d.members[b].contactrequests.filter(function(a){return a.userid!=b}),d.members[c].contactrequests=d.members[c].contactrequests.filter(function(a){return a.requesteduserid!=c})}),d};return{buildInitialState:d,addMessages:e,removeMessages:f,removeMessagesById:g,addMembers:h,removeMembers:i,setLoadingMessages:j,setSendingMessage:k,setLoadingMembers:l,setId:m,setName:n,setSubname:o,setType:p,setIsFavourite:q,setTotalMemberCount:r,setImageUrl:s,setLoadingConfirmAction:t,setPendingDeleteConversation:u,addPendingBlockUsersById:v,addPendingRemoveContactsById:w,addPendingUnblockUsersById:x,addPendingAddContactsById:y,addPendingDeleteMessagesById:z,removePendingBlockUsersById:A,removePendingRemoveContactsById:B,removePendingUnblockUsersById:C,removePendingAddContactsById:D,removePendingDeleteMessagesById:E,addSelectedMessagesById:F,removeSelectedMessagesById:G,markMessagesAsRead:H,addContactRequests:I,removeContactRequests:J}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_group_info.min.js b/message/amd/build/message_drawer_view_group_info.min.js new file mode 100644 index 00000000000..ad2eba3aa16 --- /dev/null +++ b/message/amd/build/message_drawer_view_group_info.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/templates","core_message/message_repository","core_message/message_drawer_lazy_load_list"],function(a,b,c,d,e){var f=50,g={CONTENT_CONTAINER:'[data-region="group-info-content-container"]',MEMBERS_LIST:'[data-region="members-list"]'},h={CONTENT:"core_message/message_drawer_view_group_info_body_content",MEMBERS_LIST:"core_message/message_drawer_view_group_info_participants_list"},i=function(a){return a.find(g.CONTENT_CONTAINER)},j=function(a,b,d){var e=b.totalMemberCount>50?50:b.totalMemberCount,f=Array.apply(null,Array(e)).map(function(){return!0}),g={name:b.name,subname:b.subname,imageurl:b.imageUrl,placeholders:f,loggedinuser:{id:d}};return c.render(h.CONTENT,g).then(function(b){return i(a).append(b),b})},k=function(a,b,c){return function(f,g){return d.getConversationMembers(a.id,g,b+1,c).then(function(a){return a.length>b?a=a.slice(0,-1):e.setLoadedAll(f,!0),c+=b,a})}},l=function(a,b){return c.render(h.MEMBERS_LIST,{contacts:b}).then(function(b){return a.append(b),b})},m=function(b,c,d){return b=a(b),i(b).empty(),j(b,c,d).then(function(){var a=e.getRoot(b);e.show(a,k(c,f,0),l)})},n=function(a,c){return b.get_string("messagedrawerviewgroupinfo","core_message",c.name)};return{show:m,description:n}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_overview.min.js b/message/amd/build/message_drawer_view_overview.min.js new file mode 100644 index 00000000000..d050e94db06 --- /dev/null +++ b/message/amd/build/message_drawer_view_overview.min.js @@ -0,0 +1 @@ +define(["jquery","core/key_codes","core/pubsub","core/str","core_message/message_drawer_view_overview_section_favourites","core_message/message_drawer_view_overview_section_group_messages","core_message/message_drawer_view_overview_section_messages","core_message/message_drawer_router","core_message/message_drawer_routes","core_message/message_drawer_events"],function(a,b,c,d,e,f,g,h,i,j){var k={CONTACT_REQUEST_COUNT:'[data-region="contact-request-count"]',FAVOURITES:'[data-region="view-overview-favourites"]',GROUP_MESSAGES:'[data-region="view-overview-group-messages"]',MESSAGES:'[data-region="view-overview-messages"]',SEARCH_INPUT:'[data-region="view-overview-search-input"]'},l=function(a){return a.find(k.SEARCH_INPUT)},m=function(a){return function(){var b=a.find(k.CONTACT_REQUEST_COUNT),c=parseInt(b.text(),10);c=isNaN(c)?0:c-1,c<=0?b.addClass("hidden"):b.text(c)}},n=function(a){var d=l(a),e=[b.tab,b.shift,b.ctrl,b.alt];d.on("click",function(){h.go(i.VIEW_SEARCH)}),d.on("keydown",function(a){e.indexOf(a.keyCode)<0&&"Meta"!=a.key&&h.go(i.VIEW_SEARCH)}),c.subscribe(j.CONTACT_REQUEST_ACCEPTED,m(a)),c.subscribe(j.CONTACT_REQUEST_DECLINED,m(a))},o=function(b,c){return b.attr("data-init")||(n(b),b.attr("data-init",!0)),l(b).val(""),a.when(e.show(c.find(k.FAVOURITES)),f.show(c.find(k.GROUP_MESSAGES)),g.show(c.find(k.MESSAGES)))},p=function(){return d.get_string("messagedrawerviewoverview","core_message")};return{show:o,description:p}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_overview_section.min.js b/message/amd/build/message_drawer_view_overview_section.min.js new file mode 100644 index 00000000000..8f69ddb8cca --- /dev/null +++ b/message/amd/build/message_drawer_view_overview_section.min.js @@ -0,0 +1 @@ +define(["jquery","core/custom_interaction_events","core/notification","core/pubsub","core/str","core/templates","core/user_date","core_message/message_repository","core_message/message_drawer_events","core_message/message_drawer_router","core_message/message_drawer_routes","core_message/message_drawer_lazy_load_list","core_message/message_drawer_view_conversation_constants"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={TOGGLE:'[data-region="toggle"]',CONVERSATION:"[data-conversation-id]",BLOCKED_ICON_CONTAINER:'[data-region="contact-icon-blocked"]',LAST_MESSAGE:'[data-region="last-message"]',LAST_MESSAGE_DATE:'[data-region="last-message-date"]',UNREAD_COUNT:'[data-region="unread-count"]',SECTION_TOTAL_COUNT:'[data-region="section-total-count"]',SECTION_UNREAD_COUNT:'[data-region="section-unread-count"]'},o={CONVERSATIONS_LIST:"core_message/message_drawer_conversations_list"},p=50,q={},r=function(a){return l.getRoot(a).hasClass("show")},s=function(a){a.addClass("expanded")},t=function(a){a.removeClass("expanded")},u=function(b,d,e){var g=d.map(function(b){var c=b.messages.length?b.messages[b.messages.length-1]:null,d={id:b.id,imageurl:b.imageurl,name:b.name,subname:b.subname,unreadcount:b.unreadcount,lastmessagedate:c?c.timecreated:null,sentfromcurrentuser:c?c.useridfrom==e:null,lastmessage:c?a(c.text).text()||c.text:null};if(b.type==m.CONVERSATION_TYPES.PRIVATE){var f=b.members.reduce(function(a,b){return a||b.id==e||(a=b),a},null);d.showonlinestatus=f.showonlinestatus,d.isonline=f.isosnline,d.isblocked=f.isblocked}return d});return f.render(o.CONVERSATIONS_LIST,{conversations:g}).then(function(a){return b.append(a),a})["catch"](c.exception)},v=function(a,b,d){return function(e,f){return h.getConversations(f,a,p+1,d,b).then(function(a){var b=a.conversations;return b.length>p?b=b.slice(0,-1):l.setLoadedAll(e,!0),d+=p,b.forEach(function(a){q[a.id]=a}),b})["catch"](c.exception)}},w=function(a){return a.find(n.SECTION_TOTAL_COUNT)},x=function(a){return a.find(n.SECTION_UNREAD_COUNT)},y=function(a){var b=w(a),c=parseInt(b.text());c+=1,b.text(c)},z=function(a){var b=w(a),c=parseInt(b.text());c-=1,b.text(c)},A=function(a){var b=x(a),c=parseInt(b.text());c-=1,b.text(c),c<1&&b.addClass("hidden")},B=function(a,b){return a.find('[data-conversation-id="'+b+'"]')},C=function(a){a.find(n.BLOCKED_ICON_CONTAINER).removeClass("hidden")},D=function(a){a.find(n.BLOCKED_ICON_CONTAINER).addClass("hidden")},E=function(b,d){var f=d.messages[d.messages.length-1],h="",i=[{key:"you",component:"core_message"},{key:"strftimetime24",component:"core_langconfig"}];return e.get_strings(i).then(function(a){return h=a[0],g.get([{timestamp:f.timeCreated,format:a[1]}])}).then(function(a){return a[0]}).then(function(c){var d=a(f.text).text();return f.fromLoggedInUser&&(d=h+" "+d),b.find(n.LAST_MESSAGE).html(d),b.find(n.LAST_MESSAGE_DATE).text(c).removeClass("hidden"),c})["catch"](c.exception)},F=function(b,d){var e=b.find(n.CONVERSATION),g="";if(!e.length){var h=l.getRoot(b);l.showContent(h),l.hideEmptyMessage(h)}var i=d.messages.length,j=i?d.messages[i-1]:null;j&&(g=a(j.text).text()||j.text);var k={id:d.id,name:d.name,subname:d.subname,lastmessagedate:j?j.timeCreated:null,sentfromcurrentuser:j?j.fromLoggedInUser:null,lastmessage:g,imageurl:d.imageUrl};return f.render(o.CONVERSATIONS_LIST,{conversations:[k]}).then(function(a){var c=l.getContentContainer(b);return c.prepend(a)}).then(function(){return y(b)})["catch"](c.exception)},G=function(a,b){b.remove(),z(a);var c=a.find(n.CONVERSATION);if(!c.length){var d=l.getRoot(a);l.hideContent(d),l.showEmptyMessage(d)}},H=function(a,b){var c=b.find(n.UNREAD_COUNT);c.text("0"),c.addClass("hidden"),A(a)},I=function(c,e,f,g){var h=l.getRoot(c),m=c.find(n.TOGGLE);c.css("min-height",m.outerHeight()),c.on("show.bs.collapse",function(){s(c),l.show(h,e,u)}),c.on("hidden.bs.collapse",function(){t(c)}),d.subscribe(i.CONTACT_BLOCKED,function(a){var b=B(c,a);b.length&&C(b)}),d.subscribe(i.CONTACT_UNBLOCKED,function(a){var b=B(c,a);b.length&&D(b)}),d.subscribe(i.CONVERSATION_NEW_LAST_MESSAGE,function(a){if(!(f&&a.type!=f||g&&!a.isFavourite||!g&&a.isFavourite)){var b=a.id,d=B(c,b);d.length?E(d,a):F(c,a)}}),d.subscribe(i.CONVERSATION_DELETED,function(a){var b=B(c,a);b.length&&G(c,b)}),d.subscribe(i.CONVERSATION_READ,function(a){var b=B(c,a);b.length&&H(c,b)}),d.subscribe(i.CONVERSATION_SET_FAVOURITE,function(a){var b=null;!g||f&&f!=a.type?f==a.type&&(b=B(c,a.id),b.length&&G(c,b)):(b=B(c,a.id),b.length||F(c,a))}),d.subscribe(i.CONVERSATION_UNSET_FAVOURITE,function(a){var b=null;g?(b=B(c,a.id),b.length&&G(c,b)):f==a.type&&(b=B(c,a.id),b.length||F(c,a))}),b.define(c,[b.events.activate]),c.on(b.events.activate,n.CONVERSATION,function(b,c){var d=a(b.target).closest(n.CONVERSATION),e=d.attr("data-conversation-id"),f=q[e];j.go(k.VIEW_CONVERSATION,f),c.originalEvent.preventDefault()})},J=function(b,c,d){if(b=a(b),!b.attr("data-init")){var e=v(c,d,0);if(I(b,e,c,d),r(b)){s(b);var f=l.getRoot(b);l.show(f,e,u)}b.attr("data-init",!0)}};return{show:J}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_overview_section_favourites.min.js b/message/amd/build/message_drawer_view_overview_section_favourites.min.js new file mode 100644 index 00000000000..fce097f2b24 --- /dev/null +++ b/message/amd/build/message_drawer_view_overview_section_favourites.min.js @@ -0,0 +1 @@ +define(["jquery","core_message/message_drawer_view_overview_section"],function(a,b){var c=null,d=!0,e=function(e){b.show(a(e),c,d)};return{show:e}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_overview_section_group_messages.min.js b/message/amd/build/message_drawer_view_overview_section_group_messages.min.js new file mode 100644 index 00000000000..0c3bb2b8001 --- /dev/null +++ b/message/amd/build/message_drawer_view_overview_section_group_messages.min.js @@ -0,0 +1 @@ +define(["jquery","core_message/message_drawer_view_overview_section"],function(a,b){var c=2,d=!1,e=function(e){e=a(e),b.show(a(e),c,d)};return{show:e}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_overview_section_messages.min.js b/message/amd/build/message_drawer_view_overview_section_messages.min.js new file mode 100644 index 00000000000..4b2c2ba734d --- /dev/null +++ b/message/amd/build/message_drawer_view_overview_section_messages.min.js @@ -0,0 +1 @@ +define(["jquery","core_message/message_drawer_view_overview_section"],function(a,b){var c=1,d=!1,e=function(e){b.show(a(e),c,d)};return{show:e}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_search.min.js b/message/amd/build/message_drawer_view_search.min.js new file mode 100644 index 00000000000..00b2f64fb16 --- /dev/null +++ b/message/amd/build/message_drawer_view_search.min.js @@ -0,0 +1 @@ +define(["jquery","core/custom_interaction_events","core/notification","core/pubsub","core/str","core/templates","core_message/message_repository","core_message/message_drawer_events"],function(a,b,c,d,e,f,g,h){var i=50,j=50,k=3,l={BLOCK_ICON_CONTAINER:'[data-region="block-icon-container"]',CANCEL_SEARCH_BUTTON:'[data-action="cancel-search"]',CONTACTS_CONTAINER:'[data-region="contacts-container"]',CONTACTS_LIST:'[data-region="contacts-container"] [data-region="list"]',EMPTY_MESSAGE_CONTAINER:'[data-region="empty-message-container"]',LIST:'[data-region="list"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',LOADING_PLACEHOLDER:'[data-region="loading-placeholder"]',MESSAGES_LIST:'[data-region="messages-container"] [data-region="list"]',MESSAGES_CONTAINER:'[data-region="messages-container"]',NON_CONTACTS_CONTAINER:'[data-region="non-contacts-container"]',NON_CONTACTS_LIST:'[data-region="non-contacts-container"] [data-region="list"]',SEARCH_ICON_CONTAINER:'[data-region="search-icon-container"]',SEARCH_ACTION:'[data-action="search"]',SEARCH_INPUT:'[data-region="search-input"]',SEARCH_RESULTS_CONTAINER:'[data-region="search-results-container"]',LOAD_MORE_USERS:'[data-action="load-more-users"]',LOAD_MORE_MESSAGES:'[data-action="load-more-messages"]',BUTTON_TEXT:'[data-region="button-text"]',NO_RESULTS_CONTAINTER:'[data-region="no-results-container"]'},m={CONTACTS_LIST:"core_message/message_drawer_contacts_list",NON_CONTACTS_LIST:"core_message/message_drawer_non_contacts_list",MESSAGES_LIST:"core_message/message_drawer_messages_list"},n=function(a){return a.attr("data-user-id")},o=function(a){return a.find(l.EMPTY_MESSAGE_CONTAINER)},p=function(a){return a.find(l.LOADING_ICON_CONTAINER)},q=function(a){return a.find(l.LOADING_PLACEHOLDER)},r=function(a){return a.find(l.SEARCH_ICON_CONTAINER)},s=function(a){return a.find(l.SEARCH_INPUT)},t=function(a){return a.find(l.SEARCH_RESULTS_CONTAINER)},u=function(a){return a.find(l.CONTACTS_CONTAINER)},v=function(a){return a.find(l.NON_CONTACTS_CONTAINER)},w=function(a){return a.find(l.MESSAGES_CONTAINER)},x=function(a){o(a).removeClass("hidden")},y=function(a){o(a).addClass("hidden")},z=function(a){p(a).removeClass("hidden")},A=function(a){p(a).addClass("hidden")},B=function(a){q(a).removeClass("hidden")},C=function(a){q(a).addClass("hidden")},D=function(a){r(a).removeClass("hidden")},E=function(a){r(a).addClass("hidden")},F=function(a){t(a).removeClass("hidden")},G=function(a){t(a).addClass("hidden")},H=function(a){s(a).prop("disabled",!0)},I=function(a){s(a).prop("disabled",!1)},J=function(a){s(a).val("")},K=function(a){a.find(l.CONTACTS_LIST).empty(),a.find(l.NON_CONTACTS_LIST).empty(),a.find(l.MESSAGES_LIST).empty(),a.find(l.NO_RESULTS_CONTAINTER).addClass("hidden"),P(a),T(a)},L=function(a,b){E(a),y(b),G(b),z(a),B(b),H(a)},M=function(a,b){D(a),y(b),F(b),A(a),C(b),I(a)},N=function(a){var b=a.find(l.LOAD_MORE_USERS);b.prop("disabled",!0),b.find(l.BUTTON_TEXT).addClass("hidden"),b.find(l.LOADING_ICON_CONTAINER).removeClass("hidden")},O=function(a){var b=a.find(l.LOAD_MORE_USERS);b.prop("disabled",!1),b.find(l.BUTTON_TEXT).removeClass("hidden"),b.find(l.LOADING_ICON_CONTAINER).addClass("hidden")},P=function(a){a.find(l.LOAD_MORE_USERS).removeClass("hidden")},Q=function(a){a.find(l.LOAD_MORE_USERS).addClass("hidden")},R=function(a){var b=a.find(l.LOAD_MORE_MESSAGES);b.prop("disabled",!0),b.find(l.BUTTON_TEXT).addClass("hidden"),b.find(l.LOADING_ICON_CONTAINER).removeClass("hidden")},S=function(a){var b=a.find(l.LOAD_MORE_MESSAGES);b.prop("disabled",!1),b.find(l.BUTTON_TEXT).removeClass("hidden"),b.find(l.LOADING_ICON_CONTAINER).addClass("hidden")},T=function(a){a.find(l.LOAD_MORE_MESSAGES).removeClass("hidden")},U=function(a){a.find(l.LOAD_MORE_MESSAGES).addClass("hidden")},V=function(a,b){return a.find('[data-contact-user-id="'+b+'"]')},W=function(a,b){var c=v(a),d=V(c,b.userid);if(d.length){d.remove();var e=u(a);e.removeClass("hidden"),e.find(l.LIST).append(d)}c.find(l.LIST).children().length||c.addClass("hidden")},X=function(a,b){var c=u(a),d=V(c,b);if(d.length){d.remove();var e=v(a);e.removeClass("hidden"),e.find(l.LIST).append(d)}c.find(l.LIST).children().length||c.addClass("hidden")},Y=function(a,b){var c=V(a,b);c.length&&c.find(l.BLOCK_ICON_CONTAINER).removeClass("hidden")},Z=function(a,b){var c=V(a,b);c.length&&c.find(l.BLOCK_ICON_CONTAINER).addClass("hidden")},$=function(b,c){var d=u(b),e=d.find(l.LIST);if(c.length||e.children().length)return f.render(m.CONTACTS_LIST,{contacts:c}).then(function(a){return e.append(a),a});var g=d.find(l.NO_RESULTS_CONTAINTER);return g.removeClass("hidden"),a.Deferred().resolve("").promise()},_=function(b,c){var d=v(b),e=d.find(l.LIST);if(c.length||e.children().length)return f.render(m.NON_CONTACTS_LIST,{noncontacts:c}).then(function(a){return e.append(a),a});var g=d.find(l.NO_RESULTS_CONTAINTER);return g.removeClass("hidden"),a.Deferred().resolve("").promise()},aa=function(b,c){var d=w(b),e=d.find(l.LIST);if(c.length||e.children().length)return f.render(m.MESSAGES_LIST,{messages:c}).then(function(a){return e.append(a),a});var g=d.find(l.NO_RESULTS_CONTAINTER);return g.removeClass("hidden"),a.Deferred().resolve("").promise()},ba=function(b,c,d,e,f){var h=!1;return N(b),g.searchUsers(c,d,e+1,f).then(function(a){var b=a.contacts,c=a.noncontacts;return b.length<=e&&c.length<=e?(h=!0,{contacts:b,noncontacts:c}):{contacts:b.slice(0,e),noncontacts:c.slice(0,e)}}).then(function(c){return a.when($(b,c.contacts),_(b,c.noncontacts))}).then(function(){O(b),h&&Q(b)})["catch"](function(a){throw O(b),a})},ca=function(a,b,c,d,e){var f=!1;return R(a),g.searchMessages(b,c,d+1,e).then(function(a){var b=a.contacts;return b.length<=d?(f=!0,b):b.slice(0,d)}).then(function(b){return aa(a,b)}).then(function(){S(a),f&&U(a)})["catch"](function(b){throw S(a),b})},da=function(b,c,d,e,f,g,h){var i=n(c);return L(b,c),K(c),a.when(ba(c,i,d,e,f),ca(c,i,d,g,h)).then(function(){M(b,c)})},ea=function(a,e){var f=n(e),g=s(a),m="",o=0,p=0,q=function(b,d){m=g.val().trim(),""!==m&&(o=0,p=0,da(a,e,m,k,p,i,o).then(function(){g.focus(),p+=k,o+=i})["catch"](c.exception)),d.originalEvent.preventDefault()};b.define(g,[b.events.enter]),b.define(a,[b.events.activate]),b.define(e,[b.events.activate]),g.on(b.events.enter,q),a.on(b.events.activate,l.SEARCH_ACTION,q),e.on(b.events.activate,l.LOAD_MORE_MESSAGES,function(a,b){""!==m&&ca(e,f,m,i,o).then(function(){o+=i})["catch"](c.exception),b.originalEvent.preventDefault()}),e.on(b.events.activate,l.LOAD_MORE_USERS,function(a,b){""!==m&&ba(e,f,m,j,p).then(function(){p+=j})["catch"](c.exception),b.originalEvent.preventDefault()}),a.on(b.events.activate,l.CANCEL_SEARCH_BUTTON,function(){J(a),x(e),D(a),G(e),A(a),C(e),p=0,o=0}),d.subscribe(h.CONTACT_ADDED,function(a){W(e,a)}),d.subscribe(h.CONTACT_REMOVED,function(a){X(e,a)}),d.subscribe(h.CONTACT_BLOCKED,function(a){Y(e,a)}),d.subscribe(h.CONTACT_UNBLOCKED,function(a){Z(e,a)})},fa=function(b,c){c.attr("data-init")||(ea(b,c),c.attr("data-init",!0));var d=s(b);return d.focus(),a.Deferred().resolve().promise()},ga=function(a){var b=s(a),c=b.val().trim();return e.get_string("messagedrawerviewsearch","core_message",c)};return{show:fa,description:ga}}); \ No newline at end of file diff --git a/message/amd/build/message_drawer_view_settings.min.js b/message/amd/build/message_drawer_view_settings.min.js new file mode 100644 index 00000000000..2d465d3feab --- /dev/null +++ b/message/amd/build/message_drawer_view_settings.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/str","core_message/message_repository","core/custom_interaction_events"],function(a,b,c,d,e){var f={SETTINGS:'[data-region="settings"]',PREFERENCE_CONTROL:'[data-region="preference-control"]',PRIVACY_PREFERENCE:'[data-preference="blocknoncontacts"] input[type="radio"]',EMAIL_ENABLED_PREFERENCE:'[data-preference="emailnotifications"] input[type="checkbox"]'},g={message_provider_moodle_instantmessage_loggedoff:{type:"emailnotifications",enabled:"email",disabled:"none"},message_provider_moodle_instantmessage_loggedin:{type:"emailnotifications",enabled:"email",disabled:"none"}},h=function(c,h){var i=c.find(f.SETTINGS);e.define(i,[e.events.activate]),i.on(e.events.activate,f.EMAIL_ENABLED_PREFERENCE,function(c){var e=a(c.target),i=e.closest(f.PREFERENCE_CONTROL),j=i.attr("data-preference"),k=e.prop("checked"),l=Object.keys(g).reduce(function(a,b){var c=g[b];return c.type===j&&a.push({type:b,value:k?c.enabled:c.disabled}),a},[]);d.savePreferences(h,l)["catch"](b.exception)}),i.on(e.events.activate,f.PRIVACY_PREFERENCE,function(c){var e=a(c.target).val(),f=[{type:"message_blocknoncontacts",value:e}];d.savePreferences(h,f)["catch"](b.exception)})},i=function(b,c,d){return c.attr("data-init")||(h(c,d),c.attr("data-init",!0)),a.Deferred().resolve().promise()},j=function(){return c.get_string("messagedrawerviewsettings","core_message")};return{show:i,description:j}}); \ No newline at end of file diff --git a/message/amd/src/message_drawer.js b/message/amd/src/message_drawer.js new file mode 100644 index 00000000000..8a3852cbb67 --- /dev/null +++ b/message/amd/src/message_drawer.js @@ -0,0 +1,243 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the message drawer. + * + * @module core_message/message_drawer + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/custom_interaction_events', + 'core/pubsub', + 'core_message/message_drawer_view_contact', + 'core_message/message_drawer_view_contacts', + 'core_message/message_drawer_view_conversation', + 'core_message/message_drawer_view_group_info', + 'core_message/message_drawer_view_overview', + 'core_message/message_drawer_view_search', + 'core_message/message_drawer_view_settings', + 'core_message/message_drawer_router', + 'core_message/message_drawer_routes', + 'core_message/message_drawer_events' +], +function( + $, + CustomEvents, + PubSub, + ViewContact, + ViewContacts, + ViewConversation, + ViewGroupInfo, + ViewOverview, + ViewSearch, + ViewSettings, + Router, + Routes, + Events +) { + + var SELECTORS = { + VIEW_CONTACT: '[data-region="view-contact"]', + VIEW_CONTACTS: '[data-region="view-contacts"]', + VIEW_CONVERSATION: '[data-region="view-conversation"]', + VIEW_GROUP_INFO: '[data-region="view-group-info"]', + VIEW_OVERVIEW: '[data-region="view-overview"]', + VIEW_SEARCH: '[data-region="view-search"]', + VIEW_SETTINGS: '[data-region="view-settings"]', + ROUTES: '[data-route]', + ROUTES_BACK: '[data-route-back]', + HEADER_CONTAINER: '[data-region="header-container"]', + BODY_CONTAINER: '[data-region="body-container"]', + FOOTER_CONTAINER: '[data-region="footer-container"]', + }; + + /** + * Get elements for route. + * + * @param {Object} root The message drawer container. + * @param {string} selector The route container. + * + * @return {array} elements Found route container objects. + */ + var getElementsForRoute = function(root, selector) { + var candidates = root.children(); + var header = candidates.filter(SELECTORS.HEADER_CONTAINER).find(selector); + var body = candidates.filter(SELECTORS.BODY_CONTAINER).find(selector); + var footer = candidates.filter(SELECTORS.FOOTER_CONTAINER).find(selector); + var elements = [header, body, footer].filter(function(element) { + return element.length; + }); + + return elements; + }; + + var routes = [ + [Routes.VIEW_CONTACT, SELECTORS.VIEW_CONTACT, ViewContact.show, ViewContact.description], + [Routes.VIEW_CONTACTS, SELECTORS.VIEW_CONTACTS, ViewContacts.show, ViewContacts.description], + [Routes.VIEW_CONVERSATION, SELECTORS.VIEW_CONVERSATION, ViewConversation.show, ViewConversation.description], + [Routes.VIEW_GROUP_INFO, SELECTORS.VIEW_GROUP_INFO, ViewGroupInfo.show, ViewGroupInfo.description], + [Routes.VIEW_OVERVIEW, SELECTORS.VIEW_OVERVIEW, ViewOverview.show, ViewOverview.description], + [Routes.VIEW_SEARCH, SELECTORS.VIEW_SEARCH, ViewSearch.show, ViewSearch.description], + [Routes.VIEW_SETTINGS, SELECTORS.VIEW_SETTINGS, ViewSettings.show, ViewSettings.description], + ]; + + /** + * Create routes. + * + * @param {Object} root The message drawer container. + */ + var createRoutes = function(root) { + routes.forEach(function(route) { + Router.add(route[0], getElementsForRoute(root, route[1]), route[2], route[3]); + }); + }; + + /** + * Show the message drawer. + * + * @param {Object} root The message drawer container. + */ + var show = function(root) { + if (!root.attr('data-shown')) { + Router.go(Routes.VIEW_OVERVIEW); + root.attr('data-shown', true); + } + + root.removeClass('hidden'); + root.attr('aria-expanded', true); + root.attr('aria-hidden', false); + }; + + /** + * Hide the message drawer. + * + * @param {Object} root The message drawer container. + */ + var hide = function(root) { + root.addClass('hidden'); + root.attr('aria-expanded', false); + root.attr('aria-hidden', true); + }; + + /** + * Check if the drawer is visible. + * + * @param {Object} root The message drawer container. + * @return {bool} + */ + var isVisible = function(root) { + return !root.hasClass('hidden'); + }; + + /** + * Listen to and handle events for routing, showing and hiding the message drawer. + * + * @param {Object} root The message drawer container. + */ + var registerEventListeners = function(root) { + CustomEvents.define(root, [CustomEvents.events.activate]); + var paramRegex = /^data-route-param-?(\d*)$/; + + root.on(CustomEvents.events.activate, SELECTORS.ROUTES, function(e, data) { + var element = $(e.target).closest(SELECTORS.ROUTES); + var route = element.attr('data-route'); + var attributes = []; + + for (var i = 0; i < element[0].attributes.length; i++) { + attributes.push(element[0].attributes[i]); + } + + var paramAttributes = attributes.filter(function(attribute) { + var name = attribute.nodeName; + var match = paramRegex.test(name); + return match; + }); + paramAttributes.sort(function(a, b) { + var aParts = paramRegex.exec(a.nodeName); + var bParts = paramRegex.exec(b.nodeName); + var aIndex = aParts.length > 1 ? aParts[1] : 0; + var bIndex = bParts.length > 1 ? bParts[1] : 0; + + if (aIndex < bIndex) { + return -1; + } else if (bIndex < aIndex) { + return 1; + } else { + return 0; + } + }); + + var params = paramAttributes.map(function(attribute) { + return attribute.nodeValue; + }); + var routeParams = [route].concat(params); + + Router.go.apply(null, routeParams); + + data.originalEvent.preventDefault(); + }); + + root.on(CustomEvents.events.activate, SELECTORS.ROUTES_BACK, function(e, data) { + Router.back(); + + data.originalEvent.preventDefault(); + }); + + PubSub.subscribe(Events.SHOW, function() { + show(root); + }); + + PubSub.subscribe(Events.HIDE, function() { + hide(root); + }); + + PubSub.subscribe(Events.TOGGLE_VISIBILITY, function() { + if (isVisible(root)) { + hide(root); + } else { + show(root); + } + }); + + PubSub.subscribe(Events.SHOW_CONVERSATION, function(conversationId) { + show(root); + Router.go(Routes.VIEW_CONVERSATION, conversationId); + }); + + PubSub.subscribe(Events.SHOW_SETTINGS, function() { + show(root); + Router.go(Routes.VIEW_SETTINGS); + }); + }; + + /** + * Initialise the message drawer. + * + * @param {Object} root The message drawer container. + */ + var init = function(root) { + root = $(root); + createRoutes(root); + registerEventListeners(root); + }; + + return { + init: init, + }; +}); diff --git a/message/amd/src/message_drawer_events.js b/message/amd/src/message_drawer_events.js new file mode 100644 index 00000000000..7362ba92a3b --- /dev/null +++ b/message/amd/src/message_drawer_events.js @@ -0,0 +1,45 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Events for the message drawer. + * + * @module core_message/message_drawer_events + * @package message + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([], function() { + return { + CONTACT_BLOCKED: 'message-drawer-contact-blocked', + CONTACT_UNBLOCKED: 'message-drawer-contact-unblocked', + CONTACT_ADDED: 'message-drawer-contact-added', + CONTACT_REMOVED: 'message-drawer-contact-removed', + CONTACT_REQUEST_ACCEPTED: 'message-drawer-contact-request-accepted', + CONTACT_REQUEST_DECLINED: 'message-drawer-contact-request-declined', + CONVERSATION_CREATED: 'message-drawer-conversation-created', + CONVERSATION_NEW_LAST_MESSAGE: 'message-drawer-conversation-new-last-message', + CONVERSATION_DELETED: 'message-drawer-conversation-deleted', + CONVERSATION_READ: 'message-drawer-conversation-read', + CONVERSATION_SET_FAVOURITE: 'message-drawer-conversation-set-favourite', + CONVERSATION_UNSET_FAVOURITE: 'message-drawer-conversation-unset-favourite', + ROUTE_CHANGED: 'message-drawer-route-change', + SHOW: 'message-drawer-show', + HIDE: 'message-drawer-hide', + TOGGLE_VISIBILITY: 'message-drawer-toggle', + SHOW_CONVERSATION: 'message-drawer-show-conversation', + SHOW_SETTINGS: 'message-drawer-show-settings', + }; +}); diff --git a/message/amd/src/message_drawer_helper.js b/message/amd/src/message_drawer_helper.js new file mode 100644 index 00000000000..ac8a491e85b --- /dev/null +++ b/message/amd/src/message_drawer_helper.js @@ -0,0 +1,62 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Provides some helper functions to trigger actions in the message drawer. + * + * @module core_message/message_drawer_helper + * @package message + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'core/pubsub', + 'core_message/message_drawer_events' +], +function( + PubSub, + MessageDrawerEvents +) { + + /** + * Trigger an event to show the message drawer. + */ + var show = function() { + PubSub.publish(MessageDrawerEvents.SHOW); + }; + + /** + * Trigger an event to show the given conversation. + * + * @param {int} conversationId Id for the conversation to show. + */ + var showConversation = function(conversationId) { + PubSub.publish(MessageDrawerEvents.SHOW_CONVERSATION, conversationId); + }; + + /** + * Trigger an event to show messaging settings. + */ + var showSettings = function() { + PubSub.publish(MessageDrawerEvents.SHOW_SETTINGS); + }; + + return { + show: show, + showConversation: showConversation, + showSettings: showSettings + }; +}); diff --git a/message/amd/src/message_drawer_lazy_load_list.js b/message/amd/src/message_drawer_lazy_load_list.js new file mode 100644 index 00000000000..13d0022af88 --- /dev/null +++ b/message/amd/src/message_drawer_lazy_load_list.js @@ -0,0 +1,327 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Lazy loaded list of items. + * + * @module core_message/message_drawer_lazy_load_list + * @package message + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/custom_interaction_events' +], +function( + $, + CustomEvents +) { + + var SELECTORS = { + ROOT: '[data-region="lazy-load-list"]', + LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', + CONTENT_CONTAINER: '[data-region="content-container"]', + EMPTY_MESSAGE: '[data-region="empty-message-container"]', + PLACEHOLDER: '[data-region="placeholder-container"]' + }; + + /** + * Flag element as loading. + * + * @param {Object} root The section container element. + */ + var startLoading = function(root) { + root.attr('data-loading', true); + }; + + /** + * Flag element as not loading. + * + * @param {Object} root The section container element. + */ + var stopLoading = function(root) { + root.attr('data-loading', false); + }; + + /** + * Check if the element is loading. + * + * @param {Object} root The section container element. + * @return {Bool} + */ + var isLoading = function(root) { + return root.attr('data-loading') === 'true'; + }; + + /** + * Get user id + * + * @param {Object} root The section container element. + * @return {Number} Logged in user id. + */ + var getUserId = function(root) { + return root.attr('data-user-id'); + }; + + /** + * Get the section content container element. + * + * @param {Object} root The section container element. + * @return {Object} The section content container element. + */ + var getContentContainer = function(root) { + return root.find(SELECTORS.CONTENT_CONTAINER); + }; + + /** + * Get the root element. + * + * @param {Object} containerElement The container element to search in. + * @return {Object} The list root element. + */ + var getRoot = function(containerElement) { + return containerElement.find(SELECTORS.ROOT); + }; + + /** + * Show the loading icon. + * + * @param {Object} root The section container element. + */ + var showLoadingIcon = function(root) { + root.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden'); + }; + + /** + * Hide the loading icon. + * + * @param {Object} root The section container element. + */ + var hideLoadingIcon = function(root) { + root.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden'); + }; + + /** + * Show the empty message. + * + * @param {Object} root The section container element. + */ + var showEmptyMessage = function(root) { + root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden'); + }; + + /** + * Hide the empty message. + * + * @param {Object} root The section container element. + */ + var hideEmptyMessage = function(root) { + root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden'); + }; + + /** + * Show the placeholder element. + * + * @param {Object} root The section container element. + */ + var showPlaceholder = function(root) { + root.find(SELECTORS.PLACEHOLDER).removeClass('hidden'); + }; + + /** + * Hide the placeholder element. + * + * @param {Object} root The section container element. + */ + var hidePlaceholder = function(root) { + root.find(SELECTORS.PLACEHOLDER).addClass('hidden'); + }; + + /** + * Show the section content container. + * + * @param {Object} root The section container element. + */ + var showContent = function(root) { + getContentContainer(root).removeClass('hidden'); + }; + + /** + * Hide the section content container. + * + * @param {Object} root The section container element. + */ + var hideContent = function(root) { + getContentContainer(root).addClass('hidden'); + }; + + /** + * If the section has loaded all content. + * + * @param {Object} root The section container element. + * @return {Bool} + */ + var hasLoadedAll = function(root) { + return root.attr('data-loaded-all') == 'true'; + }; + + /** + * If the section has loaded all content. + * + * @param {Object} root The section container element. + * @param {Bool} value If all items have been loaded. + */ + var setLoadedAll = function(root, value) { + root.attr('data-loaded-all', value); + }; + + /** + * If the section can load more items. + * + * @param {Object} root The section container element. + * @return {Bool} + */ + var canLoadMore = function(root) { + return !hasLoadedAll(root) && !isLoading(root); + }; + + /** + * Load all items in this container from callback and render them. + * + * @param {Object} root The section container element. + * @param {Function} loadCallback The callback to load items. + * @param {Function} renderCallback The callback to render the results. + * @return {Object} jQuery promise + */ + var loadAndRender = function(root, loadCallback, renderCallback) { + var userId = getUserId(root); + startLoading(root); + + return loadCallback(root, userId) + .then(function(items) { + if (items.length > 0) { + var contentContainer = getContentContainer(root); + return renderCallback(contentContainer, items, userId) + .then(function() { + return items; + }); + } else { + return items; + } + }) + .then(function(items) { + stopLoading(root); + root.attr('data-seen', true); + + if (!items.length) { + setLoadedAll(root, true); + } + + return items; + }) + .catch(function() { + stopLoading(root); + root.attr('data-seen', true); + return; + }); + }; + + /** + * First load of this section. + * + * @param {Object} root The section container element. + * @param {Function} loadCallback The callback to load items. + * @param {Function} renderCallback The callback to render the results. + * @return {Object} promise + */ + var initialLoadAndRender = function(root, loadCallback, renderCallback) { + getContentContainer(root).empty(); + showPlaceholder(root); + hideContent(root); + return loadAndRender(root, loadCallback, renderCallback) + .then(function(items) { + hidePlaceholder(root); + + if (!items.length) { + showEmptyMessage(root); + } else { + showContent(root); + } + + return; + }) + .catch(function() { + hidePlaceholder(root); + showContent(root); + return; + }); + }; + + /** + * Listen to, and handle events in this section. + * + * @param {Object} root The section container element. + * @param {Function} loadCallback The callback to load items. + * @param {Function} renderCallback The callback to render the results. + */ + var registerEventListeners = function(root, loadCallback, renderCallback) { + CustomEvents.define(root, [ + CustomEvents.events.scrollBottom + ]); + + root.on(CustomEvents.events.scrollBottom, function() { + if (canLoadMore(root)) { + showLoadingIcon(root); + loadAndRender(root, loadCallback, renderCallback) + .then(function() { + return hideLoadingIcon(root); + }) + .catch(function() { + return hideLoadingIcon(root); + }); + } + }); + }; + + /** + * Setup the section. + * + * @param {Object} root The section container element. + * @param {Function} loadCallback The callback to load items. + * @param {Function} renderCallback The callback to render the results. + */ + var show = function(root, loadCallback, renderCallback) { + root = $(root); + + if (!root.attr('data-init')) { + registerEventListeners(root, loadCallback, renderCallback); + initialLoadAndRender(root, loadCallback, renderCallback); + root.attr('data-init', true); + } + }; + + return { + show: show, + getContentContainer: getContentContainer, + getRoot: getRoot, + setLoadedAll: setLoadedAll, + showEmptyMessage: showEmptyMessage, + hideEmptyMessage: hideEmptyMessage, + showContent: showContent, + hideContent: hideContent + }; +}); diff --git a/message/amd/src/message_drawer_router.js b/message/amd/src/message_drawer_router.js new file mode 100644 index 00000000000..5b82df9d9e8 --- /dev/null +++ b/message/amd/src/message_drawer_router.js @@ -0,0 +1,225 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * A simple router for the message drawer that allows navigating between + * the "pages" in the drawer. + * + * This module will maintain a linear history of the unique pages access + * to allow navigating back. + * + * @module core_message/message_drawer_router + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/pubsub', + 'core/str', + 'core_message/message_drawer_events' +], +function( + $, + PubSub, + Str, + MessageDrawerEvents +) { + + /* @var {object} routes Message drawer route elements and callbacks. */ + var routes = {}; + + /* @var {array} history Store for route objects history. */ + var history = []; + + var SELECTORS = { + CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]', + ROUTES_BACK: '[data-route-back]' + }; + + /** + * Add a route. + * + * @param {string} route Route config name. + * @param {array} elements Route container objects. + * @param {callback} onGo Route initialization function. + * @param {callback} getDescription Route initialization function. + */ + var add = function(route, elements, onGo, getDescription) { + routes[route] = { + elements: elements, + onGo: onGo, + getDescription: getDescription + }; + }; + + /** + * Go to a defined route and run the route callbacks. + * + * @param {string} newRoute Route config name. + * @return {object} record Current route record with route config name and parameters. + */ + var changeRoute = function(newRoute) { + var newConfig; + // Get the rest of the arguments, if any. + var args = [].slice.call(arguments, 1); + var renderPromise = $.Deferred().resolve().promise(); + + Object.keys(routes).forEach(function(route) { + var config = routes[route]; + var isMatch = route === newRoute; + + if (isMatch) { + newConfig = config; + } + + config.elements.forEach(function(element) { + element.removeClass('previous'); + + if (isMatch) { + element.removeClass('hidden'); + element.attr('aria-hidden', false); + } else { + element.addClass('hidden'); + element.attr('aria-hidden', true); + } + }); + }); + + if (newConfig) { + if (newConfig.onGo) { + renderPromise = newConfig.onGo.apply(undefined, newConfig.elements.concat(args)); + var currentFocusElement = $(document.activeElement); + var hasFocus = false; + + for (var i = 0; i < newConfig.elements.length; i++) { + var element = newConfig.elements[i]; + + if (element.has(currentFocusElement).length) { + hasFocus = true; + break; + } + } + + if (!hasFocus) { + // This page doesn't have focus yet so focus the first focusable + // element in the new view. + newConfig.elements[0].find(SELECTORS.CAN_RECEIVE_FOCUS).first().focus(); + } + } + } + + var record = { + route: newRoute, + params: args, + renderPromise: renderPromise + }; + + PubSub.publish(MessageDrawerEvents.ROUTE_CHANGED, record); + + return record; + }; + + /** + * Go to a defined route and store the route history. + * + * @param {string} newRoute Route config name. + * @return {object} record Current route record with route config name and parameters. + */ + var go = function() { + var currentFocusElement = $(document.activeElement); + var record = changeRoute.apply(null, arguments); + var inHistory = false; + // History stores a unique list of routes. Check to see if the new route + // is already in the history, if it is then forget all history after it. + // This ensures there are no duplicate routes in history and that it represents + // a linear path of routes (it never stores something like [foo, bar, foo])/ + history = history.reduce(function(carry, previous) { + if (previous.route === record.route) { + inHistory = true; + } + + if (!inHistory) { + carry.push(previous); + } + + return carry; + }, []); + + var previousRecord = history.length ? history[history.length - 1] : null; + + if (previousRecord) { + var prevConfig = routes[previousRecord.route]; + prevConfig.elements.forEach(function(element) { + element.addClass('previous'); + }); + + previousRecord.focusElement = currentFocusElement; + + if (prevConfig.getDescription) { + // If the route has a description then set it on the back button for + // the new page we're displaying. + prevConfig.getDescription.apply(null, prevConfig.elements.concat(previousRecord.params)) + .then(function(description) { + return Str.get_string('backto', 'core_message', description); + }) + .then(function(label) { + // Wait for the new page to finish rendering so that we know + // that the back button is visible. + return record.renderPromise.then(function() { + // Find the elements for the new route we displayed. + routes[record.route].elements.forEach(function(element) { + // Update the aria label for the back button. + element.find(SELECTORS.ROUTES_BACK).attr('aria-label', label); + }); + }); + }) + .catch(function() { + // Silently ignore. + }); + } + } + + history.push(record); + return record; + }; + + /** + * Go back to the previous route record stored in history. + */ + var back = function() { + if (history.length) { + // Remove the current route. + history.pop(); + var previous = history.pop(); + + if (previous) { + // If we have a previous route then show it. + go.apply(undefined, [previous.route].concat(previous.params)); + // Delay the focus 50 milliseconds otherwise it doesn't correctly + // focus the element for some reason... + window.setTimeout(function() { + previous.focusElement.focus(); + }, 50); + } + } + }; + + return { + add: add, + go: go, + back: back + }; +}); diff --git a/message/amd/src/message_drawer_routes.js b/message/amd/src/message_drawer_routes.js new file mode 100644 index 00000000000..66ef76710d9 --- /dev/null +++ b/message/amd/src/message_drawer_routes.js @@ -0,0 +1,33 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Available routes for the message drawer. + * + * @module core_message/message_drawer_routes + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([], function() { + return { + VIEW_CONTACT: 'view-contact', + VIEW_CONTACTS: 'view-contacts', + VIEW_CONVERSATION: 'view-conversation', + VIEW_GROUP_INFO: 'view-group-info', + VIEW_OVERVIEW: 'view-overview', + VIEW_SEARCH: 'view-search', + VIEW_SETTINGS: 'view-settings' + }; +}); diff --git a/message/amd/src/message_drawer_view_contact.js b/message/amd/src/message_drawer_view_contact.js new file mode 100644 index 00000000000..ffae0dfe71d --- /dev/null +++ b/message/amd/src/message_drawer_view_contact.js @@ -0,0 +1,97 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the contact page in the message drawer. + * + * @module core_message/message_drawer_view_contact + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/str', + 'core/templates' +], +function( + $, + Str, + Templates +) { + + var SELECTORS = { + CONTENT_CONTAINER: '[data-region="content-container"]' + }; + + var TEMPLATES = { + CONTENT: 'core_message/message_drawer_view_contact_body_content' + }; + + /** + * Get the content container of the contact view container. + * + * @param {Object} root Contact container element. + * @returns {Object} jQuery object + */ + var getContentContainer = function(root) { + return root.find(SELECTORS.CONTENT_CONTAINER); + }; + + /** + * Render the contact profile in the content container. + * + * @param {Object} root Contact container element. + * @param {Object} profile Contact profile details. + * @returns {Object} jQuery promise + */ + var render = function(root, profile) { + return Templates.render(TEMPLATES.CONTENT, profile) + .then(function(html) { + getContentContainer(root).append(html); + return html; + }); + }; + + /** + * Setup the contact page. + * + * @param {Object} root Contact container element. + * @param {Object} contact The contact object. + * @returns {Object} jQuery promise + */ + var show = function(root, contact) { + root = $(root); + + getContentContainer(root).empty(); + return render(root, contact); + }; + + /** + * String describing this page used for aria-labels. + * + * @param {Object} root Contact container element. + * @param {Object} contact The contact object. + * @return {Object} jQuery promise + */ + var description = function(root, contact) { + return Str.get_string('messagedrawerviewcontact', 'core_message', contact.fullname); + }; + + return { + show: show, + description: description + }; +}); diff --git a/message/amd/src/message_drawer_view_contacts.js b/message/amd/src/message_drawer_view_contacts.js new file mode 100644 index 00000000000..b3821ab5bd7 --- /dev/null +++ b/message/amd/src/message_drawer_view_contacts.js @@ -0,0 +1,163 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the contacts page of the message drawer. + * + * @module core_message/message_drawer_view_contacts + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/pubsub', + 'core/str', + 'core_message/message_drawer_events', + 'core_message/message_drawer_view_contacts_section_contacts', + 'core_message/message_drawer_view_contacts_section_requests' +], +function( + $, + PubSub, + Str, + MessageDrawerEvents, + ContactsSection, + RequestsSection +) { + + var SELECTORS = { + ACTION_SHOW_CONTACTS_SECTION: '[data-action="show-contacts-section"]', + ACTION_SHOW_REQUESTS_SECTION: '[data-action="show-requests-section"]', + CONTACT_REQUEST_COUNT: '[data-region="contact-request-count"]', + CONTACTS_SECTION_CONTAINER: '[data-section="contacts"]', + REQUESTS_SECTION_CONTAINER: '[data-section="requests"]', + }; + + /** + * Get the container element for the contacts section. + * + * @param {Object} body Contacts page body element. + * @return {Object} + */ + var getContactsSectionContainer = function(body) { + return body.find(SELECTORS.CONTACTS_SECTION_CONTAINER); + }; + + /** + * Get the container element for the requests section. + * + * @param {Object} body Contacts page body element. + * @return {Object} + */ + var getRequestsSectionContainer = function(body) { + return body.find(SELECTORS.REQUESTS_SECTION_CONTAINER); + }; + + /** + * Check if the given section is visible. + * + * @param {Object} sectionRoot The root element for the section + * @return {Bool} + */ + var isSectionVisible = function(sectionRoot) { + return sectionRoot.hasClass('active'); + }; + + /** + * Decrement the contact request count. If the count is zero or below then + * hide the count. + * + * @param {Object} body Conversation body container element. + * @return {Function} A function to handle decrementing the count. + */ + var decrementContactRequestCount = function(body) { + return function() { + var countContainer = body.find(SELECTORS.CONTACT_REQUEST_COUNT); + var count = parseInt(countContainer.text(), 10); + count = isNaN(count) ? 0 : count - 1; + + if (count <= 0) { + countContainer.addClass('hidden'); + } else { + countContainer.text(count); + } + }; + }; + + /** + * Listen to and handle events for contacts. + * + * @param {Object} body Contacts body container element. + */ + var registerEventListeners = function(body) { + var contactsSection = getContactsSectionContainer(body); + var requestsSection = getRequestsSectionContainer(body); + var showContactsAction = body.find(SELECTORS.ACTION_SHOW_CONTACTS_SECTION); + var showRequestsAction = body.find(SELECTORS.ACTION_SHOW_REQUESTS_SECTION); + + showContactsAction.on('show.bs.tab', function() { + ContactsSection.show(contactsSection); + }); + + showRequestsAction.on('show.bs.tab', function() { + RequestsSection.show(requestsSection); + }); + + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(body)); + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(body)); + }; + + /** + * Setup the contact page. + * + * @param {Object} header Contacts header container element. + * @param {Object} body Contacts body container element. + * @return {Object} jQuery promise + */ + var show = function(header, body) { + body = $(body); + + if (!body.attr('data-contacts-init')) { + registerEventListeners(body); + body.attr('data-contacts-init', true); + } + + var contactsSection = getContactsSectionContainer(body); + var requestsSection = getRequestsSectionContainer(body); + + if (isSectionVisible(contactsSection)) { + ContactsSection.show(contactsSection); + } else { + RequestsSection.show(requestsSection); + } + + return $.Deferred().resolve().promise(); + }; + + /** + * String describing this page used for aria-labels. + * + * @return {Object} jQuery promise + */ + var description = function() { + return Str.get_string('messagedrawerviewcontacts', 'core_message'); + }; + + return { + show: show, + description: description + }; +}); diff --git a/message/amd/src/message_drawer_view_contacts_section.js b/message/amd/src/message_drawer_view_contacts_section.js new file mode 100644 index 00000000000..abc71066f84 --- /dev/null +++ b/message/amd/src/message_drawer_view_contacts_section.js @@ -0,0 +1,317 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls a section on the contacts page of the message drawer. + * + * @module core_message/message_drawer_view_contacts_section + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/notification', + 'core/pubsub', + 'core/templates', + 'core/custom_interaction_events', + 'core_message/message_repository', + 'core_message/message_drawer_events' +], +function( + $, + Notification, + PubSub, + Templates, + CustomEvents, + MessageRepository, + Events +) { + + var LOAD_CONTACTS_LIMIT = 100; + + var numContacts = 0; + var contactsOffset = 0; + var loadedAllContacts = false; + var waitForScrollLoad = false; + + var SELECTORS = { + BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]', + CONTACTS: '[data-region="contacts-container"]', + LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', + CONTENT_CONTAINER: '[data-region="contacts-content-container"]', + EMPTY_MESSAGE: '[data-region="empty-message-container"]', + PLACEHOLDER: '[data-region="placeholder-container"]' + }; + + var TEMPLATES = { + CONTACTS_LIST: 'core_message/message_drawer_contacts_list' + }; + + /** + * Show the loading icon. + * + * @param {Object} body Contacts body container element. + */ + var startLoading = function(body) { + body.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden'); + }; + + /** + * Hide the loading icon. + * + * @param {Object} body Contacts body container element. + */ + var stopLoading = function(body) { + body.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden'); + }; + + /** + * Get the content container of the contacts body container element. + * + * @param {Object} body Contacts body container element. + * @return {Object} jQuery element + */ + var getContentContainer = function(body) { + return body.find(SELECTORS.CONTENT_CONTAINER); + }; + + /** + * Get the contacts container of the contacts body container element. + * + * @param {Object} body Contacts body container element. + * @return {Object} jQuery element + */ + var getContactsContainer = function(body) { + return body.find(SELECTORS.CONTACTS); + }; + + /** + * Show a message when no contacts found. + * + * @param {Object} body Contacts body container element. + */ + var showEmptyMessage = function(body) { + getContentContainer(body).addClass('hidden'); + body.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden'); + }; + + /** + * Hide the placeholder image. + * + * @param {Object} body Contacts body container element. + */ + var hidePlaceholder = function(body) { + body.find(SELECTORS.PLACEHOLDER).addClass('hidden'); + }; + + /** + * Show the content container. + * + * @param {Object} body Contacts body container element. + */ + var showContent = function(body) { + getContentContainer(body).removeClass('hidden'); + }; + + /** + * Find a contact element. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId User id of contact. + * @return {Object} contact element. + */ + var findContact = function(body, userId) { + return body.find('[data-contact-user-id="' + userId + '"]'); + }; + + /** + * Get logged in userid. + * + * @param {Object} body Contacts body container element. + * @return {Number} Logged in userid. + */ + var getLoggedInUserId = function(body) { + return body.attr('data-user-id'); + }; + + /** + * Render the contacts in the content container. + * + * @param {Object} body Contacts body container element. + * @param {Array} contacts List of contacts. + * @return {Object} jQuery promise + */ + var render = function(body, contacts) { + var contentContainer = getContentContainer(body); + return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts}) + .then(function(html) { + hidePlaceholder(body); + contentContainer.append(html); + showContent(body); + return html; + }); + }; + + /** + * Load the user contacts and call the renderer. + * + * @param {Object} body Contacts body container element. + * @return {Object} jQuery promise + */ + var loadContacts = function(body) { + var userId = getLoggedInUserId(body); + return MessageRepository.getContacts(userId, (LOAD_CONTACTS_LIMIT + 1), contactsOffset) + .then(function(result) { + return result.contacts; + }) + .then(function(contacts) { + if (contacts.length > LOAD_CONTACTS_LIMIT) { + contacts.pop(); + } else { + loadedAllContacts = true; + } + return contacts; + }) + .then(function(contacts) { + if (contactsOffset == 0 && contacts.length == 0) { + hidePlaceholder(body); + showEmptyMessage(body); + } + + numContacts = numContacts + contacts.length; + + contactsOffset = contactsOffset + LOAD_CONTACTS_LIMIT; + if (contacts.length > 0) { + return render(body, contacts); + } + + return contacts; + }); + }; + + /** + * Remove contact from view. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId Contact userid. + */ + var removeContact = function(body, userId) { + findContact(body, userId).remove(); + }; + + /** + * Show the contact has been blocked. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId Contact userid. + */ + var showContactBlocked = function(body, userId) { + var contact = findContact(body, userId); + if (contact.length) { + contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden'); + } + }; + + /** + * Show the contact has been unblocked. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId Contact userid. + */ + var showContactUnblocked = function(body, userId) { + var contact = findContact(body, userId); + if (contact.length) { + contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden'); + } + }; + + /** + * Listen to and handle events for contacts. + * + * @param {Object} body Contacts body container element. + */ + var registerEventListeners = function(body) { + PubSub.subscribe(Events.CONTACT_ADDED, function() { + contactsOffset = 0; + loadedAllContacts = false; + getContentContainer(body).empty(); + loadContacts(body); + }); + + PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) { + removeContact(body, userId); + }); + + PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) { + showContactBlocked(body, userId); + }); + + PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) { + showContactUnblocked(body, userId); + }); + + var contactsContainer = getContactsContainer(body); + + CustomEvents.define(contactsContainer, [ + CustomEvents.events.scrollBottom, + CustomEvents.events.scrollLock + ]); + + contactsContainer.on(CustomEvents.events.scrollBottom, function(e, data) { + var hasContacts = numContacts > 1; + if (!loadedAllContacts && hasContacts && !waitForScrollLoad) { + waitForScrollLoad = true; + startLoading(body); + loadContacts(body) + .then(function() { + stopLoading(body); + waitForScrollLoad = false; + return; + }) + .catch(function(error) { + stopLoading(body); + waitForScrollLoad = false; + Notification.exception(error); + }); + } + data.originalEvent.preventDefault(); + }); + }; + + /** + * Setup the contact page. + * + * @param {Object} header Contacts header container element. + * @param {Object} body Contacts body container element. + */ + var show = function(header, body) { + body = $(body); + contactsOffset = 0; + + if (!body.attr('data-contacts-init')) { + registerEventListeners(body); + body.attr('data-contacts-init', true); + } + + if (!loadedAllContacts) { + loadContacts(body); + } + }; + + return { + show: show, + }; +}); diff --git a/message/amd/src/message_drawer_view_contacts_section_contacts.js b/message/amd/src/message_drawer_view_contacts_section_contacts.js new file mode 100644 index 00000000000..ea868c9307e --- /dev/null +++ b/message/amd/src/message_drawer_view_contacts_section_contacts.js @@ -0,0 +1,200 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the contacts section of the contacts page. + * + * @module core_message/message_drawer_view_contacts_section_contacts + * @package message + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/notification', + 'core/pubsub', + 'core/templates', + 'core_message/message_repository', + 'core_message/message_drawer_events', + 'core_message/message_drawer_lazy_load_list' +], +function( + $, + Notification, + PubSub, + Templates, + MessageRepository, + Events, + LazyLoadList +) { + + var limit = 100; + var offset = 0; + + var SELECTORS = { + BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]', + CONTACT: '[data-region="contact"]', + CONTENT_CONTAINER: '[data-region="contacts-content-container"]' + }; + + var TEMPLATES = { + CONTACTS_LIST: 'core_message/message_drawer_contacts_list' + }; + + /** + * Find a contact element. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId User id of contact. + * @return {Object} contact element. + */ + var findContact = function(body, userId) { + return body.find('[data-contact-user-id="' + userId + '"]'); + }; + + /** + * Render the contacts in the content container. + * + * @param {Object} contentContainer Content container element. + * @param {Array} contacts List of contacts. + * @return {Object} jQuery promise + */ + var render = function(contentContainer, contacts) { + var formattedContacts = contacts.map(function(contact) { + return $.extend(contact, {id: contact.userid}); + }); + return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: formattedContacts}) + .then(function(html) { + contentContainer.append(html); + return html; + }) + .catch(Notification.exception); + }; + + /** + * Load the user contacts and call the renderer. + * + * @param {Object} listRoot The lazy loaded list root element + * @param {Integer} userId The logged in user id. + * @return {Object} jQuery promise + */ + var load = function(listRoot, userId) { + return MessageRepository.getContacts(userId, (limit + 1), offset) + .then(function(result) { + return result.contacts; + }) + .then(function(contacts) { + if (contacts.length > limit) { + contacts.pop(); + } else { + LazyLoadList.setLoadedAll(listRoot, true); + } + return contacts; + }) + .then(function(contacts) { + offset = offset + limit; + return contacts; + }) + .catch(Notification.exception); + }; + + /** + * Remove contact from view. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId Contact userid. + */ + var removeContact = function(body, userId) { + findContact(body, userId).remove(); + }; + + /** + * Show the contact has been blocked. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId Contact userid. + */ + var showContactBlocked = function(body, userId) { + var contact = findContact(body, userId); + if (contact.length) { + contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden'); + } + }; + + /** + * Show the contact has been unblocked. + * + * @param {Object} body Contacts body container element. + * @param {Number} userId Contact userid. + */ + var showContactUnblocked = function(body, userId) { + var contact = findContact(body, userId); + if (contact.length) { + contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden'); + } + }; + + /** + * Listen to and handle events for contacts. + * + * @param {Object} root Contacts section container element. + */ + var registerEventListeners = function(root) { + PubSub.subscribe(Events.CONTACT_ADDED, function(profile) { + var listContentContainer = LazyLoadList.getContentContainer(root); + render(listContentContainer, [profile]); + LazyLoadList.hideEmptyMessage(root); + LazyLoadList.showContent(root); + }); + + PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) { + removeContact(root, userId); + var contacts = root.find(SELECTORS.CONTACT); + + if (!contacts.length) { + LazyLoadList.hideContent(root); + LazyLoadList.showEmptyMessage(root); + } + }); + + PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) { + showContactBlocked(root, userId); + }); + + PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) { + showContactUnblocked(root, userId); + }); + }; + + /** + * Setup the contacts section. + * + * @param {Object} root Contacts section container. + */ + var show = function(root) { + if (!root.attr('data-contacts-init')) { + registerEventListeners(root); + root.attr('data-contacts-init', true); + } + + // The root element is already the lazy loaded list root. + LazyLoadList.show(root, load, render); + }; + + return { + show: show, + }; +}); diff --git a/message/amd/src/message_drawer_view_contacts_section_requests.js b/message/amd/src/message_drawer_view_contacts_section_requests.js new file mode 100644 index 00000000000..08467f73cd6 --- /dev/null +++ b/message/amd/src/message_drawer_view_contacts_section_requests.js @@ -0,0 +1,139 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the requests section of the contacts page. + * + * @module core_message/message_drawer_view_contacts_section_requests + * @package message + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/notification', + 'core/pubsub', + 'core/templates', + 'core_message/message_repository', + 'core_message/message_drawer_events', + 'core_message/message_drawer_lazy_load_list' +], +function( + $, + Notification, + PubSub, + Templates, + MessageRepository, + MessageDrawerEvents, + LazyLoadList +) { + + var SELECTORS = { + CONTACT_REQUEST: '[data-region="contact-request"]' + }; + + var TEMPLATES = { + REQUESTS_LIST: 'core_message/message_drawer_view_contacts_body_section_requests_list' + }; + + /** + * Render the requests in the content container. + * + * @param {Object} contentContainer List container element. + * @param {Array} requests List of requests. + * @return {Object} jQuery promise + */ + var render = function(contentContainer, requests) { + var formattedRequests = requests.map(function(request) { + return { + // This is actually the user id. + id: request.id, + profileimageurl: request.profileimageurl, + fullname: request.fullname + }; + }); + return Templates.render(TEMPLATES.REQUESTS_LIST, {requests: formattedRequests}) + .then(function(html) { + contentContainer.append(html); + return html; + }) + .catch(Notification.exception); + }; + + /** + * Load the user contacts and call the renderer. + * + * @param {Object} listRoot The lazy loaded list root element + * @param {Integer} userId The logged in user id. + * @return {Object} jQuery promise + */ + var load = function(listRoot, userId) { + return MessageRepository.getContactRequests(userId) + .then(function(requests) { + LazyLoadList.setLoadedAll(listRoot, true); + return requests; + }) + .catch(Notification.exception); + }; + + /** + * Handle when a contact request is accepted or declined by removing the contact + * list from the page. + * + * @param {Object} root The section root element + * @return {Function} The event handler function + */ + var handleContactRequestProcessed = function(root) { + return function(request) { + root.find('[data-request-id="' + request.userid + '"]').remove(); + var contactRequests = root.find(SELECTORS.CONTACT_REQUEST); + + if (!contactRequests.length) { + LazyLoadList.showEmptyMessage(root); + LazyLoadList.hideContent(root); + } + }; + }; + + /** + * Listen for any events that might affect the requests section. + * + * @param {Object} root The section root element + */ + var registerEventListeners = function(root) { + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, handleContactRequestProcessed(root)); + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, handleContactRequestProcessed(root)); + }; + + /** + * Setup the requests section. + * + * @param {Object} root Requests section container. + */ + var show = function(root) { + if (!root.attr('data-contacts-init')) { + registerEventListeners(root); + root.attr('data-contacts-init', true); + } + + // The root element is already the lazy loaded list root. + LazyLoadList.show(root, load, render); + }; + + return { + show: show, + }; +}); diff --git a/message/amd/src/message_drawer_view_conversation.js b/message/amd/src/message_drawer_view_conversation.js new file mode 100644 index 00000000000..77e49ea0999 --- /dev/null +++ b/message/amd/src/message_drawer_view_conversation.js @@ -0,0 +1,1564 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the conversation page in the message drawer. + * + * This function handles all of the user actions that the user can take + * when interacting with the conversation page. + * + * It maintains a view state which is a data representation of the view + * and only operates on that data. + * + * The view state is immutable and should never be modified directly. Instead + * all changes to the view state should be done using the StateManager which + * will generate a new version of the view state with the requested changes. + * + * After any changes to the view state the module will call the render function + * to ask the renderer to update the UI. + * + * General rules for this module: + * 1.) Never modify viewState directly. All changes should be via the StateManager. + * 2.) Call render() with the new state when you want to update the UI + * 3.) Never modify the UI directly in this module. This module is only concerned + * with the data in the view state. + * + * The general flow for a user interaction will be something like: + * User interaction: User clicks "confirm block" button to block the other user + * 1.) This module is hears the click + * 2.) This module sends a request to the server to block the user + * 3.) The server responds with the new user profile + * 4.) This module generates a new state using the StateManager with the updated + * user profile. + * 5.) This module asks the Patcher to generate a patch from the current state and + * the newly generated state. This patch tells the renderer what has changed + * between the states. + * 6.) This module gives the Renderer the generated patch. The renderer updates + * the UI with changes according to the patch. + * + * @module core_message/message_drawer_view_conversation + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/auto_rows', + 'core/backoff_timer', + 'core/custom_interaction_events', + 'core/notification', + 'core/pubsub', + 'core/str', + 'core_message/message_repository', + 'core_message/message_drawer_events', + 'core_message/message_drawer_view_conversation_constants', + 'core_message/message_drawer_view_conversation_patcher', + 'core_message/message_drawer_view_conversation_renderer', + 'core_message/message_drawer_view_conversation_state_manager', + 'core_message/message_drawer_router', + 'core_message/message_drawer_routes', +], +function( + $, + AutoRows, + BackOffTimer, + CustomEvents, + Notification, + PubSub, + Str, + Repository, + MessageDrawerEvents, + Constants, + Patcher, + Renderer, + StateManager, + MessageDrawerRouter, + MessageDrawerRoutes +) { + + // Contains a cache of all view states that have been loaded so far + // which saves us having to reload stuff with network requests when + // switching between conversations. + var stateCache = {}; + // The current data representation of the view. + var viewState = null; + var loadedAllMessages = false; + var messagesOffset = 0; + var newMessagesPollTimer = null; + // This is the render function which will be generated when this module is + // first called. See generateRenderFunction for details. + var render = null; + + var NEWEST_FIRST = Constants.NEWEST_MESSAGES_FIRST; + var LOAD_MESSAGE_LIMIT = Constants.LOAD_MESSAGE_LIMIT; + var INITIAL_NEW_MESSAGE_POLL_TIMEOUT = Constants.INITIAL_NEW_MESSAGE_POLL_TIMEOUT; + var SELECTORS = Constants.SELECTORS; + var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES; + + /** + * Get the other user userid. + * + * @return {Number} Userid. + */ + var getOtherUserId = function() { + if (!viewState || viewState.type != CONVERSATION_TYPES.PRIVATE) { + return null; + } + + var loggedInUserId = viewState.loggedInUserId; + var otherUserIds = Object.keys(viewState.members).filter(function(userId) { + return loggedInUserId != userId; + }); + + return otherUserIds.length ? otherUserIds[0] : null; + }; + + /** + * Search the cache to see if we've already loaded a private conversation + * with the given user id. + * + * @param {Number} userId The id of the other user. + * @return {Number|null} Conversation id. + */ + var getCachedPrivateConversationIdFromUserId = function(userId) { + return Object.keys(stateCache).reduce(function(carry, id) { + if (!carry) { + var state = stateCache[id].state; + + if (state.type == CONVERSATION_TYPES.PRIVATE) { + if (userId in state.members) { + // We've found a cached conversation for this user! + carry = state.id; + } + } + } + + return carry; + }, null); + }; + + /** + * Get profile info for logged in user. + * + * @param {Object} body Conversation body container element. + * @return {Object} + */ + var getLoggedInUserProfile = function(body) { + return { + id: parseInt(body.attr('data-user-id'), 10), + contactrequests: [] + }; + }; + + /** + * Get the messages offset value to load more messages. + * + * @return {Number} + */ + var getMessagesOffset = function() { + return messagesOffset; + }; + + /** + * Set the messages offset value for loading more messages. + * + * @param {Number} value The offset value + */ + var setMessagesOffset = function(value) { + messagesOffset = value; + stateCache[viewState.id].messagesOffset = value; + }; + + /** + * Check if all messages have been loaded. + * + * @return {Bool} + */ + var hasLoadedAllMessages = function() { + return loadedAllMessages; + }; + + /** + * Set whether all messages have been loaded or not. + * + * @param {Bool} value If all messages have been loaded. + */ + var setLoadedAllMessages = function(value) { + loadedAllMessages = value; + stateCache[viewState.id].loadedAllMessages = value; + }; + + /** + * Get the messages container element. + * + * @param {Object} body Conversation body container element. + * @return {Object} The messages container element. + */ + var getMessagesContainer = function(body) { + return body.find(SELECTORS.MESSAGES_CONTAINER); + }; + + /** + * Reformat the conversation for an event payload. + * + * @param {Object} state The view state. + * @return {Object} New formatted conversation. + */ + var formatConversationForEvent = function(state) { + return { + id: state.id, + name: state.name, + subname: state.subname, + imageUrl: state.imageUrl, + isFavourite: state.isFavourite, + type: state.type, + totalMemberCount: state.totalMemberCount, + loggedInUserId: state.loggedInUserId, + messages: state.messages.map(function(message) { + return $.extend({}, message); + }), + members: Object.keys(state.members).reduce(function(carry, id) { + carry[id] = $.extend({}, state.members[id]); + carry[id].contactrequests = state.members[id].contactrequests.map(function(request) { + return $.extend({}, request); + }); + return carry; + }, {}) + }; + }; + + /** + * Load up an empty private conversation between the logged in user and the + * other user. Sets all of the conversation details based on the other user. + * + * A conversation isn't created until the user sends the first message. + * + * @param {Object} loggedInUserProfile The logged in user profile. + * @param {Number} otherUserId The other user id. + * @return {Object} Profile returned from repository. + */ + var loadEmptyPrivateConversation = function(loggedInUserProfile, otherUserId) { + var loggedInUserId = loggedInUserProfile.id; + var newState = StateManager.setLoadingMembers(viewState, true); + newState = StateManager.setLoadingMessages(newState, true); + return render(newState) + .then(function() { + return Repository.getMemberInfo(loggedInUserId, [otherUserId], true, true); + }) + .then(function(profiles) { + if (profiles.length) { + return profiles[0]; + } else { + throw new Error('Unable to load other user profile'); + } + }) + .then(function(profile) { + var newState = StateManager.addMembers(viewState, [profile, loggedInUserProfile]); + newState = StateManager.setLoadingMembers(newState, false); + newState = StateManager.setLoadingMessages(newState, false); + newState = StateManager.setName(newState, profile.fullname); + newState = StateManager.setType(newState, 1); + newState = StateManager.setImageUrl(newState, profile.profileimageurl); + newState = StateManager.setTotalMemberCount(newState, 2); + return render(newState) + .then(function() { + return profile; + }); + }) + .catch(function(error) { + var newState = StateManager.setLoadingMembers(viewState, false); + render(newState); + Notification.exception(error); + }); + }; + + /** + * Create a new state from a conversation object. + * + * @param {Object} conversation The conversation object. + * @param {Number} loggedInUserId The logged in user id. + * @return {Object} new state. + */ + var updateStateFromConversation = function(conversation, loggedInUserId) { + var otherUsers = conversation.members.filter(function(member) { + return member.id != loggedInUserId; + }); + var otherUser = otherUsers.length ? otherUsers[0] : null; + var name = conversation.name; + var imageUrl = conversation.imageurl; + + if (conversation.type == CONVERSATION_TYPES.PRIVATE) { + name = name || otherUser ? otherUser.fullname : ''; + imageUrl = imageUrl || otherUser ? otherUser.profileimageurl : ''; + } + + var newState = StateManager.addMembers(viewState, conversation.members); + newState = StateManager.setName(newState, name); + newState = StateManager.setSubname(newState, conversation.subname); + newState = StateManager.setType(newState, conversation.type); + newState = StateManager.setImageUrl(newState, imageUrl); + newState = StateManager.setTotalMemberCount(newState, conversation.membercount); + newState = StateManager.setIsFavourite(newState, conversation.isfavourite); + newState = StateManager.addMessages(newState, conversation.messages); + return newState; + }; + + /** + * Get the details for a conversation from the conversation id. + * + * @param {Number} conversationId The conversation id. + * @param {Object} loggedInUserProfile The logged in user profile. + * @param {Number} messageLimit The number of messages to include. + * @param {Number} messageOffset The number of messages to skip. + * @param {Bool} newestFirst Order messages newest first. + * @return {Object} Promise resolved when loaded. + */ + var loadNewConversation = function( + conversationId, + loggedInUserProfile, + messageLimit, + messageOffset, + newestFirst + ) { + var loggedInUserId = loggedInUserProfile.id; + var newState = StateManager.setLoadingMembers(viewState, true); + newState = StateManager.setLoadingMessages(newState, true); + return render(newState) + .then(function() { + return Repository.getConversation( + loggedInUserId, + conversationId, + true, + true, + 0, + 0, + messageLimit + 1, + messageOffset, + newestFirst + ); + }) + .then(function(conversation) { + if (conversation.messages.length > messageLimit) { + conversation.messages = conversation.messages.slice(1); + } else { + setLoadedAllMessages(true); + } + + setMessagesOffset(messageOffset + messageLimit); + + return conversation; + }) + .then(function(conversation) { + var hasLoggedInUser = conversation.members.filter(function(member) { + return member.id == loggedInUserProfile.id; + }); + + if (hasLoggedInUser.length < 1) { + conversation.members = conversation.members.concat([loggedInUserProfile]); + } + + var newState = updateStateFromConversation(conversation, loggedInUserProfile.id); + newState = StateManager.setLoadingMembers(newState, false); + newState = StateManager.setLoadingMessages(newState, false); + return render(newState) + .then(function() { + return conversation; + }); + }) + .then(function() { + return markConversationAsRead(conversationId); + }) + .catch(function(error) { + var newState = StateManager.setLoadingMembers(viewState, false); + newState = StateManager.setLoadingMessages(newState, false); + render(newState); + Notification.exception(error); + }); + }; + + /** + * Get the details for a conversation from and existing conversation object. + * + * @param {Object} conversation The conversation object. + * @param {Object} loggedInUserProfile The logged in user profile. + * @param {Number} messageLimit The number of messages to include. + * @param {Bool} newestFirst Order messages newest first. + * @return {Object} Promise resolved when loaded. + */ + var loadExistingConversation = function( + conversation, + loggedInUserProfile, + messageLimit, + newestFirst + ) { + var hasLoggedInUser = conversation.members.filter(function(member) { + return member.id == loggedInUserProfile.id; + }); + + if (hasLoggedInUser.length < 1) { + conversation.members = conversation.members.concat([loggedInUserProfile]); + } + + var newState = updateStateFromConversation(conversation, loggedInUserProfile.id); + newState = StateManager.setLoadingMembers(newState, false); + newState = StateManager.setLoadingMessages(newState, true); + var messageCount = conversation.messages.length; + return render(newState) + .then(function() { + if (messageCount < messageLimit) { + // We haven't got enough messages so let's load some more. + return loadMessages(conversation.id, messageLimit, messageCount, newestFirst, []); + } else { + // We've got enough messages. No need to load any more for now. + var newState = StateManager.setLoadingMessages(newState, false); + return render(newState); + } + }) + .then(function(messages) { + setMessagesOffset(messageCount + messageLimit); + return messages; + }) + .then(function() { + return markConversationAsRead(conversation.id); + }) + .catch(Notification.exception); + }; + + /** + * Load messages for this conversation and pass them to the renderer. + * + * @param {Number} conversationId Conversation id. + * @param {Number} limit Number of messages to load. + * @param {Number} offset Get messages from offset. + * @param {Bool} newestFirst Get newest messages first. + * @param {Array} ignoreList Ignore any messages with ids in this list. + * @param {Number|null} timeFrom Only get messages from this time onwards. + * @return {Promise} renderer promise. + */ + var loadMessages = function(conversationId, limit, offset, newestFirst, ignoreList, timeFrom) { + return Repository.getMessages( + viewState.loggedInUserId, + conversationId, + limit ? limit + 1 : limit, + offset, + newestFirst, + timeFrom + ) + .then(function(result) { + if (result.messages.length && ignoreList.length) { + result.messages = result.messages.filter(function(message) { + // Skip any messages in our ignore list. + return ignoreList.indexOf(parseInt(message.id, 10)) < 0; + }); + } + + return result; + }) + .then(function(result) { + if (!limit) { + return result; + } else if (result.messages.length > limit) { + result.messages = result.messages.slice(1); + } else { + setLoadedAllMessages(true); + } + + return result; + }) + .then(function(result) { + var membersToAdd = result.members.filter(function(member) { + return !(member.id in viewState.members); + }); + var newState = StateManager.addMembers(viewState, membersToAdd); + newState = StateManager.addMessages(newState, result.messages); + newState = StateManager.setLoadingMessages(newState, false); + return render(newState) + .then(function() { + return result; + }); + }) + .catch(function(error) { + var newState = StateManager.setLoadingMessages(viewState, false); + render(newState); + // Re-throw the error for other error handlers. + throw error; + }); + }; + + /** + * Create a callback function for getting new messages for this conversation. + * + * @param {Number} conversationId Conversation id. + * @param {Bool} newestFirst Show newest messages first + * @return {Function} Callback function that returns a renderer promise. + */ + var getLoadNewMessagesCallback = function(conversationId, newestFirst) { + return function() { + var messages = viewState.messages; + var mostRecentMessage = messages.length ? messages[messages.length - 1] : null; + + if (mostRecentMessage) { + // There may be multiple messages with the same time created value since + // the accuracy is only down to the second. The server will include these + // messages in the result (since it does a >= comparison on time from) so + // we need to filter them back out of the result so that we're left only + // with the new messages. + var ignoreMessageIds = []; + for (var i = messages.length - 1; i >= 0; i--) { + var message = messages[i]; + if (message.timeCreated === mostRecentMessage.timeCreated) { + ignoreMessageIds.push(message.id); + } else { + // Since the messages are ordered in ascending order of time created + // we can break as soon as we hit a message with a different time created + // because we know all other messages will have lower values. + break; + } + } + + return loadMessages( + conversationId, + 0, + 0, + newestFirst, + ignoreMessageIds, + mostRecentMessage.timeCreated + ) + .then(function(result) { + if (result.messages.length) { + // If we found some results then restart the polling timer + // because the other user might be sending messages. + newMessagesPollTimer.restart(); + return markConversationAsRead(conversationId); + } else { + return result; + } + }); + } + }; + }; + + /** + * Mark a conversation as read. + * + * @param {Number} conversationId The conversation id. + * @return {Promise} The renderer promise. + */ + var markConversationAsRead = function(conversationId) { + var loggedInUserId = viewState.loggedInUserId; + + return Repository.markAllConversationMessagesAsRead(loggedInUserId, conversationId) + .then(function() { + var newState = StateManager.markMessagesAsRead(viewState, viewState.messages); + PubSub.publish(MessageDrawerEvents.CONVERSATION_READ, conversationId); + return render(newState); + }); + }; + + /** + * Tell the statemanager there is request to block a user and run the renderer + * to show the block user dialogue. + * + * @param {Number} userId User id. + * @return {Promise} Renderer promise. + */ + var requestBlockUser = function(userId) { + return cancelRequest(userId).then(function() { + var newState = StateManager.addPendingBlockUsersById(viewState, [userId]); + return render(newState); + }); + }; + + /** + * Send the repository a request to block a user, update the statemanager and publish + * a contact has been blocked. + * + * @param {Number} userId User id of user to block. + * @return {Promise} Renderer promise. + */ + var blockUser = function(userId) { + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.blockUser(viewState.loggedInUserId, userId); + }) + .then(function(profile) { + var newState = StateManager.addMembers(viewState, [profile]); + newState = StateManager.removePendingBlockUsersById(newState, [userId]); + newState = StateManager.setLoadingConfirmAction(newState, false); + PubSub.publish(MessageDrawerEvents.CONTACT_BLOCKED, newState.id); + return render(newState); + }); + }; + + /** + * Tell the statemanager there is a request to unblock a user and run the renderer + * to show the unblock user dialogue. + * + * @param {Number} userId User id of user to unblock. + * @return {Promise} Renderer promise. + */ + var requestUnblockUser = function(userId) { + return cancelRequest(userId).then(function() { + var newState = StateManager.addPendingUnblockUsersById(viewState, [userId]); + return render(newState); + }); + }; + + /** + * Send the repository a request to unblock a user, update the statemanager and publish + * a contact has been unblocked. + * + * @param {Number} userId User id of user to unblock. + * @return {Promise} Renderer promise. + */ + var unblockUser = function(userId) { + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.unblockUser(viewState.loggedInUserId, userId); + }) + .then(function(profile) { + var newState = StateManager.addMembers(viewState, [profile]); + newState = StateManager.removePendingUnblockUsersById(newState, [userId]); + newState = StateManager.setLoadingConfirmAction(newState, false); + PubSub.publish(MessageDrawerEvents.CONTACT_UNBLOCKED, newState.id); + return render(newState); + }); + }; + + /** + * Tell the statemanager there is a request to remove a user from the contact list + * and run the renderer to show the remove user from contacts dialogue. + * + * @param {Number} userId User id of user to remove from contacts. + * @return {Promise} Renderer promise. + */ + var requestRemoveContact = function(userId) { + return cancelRequest(userId).then(function() { + var newState = StateManager.addPendingRemoveContactsById(viewState, [userId]); + return render(newState); + }); + }; + + /** + * Send the repository a request to remove a user from the contacts list. update the statemanager + * and publish a contact has been removed. + * + * @param {Number} userId User id of user to remove from contacts. + * @return {Promise} Renderer promise. + */ + var removeContact = function(userId) { + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.deleteContacts(viewState.loggedInUserId, [userId]); + }) + .then(function(profiles) { + var newState = StateManager.addMembers(viewState, profiles); + newState = StateManager.removePendingRemoveContactsById(newState, [userId]); + newState = StateManager.setLoadingConfirmAction(newState, false); + PubSub.publish(MessageDrawerEvents.CONTACT_REMOVED, userId); + return render(newState); + }); + }; + + /** + * Tell the statemanager there is a request to add a user to the contact list + * and run the renderer to show the add user to contacts dialogue. + * + * @param {Number} userId User id of user to add to contacts. + * @return {Promise} Renderer promise. + */ + var requestAddContact = function(userId) { + return cancelRequest(userId).then(function() { + var newState = StateManager.addPendingAddContactsById(viewState, [userId]); + return render(newState); + }); + }; + + /** + * Send the repository a request to add a user to the contacts list. update the statemanager + * and publish a contact has been added. + * + * @param {Number} userId User id of user to add to contacts. + * @return {Promise} Renderer promise. + */ + var addContact = function(userId) { + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.createContactRequest(viewState.loggedInUserId, userId); + }) + .then(function(response) { + if (!response.request) { + throw new Error(response.warnings[0].message); + } + + return response.request; + }) + .then(function(request) { + var newState = StateManager.removePendingAddContactsById(viewState, [userId]); + newState = StateManager.addContactRequests(newState, [request]); + newState = StateManager.setLoadingConfirmAction(newState, false); + return render(newState); + }); + }; + + /** + * Set the current conversation as a favourite conversation. + * + * @return {Promise} Renderer promise. + */ + var setFavourite = function() { + var userId = viewState.loggedInUserId; + var conversationId = viewState.id; + + return Repository.setFavouriteConversations(userId, [conversationId]) + .then(function() { + var newState = StateManager.setIsFavourite(viewState, true); + return render(newState); + }) + .then(function() { + return PubSub.publish( + MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, + formatConversationForEvent(viewState) + ); + }); + }; + + /** + * Unset the current conversation as a favourite conversation. + * + * @return {Promise} Renderer promise. + */ + var unsetFavourite = function() { + var userId = viewState.loggedInUserId; + var conversationId = viewState.id; + + return Repository.unsetFavouriteConversations(userId, [conversationId]) + .then(function() { + var newState = StateManager.setIsFavourite(viewState, false); + return render(newState); + }) + .then(function() { + return PubSub.publish( + MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, + formatConversationForEvent(viewState) + ); + }); + }; + + /** + * Tell the statemanager there is a request to delete the selected messages + * and run the renderer to show confirm delete messages dialogue. + * + * @param {Number} userId User id. + * @return {Promise} Renderer promise. + */ + var requestDeleteSelectedMessages = function(userId) { + var selectedMessageIds = viewState.selectedMessageIds; + return cancelRequest(userId).then(function() { + var newState = StateManager.addPendingDeleteMessagesById(viewState, selectedMessageIds); + return render(newState); + }); + }; + + /** + * Send the repository a request to delete the messages pending deletion. Update the statemanager + * and publish a message deletion event. + * + * @return {Promise} Renderer promise. + */ + var deleteSelectedMessages = function() { + var messageIds = viewState.pendingDeleteMessageIds; + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.deleteMessages(viewState.loggedInUserId, messageIds); + }) + .then(function() { + var newState = StateManager.removeMessagesById(viewState, messageIds); + newState = StateManager.removePendingDeleteMessagesById(newState, messageIds); + newState = StateManager.removeSelectedMessagesById(newState, messageIds); + newState = StateManager.setLoadingConfirmAction(newState, false); + + var prevLastMessage = viewState.messages[viewState.messages.length - 1]; + var newLastMessage = newState.messages.length ? newState.messages[newState.messages.length - 1] : null; + + if (newLastMessage && newLastMessage.id != prevLastMessage.id) { + var conversation = formatConversationForEvent(newState); + PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation); + } else if (!newState.messages.length) { + PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id); + } + + return render(newState); + }); + }; + + /** + * Tell the statemanager there is a request to delete a conversation + * and run the renderer to show confirm delete conversation dialogue. + * + * @param {Number} userId User id of other user. + * @return {Promise} Renderer promise. + */ + var requestDeleteConversation = function(userId) { + return cancelRequest(userId).then(function() { + var newState = StateManager.setPendingDeleteConversation(viewState, true); + return render(newState); + }); + }; + + /** + * Send the repository a request to delete a conversation. Update the statemanager + * and publish a conversation deleted event. + * + * @return {Promise} Renderer promise. + */ + var deleteConversation = function() { + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.deleteCoversation(viewState.loggedInUserId, getOtherUserId()); + }) + .then(function() { + var newState = StateManager.removeMessages(viewState, viewState.messages); + newState = StateManager.removeSelectedMessagesById(newState, viewState.selectedMessageIds); + newState = StateManager.setPendingDeleteConversation(newState, false); + newState = StateManager.setLoadingConfirmAction(newState, false); + PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id); + return render(newState); + }); + }; + + /** + * Tell the statemanager to cancel all pending actions. + * + * @param {Number} userId User id. + * @return {Promise} Renderer promise. + */ + var cancelRequest = function(userId) { + var pendingDeleteMessageIds = viewState.pendingDeleteMessageIds; + var newState = StateManager.removePendingAddContactsById(viewState, [userId]); + newState = StateManager.removePendingRemoveContactsById(newState, [userId]); + newState = StateManager.removePendingUnblockUsersById(newState, [userId]); + newState = StateManager.removePendingBlockUsersById(newState, [userId]); + newState = StateManager.removePendingDeleteMessagesById(newState, pendingDeleteMessageIds); + newState = StateManager.setPendingDeleteConversation(newState, false); + return render(newState); + }; + + /** + * Accept the contact request from the given user. + * + * @param {Number} userId User id of other user. + * @return {Promise} Renderer promise. + */ + var acceptContactRequest = function(userId) { + // Search the list of the logged in user's contact requests to find the + // one from this user. + var loggedInUserId = viewState.loggedInUserId; + var requests = viewState.members[userId].contactrequests.filter(function(request) { + return request.requesteduserid == loggedInUserId; + }); + var request = requests[0]; + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.acceptContactRequest(userId, loggedInUserId); + }) + .then(function(profile) { + var newState = StateManager.removeContactRequests(viewState, [request]); + newState = StateManager.addMembers(viewState, [profile]); + newState = StateManager.setLoadingConfirmAction(newState, false); + return render(newState); + }) + .then(function() { + PubSub.publish(MessageDrawerEvents.CONTACT_ADDED, viewState.members[userId]); + PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, request); + return; + }); + }; + + /** + * Decline the contact request from the given user. + * + * @param {Number} userId User id of other user. + * @return {Promise} Renderer promise. + */ + var declineContactRequest = function(userId) { + // Search the list of the logged in user's contact requests to find the + // one from this user. + var loggedInUserId = viewState.loggedInUserId; + var requests = viewState.members[loggedInUserId].contactrequests.filter(function(request) { + return request.userid == userId; + }); + var request = requests[0]; + var newState = StateManager.setLoadingConfirmAction(viewState, true); + return render(newState) + .then(function() { + return Repository.declineContactRequest(userId, loggedInUserId); + }) + .then(function(profile) { + var newState = StateManager.removeContactRequests(viewState, [request]); + newState = StateManager.addMembers(viewState, [profile]); + newState = StateManager.setLoadingConfirmAction(newState, false); + return render(newState); + }) + .then(function() { + PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, request); + return; + }); + }; + + /** + * Send a message to the repository, update the statemanager publish a message send event + * and call the renderer. + * + * @param {Number} conversationId The conversation to send to. + * @param {String} text Text to send. + * @return {Promise} Renderer promise. + */ + var sendMessage = function(conversationId, text) { + var newState = StateManager.setSendingMessage(viewState, true); + var newConversationId = null; + return render(newState) + .then(function() { + if (!conversationId && viewState.type == CONVERSATION_TYPES.PRIVATE) { + // If it's a new private conversation then we need to use the old + // web service function to create the conversation. + var otherUserId = getOtherUserId(); + return Repository.sendMessageToUser(otherUserId, text) + .then(function(message) { + newConversationId = parseInt(message.conversationid, 10); + return message; + }); + } else { + return Repository.sendMessageToConversation(conversationId, text); + } + }) + .then(function(message) { + var newState = StateManager.addMessages(viewState, [message]); + newState = StateManager.setSendingMessage(newState, false); + var conversation = formatConversationForEvent(newState); + + if (!newState.id) { + // If this message created the conversation then save the conversation + // id. + newState = StateManager.setId(newState, newConversationId); + conversation.id = newConversationId; + resetMessagePollTimer(newConversationId); + PubSub.publish(MessageDrawerEvents.CONVERSATION_CREATED, conversation); + } + + return render(newState) + .then(function() { + PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation); + return; + }); + }) + .catch(function(error) { + var newState = StateManager.setSendingMessage(viewState, false); + render(newState); + Notification.exception(error); + }); + }; + + /** + * Toggle the selected messages update the statemanager and render the result. + * + * @param {Number} messageId The id of the message to be toggled + * @return {Promise} Renderer promise. + */ + var toggleSelectMessage = function(messageId) { + var newState = viewState; + + if (viewState.selectedMessageIds.indexOf(messageId) > -1) { + newState = StateManager.removeSelectedMessagesById(viewState, [messageId]); + } else { + newState = StateManager.addSelectedMessagesById(viewState, [messageId]); + } + + return render(newState); + }; + + /** + * Cancel edit mode (selecting the messages). + * + * @return {Promise} Renderer promise. + */ + var cancelEditMode = function() { + return cancelRequest(getOtherUserId()) + .then(function() { + var newState = StateManager.removeSelectedMessagesById(viewState, viewState.selectedMessageIds); + return render(newState); + }); + }; + + /** + * Create a function to render the Conversation. + * + * @param {Object} header The conversation header container element. + * @param {Object} body The conversation body container element. + * @param {Object} footer The conversation footer container element. + * @return {Promise} Renderer promise. + */ + var generateRenderFunction = function(header, body, footer) { + return function(newState) { + var patch = Patcher.buildPatch(viewState, newState); + // This is a great place to add in some console logging if you need + // to debug something. You can log the current state, the next state, + // and the generated patch and see exactly what will be updated. + return Renderer.render(header, body, footer, patch) + .then(function() { + viewState = newState; + if (newState.id) { + // Only cache created conversations. + stateCache[newState.id] = { + state: newState, + messagesOffset: getMessagesOffset(), + loadedAllMessages: hasLoadedAllMessages() + }; + } + return; + }); + }; + }; + + /** + * Create a confirm action function. + * + * @param {Function} actionCallback The callback function. + * @return {Function} Confirm action handler. + */ + var generateConfirmActionHandler = function(actionCallback) { + return function(e, data) { + if (!viewState.loadingConfirmAction) { + actionCallback(getOtherUserId()) + .catch(function(error) { + var newState = StateManager.setLoadingConfirmAction(viewState, false); + render(newState); + Notification.exception(error); + }); + } + data.originalEvent.preventDefault(); + }; + }; + + /** + * Send message event handler. + * + * @param {Object} e Element this event handler is called on. + * @param {Object} data Data for this event. + */ + var handleSendMessage = function(e, data) { + var target = $(e.target); + var footerContainer = target.closest(SELECTORS.FOOTER_CONTAINER); + var textArea = footerContainer.find(SELECTORS.MESSAGE_TEXT_AREA); + var text = textArea.val().trim(); + + if (text !== '') { + sendMessage(viewState.id, text); + } + + data.originalEvent.preventDefault(); + }; + + /** + * Select message event handler. + * + * @param {Object} e Element this event handler is called on. + * @param {Object} data Data for this event. + */ + var handleSelectMessage = function(e, data) { + var selection = window.getSelection(); + var target = $(e.target); + + if (selection.toString() != '') { + // Bail if we're selecting. + return; + } + + if (target.is('a')) { + // Clicking on a link in the message so ignore it. + return; + } + + var element = target.closest(SELECTORS.MESSAGE); + var messageId = parseInt(element.attr('data-message-id'), 10); + + toggleSelectMessage(messageId).catch(Notification.exception); + + data.originalEvent.preventDefault(); + }; + + /** + * Cancel edit mode event handler. + * + * @param {Object} e Element this event handler is called on. + * @param {Object} data Data for this event. + */ + var handleCancelEditMode = function(e, data) { + cancelEditMode().catch(Notification.exception); + data.originalEvent.preventDefault(); + }; + + /** + * Show the view contact page. + * + * @param {Object} e Element this event handler is called on. + * @param {Object} data Data for this event. + */ + var handleViewContact = function(e, data) { + var otherUserId = getOtherUserId(); + var otherUser = viewState.members[otherUserId]; + MessageDrawerRouter.go(MessageDrawerRoutes.VIEW_CONTACT, otherUser); + data.originalEvent.preventDefault(); + }; + + /** + * Set this conversation as a favourite. + * + * @param {Object} e Element this event handler is called on. + * @param {Object} data Data for this event. + */ + var handleSetFavourite = function(e, data) { + setFavourite().catch(Notification.exception); + data.originalEvent.preventDefault(); + }; + + /** + * Unset this conversation as a favourite. + * + * @param {Object} e Element this event handler is called on. + * @param {Object} data Data for this event. + */ + var handleUnsetFavourite = function(e, data) { + unsetFavourite().catch(Notification.exception); + data.originalEvent.preventDefault(); + }; + + /** + * Show the view contact page. + * + * @param {Object} e Element this event handler is called on. + * @param {Object} data Data for this event. + */ + var handleViewGroupInfo = function(e, data) { + MessageDrawerRouter.go( + MessageDrawerRoutes.VIEW_GROUP_INFO, + { + id: viewState.id, + name: viewState.name, + subname: viewState.subname, + imageUrl: viewState.imageUrl, + totalMemberCount: viewState.totalMemberCount + }, + viewState.loggedInUserId + ); + data.originalEvent.preventDefault(); + }; + + var headerActivateHandlers = [ + [SELECTORS.ACTION_REQUEST_BLOCK, generateConfirmActionHandler(requestBlockUser)], + [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)], + [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)], + [SELECTORS.ACTION_REQUEST_REMOVE_CONTACT, generateConfirmActionHandler(requestRemoveContact)], + [SELECTORS.ACTION_REQUEST_DELETE_CONVERSATION, generateConfirmActionHandler(requestDeleteConversation)], + [SELECTORS.ACTION_CANCEL_EDIT_MODE, handleCancelEditMode], + [SELECTORS.ACTION_VIEW_CONTACT, handleViewContact], + [SELECTORS.ACTION_VIEW_GROUP_INFO, handleViewGroupInfo], + [SELECTORS.ACTION_CONFIRM_FAVOURITE, handleSetFavourite], + [SELECTORS.ACTION_CONFIRM_UNFAVOURITE, handleUnsetFavourite], + ]; + var bodyActivateHandlers = [ + [SELECTORS.ACTION_CANCEL_CONFIRM, generateConfirmActionHandler(cancelRequest)], + [SELECTORS.ACTION_CONFIRM_BLOCK, generateConfirmActionHandler(blockUser)], + [SELECTORS.ACTION_CONFIRM_UNBLOCK, generateConfirmActionHandler(unblockUser)], + [SELECTORS.ACTION_CONFIRM_ADD_CONTACT, generateConfirmActionHandler(addContact)], + [SELECTORS.ACTION_CONFIRM_REMOVE_CONTACT, generateConfirmActionHandler(removeContact)], + [SELECTORS.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(deleteSelectedMessages)], + [SELECTORS.ACTION_CONFIRM_DELETE_CONVERSATION, generateConfirmActionHandler(deleteConversation)], + [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)], + [SELECTORS.ACTION_ACCEPT_CONTACT_REQUEST, generateConfirmActionHandler(acceptContactRequest)], + [SELECTORS.ACTION_DECLINE_CONTACT_REQUEST, generateConfirmActionHandler(declineContactRequest)], + [SELECTORS.MESSAGE, handleSelectMessage] + ]; + var footerActivateHandlers = [ + [SELECTORS.SEND_MESSAGE_BUTTON, handleSendMessage], + [SELECTORS.ACTION_REQUEST_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(requestDeleteSelectedMessages)], + [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)], + [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)], + ]; + + /** + * Listen to, and handle events for conversations. + * + * @param {Object} header Conversation header container element. + * @param {Object} body Conversation body container element. + * @param {Object} footer Conversation footer container element. + */ + var registerEventListeners = function(header, body, footer) { + var isLoadingMoreMessages = false; + var messagesContainer = getMessagesContainer(body); + + AutoRows.init(footer); + + CustomEvents.define(header, [ + CustomEvents.events.activate + ]); + CustomEvents.define(body, [ + CustomEvents.events.activate + ]); + CustomEvents.define(footer, [ + CustomEvents.events.activate + ]); + CustomEvents.define(messagesContainer, [ + CustomEvents.events.scrollTop, + CustomEvents.events.scrollLock + ]); + + messagesContainer.on(CustomEvents.events.scrollTop, function(e, data) { + var hasMembers = Object.keys(viewState.members).length > 1; + + if (!isLoadingMoreMessages && !hasLoadedAllMessages() && hasMembers) { + var newState = StateManager.setLoadingMessages(viewState, true); + render(newState) + .then(function() { + return loadMessages(viewState.id, LOAD_MESSAGE_LIMIT, getMessagesOffset(), NEWEST_FIRST, []); + }) + .then(function() { + isLoadingMoreMessages = false; + setMessagesOffset(getMessagesOffset() + LOAD_MESSAGE_LIMIT); + return; + }) + .catch(function(error) { + isLoadingMoreMessages = false; + Notification.exception(error); + }); + } + + data.originalEvent.preventDefault(); + }); + + headerActivateHandlers.forEach(function(handler) { + var selector = handler[0]; + var handlerFunction = handler[1]; + header.on(CustomEvents.events.activate, selector, handlerFunction); + }); + + bodyActivateHandlers.forEach(function(handler) { + var selector = handler[0]; + var handlerFunction = handler[1]; + body.on(CustomEvents.events.activate, selector, handlerFunction); + }); + + footerActivateHandlers.forEach(function(handler) { + var selector = handler[0]; + var handlerFunction = handler[1]; + footer.on(CustomEvents.events.activate, selector, handlerFunction); + }); + + PubSub.subscribe(MessageDrawerEvents.ROUTE_CHANGED, function(newRouteData) { + if (newMessagesPollTimer) { + if (newRouteData.route == MessageDrawerRoutes.VIEW_CONVERSATION) { + newMessagesPollTimer.restart(); + } else { + newMessagesPollTimer.stop(); + } + } + }); + }; + + /** + * Reset the timer that polls for new messages. + * + * @param {Number} conversationId The conversation id + */ + var resetMessagePollTimer = function(conversationId) { + if (newMessagesPollTimer) { + newMessagesPollTimer.stop(); + } + + newMessagesPollTimer = new BackOffTimer( + getLoadNewMessagesCallback(conversationId, NEWEST_FIRST), + function(time) { + if (!time) { + return INITIAL_NEW_MESSAGE_POLL_TIMEOUT; + } + + return time * 2; + } + ); + + newMessagesPollTimer.start(); + }; + + /** + * Reset the state to the initial state and render the UI. + * + * @param {Object} body Conversation body container element. + * @param {Number|null} conversationId The conversation id. + * @param {Object} loggedInUserProfile The logged in user's profile. + * @return {Promise} Renderer promise. + */ + var resetState = function(body, conversationId, loggedInUserProfile) { + var loggedInUserId = loggedInUserProfile.id; + var midnight = parseInt(body.attr('data-midnight'), 10); + var initialState = StateManager.buildInitialState(midnight, loggedInUserId, conversationId); + + if (!viewState) { + viewState = initialState; + } + + if (newMessagesPollTimer) { + newMessagesPollTimer.stop(); + } + + return render(initialState); + }; + + /** + * Load a new empty private conversation between two users. + * + * @param {Object} body Conversation body container element. + * @param {Object} loggedInUserProfile The logged in user's profile. + * @param {Int} otherUserId The other user's id. + * @return {Promise} Renderer promise. + */ + var resetNoConversation = function(body, loggedInUserProfile, otherUserId) { + // Always reset the state back to the initial state so that the + // state manager and patcher can work correctly. + return resetState(body, null, loggedInUserProfile) + .then(function() { + return Repository.getConversationBetweenUsers( + loggedInUserProfile.id, + otherUserId, + true, + true, + 0, + 0, + LOAD_MESSAGE_LIMIT, + 0, + NEWEST_FIRST + ) + .then(function(conversation) { + // Looks like we have a conversation after all! Let's use that. + return resetByConversation(body, conversation, loggedInUserProfile); + }) + .catch(function() { + // Can't find a conversation. Oh well. Just load up a blank one. + return loadEmptyPrivateConversation(loggedInUserProfile, otherUserId); + }); + }); + }; + + /** + * Load new messages into the conversation based on a time interval. + * + * @param {Object} body Conversation body container element. + * @param {Number} conversationId The conversation id. + * @param {Object} loggedInUserProfile The logged in user's profile. + * @return {Promise} Renderer promise. + */ + var resetById = function(body, conversationId, loggedInUserProfile) { + var cache = null; + if (conversationId in stateCache) { + cache = stateCache[conversationId]; + } + + // Always reset the state back to the initial state so that the + // state manager and patcher can work correctly. + return resetState(body, conversationId, loggedInUserProfile) + .then(function() { + if (cache) { + // We've seen this conversation before so there is no need to + // send any network requests. + var newState = cache.state; + // Reset some loading states just in case they were left weirdly. + newState = StateManager.setLoadingMessages(newState, false); + newState = StateManager.setLoadingMembers(newState, false); + setMessagesOffset(cache.messagesOffset); + setLoadedAllMessages(cache.loadedAllMessages); + return render(newState); + } else { + return loadNewConversation( + conversationId, + loggedInUserProfile, + LOAD_MESSAGE_LIMIT, + 0, + NEWEST_FIRST + ); + } + }) + .then(function() { + return resetMessagePollTimer(conversationId); + }); + }; + + /** + * Load new messages into the conversation based on a time interval. + * + * @param {Object} body Conversation body container element. + * @param {Object} conversation The conversation. + * @param {Object} loggedInUserProfile The logged in user's profile. + * @return {Promise} Renderer promise. + */ + var resetByConversation = function(body, conversation, loggedInUserProfile) { + var cache = null; + if (conversation.id in stateCache) { + cache = stateCache[conversation.id]; + } + + // Always reset the state back to the initial state so that the + // state manager and patcher can work correctly. + return resetState(body, conversation.id, loggedInUserProfile) + .then(function() { + if (cache) { + // We've seen this conversation before so there is no need to + // send any network requests. + var newState = cache.state; + // Reset some loading states just in case they were left weirdly. + newState = StateManager.setLoadingMessages(newState, false); + newState = StateManager.setLoadingMembers(newState, false); + setMessagesOffset(cache.messagesOffset); + setLoadedAllMessages(cache.loadedAllMessages); + return render(newState); + } else { + return loadExistingConversation( + conversation, + loggedInUserProfile, + LOAD_MESSAGE_LIMIT, + NEWEST_FIRST + ); + } + }) + .then(function() { + return resetMessagePollTimer(conversation.id); + }); + }; + + /** + * Setup the conversation page. This is a rather complex function because there are a + * few combinations of arguments that can be provided to this function to show the + * conversation. + * + * There are: + * 1.) A conversation object with no action or other user id (e.g. from the overview page) + * 2.) A conversation id with no action or other user id (e.g. from the contacts page) + * 3.) No conversation/id with an action and other other user id. (e.g. from contact page) + * + * @param {Object} header Conversation header container element. + * @param {Object} body Conversation body container element. + * @param {Object} footer Conversation footer container element. + * @param {Object|Number|null} conversationOrId Conversation or id or null + * @param {String} action An action to take on the conversation + * @param {Number} otherUserId The other user id for a private conversation + * @return {Object} jQuery promise + */ + var show = function(header, body, footer, conversationOrId, action, otherUserId) { + var conversation = null; + var conversationId = null; + + // Check what we were given to identify the conversation. + if (typeof conversationOrId == 'object') { + conversation = conversationOrId; + conversationId = parseInt(conversation.id, 10); + } else { + conversation = null; + conversationId = parseInt(conversationOrId, 10); + conversationId = isNaN(conversationId) ? null : conversationId; + } + + if (!conversationId && action && otherUserId) { + // If we didn't get a conversation id got a user id then let's see if we've + // previously loaded a private conversation with this user. + conversationId = getCachedPrivateConversationIdFromUserId(otherUserId); + } + + if (!body.attr('data-init')) { + // Generate the render function to bind the header, body, and footer + // elements to it so that we don't need to pass them around this module. + render = generateRenderFunction(header, body, footer); + registerEventListeners(header, body, footer); + body.attr('data-init', true); + } + + // This is a new conversation if: + // 1. We don't already have a state + // 2. The given conversation doesn't match the one currently loaded + // 3. We have a view state without a conversation id and we weren't given one + // but we were given a different other user id. This happens when the user + // goes from viewing a user that they haven't yet initialised a conversation + // with to viewing a different user that they also haven't initialised a + // conversation with. + var isNewConversation = !viewState || (viewState.id != conversationId) || (otherUserId && otherUserId != getOtherUserId()); + if (isNewConversation) { + // Reset all of the states back to the beginning if we're loading a new + // conversation. + var renderPromise = null; + var loggedInUserProfile = getLoggedInUserProfile(body); + if (conversation) { + renderPromise = resetByConversation(body, conversation, loggedInUserProfile, otherUserId); + } else if (conversationId) { + renderPromise = resetById(body, conversationId, loggedInUserProfile, otherUserId); + } else { + renderPromise = resetNoConversation(body, loggedInUserProfile, otherUserId); + } + + return renderPromise + .then(function() { + // Focus the first element that can receieve it in the header. + header.find(Constants.SELECTORS.CAN_RECEIVE_FOCUS).first().focus(); + return; + }) + .catch(Notification.exception); + } else if (viewState.type == CONVERSATION_TYPES.PRIVATE && action) { + // There are special actions that the user can perform in a private (aka 1-to-1) + // conversation. + var currentOtherUserId = getOtherUserId(); + + switch (action) { + case 'block': + return requestBlockUser(currentOtherUserId); + case 'unblock': + return requestUnblockUser(currentOtherUserId); + case 'add-contact': + return requestAddContact(currentOtherUserId); + case 'remove-contact': + return requestRemoveContact(currentOtherUserId); + } + } + + // Final fallback to return a promise if we didn't need to do anything. + return $.Deferred().resolve().promise(); + }; + + /** + * String describing this page used for aria-labels. + * + * @return {Object} jQuery promise + */ + var description = function() { + return Str.get_string('messagedrawerviewconversation', 'core_message', viewState.name); + }; + + return { + show: show, + description: description + }; +}); diff --git a/message/amd/src/message_drawer_view_conversation_constants.js b/message/amd/src/message_drawer_view_conversation_constants.js new file mode 100644 index 00000000000..83507c00528 --- /dev/null +++ b/message/amd/src/message_drawer_view_conversation_constants.js @@ -0,0 +1,106 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Constant values for the conversation page in the message drawer. + * + * @module core_message/message_drawer_view_conversation_constants + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([], function() { + + var SELECTORS = { + ACTION_ACCEPT_CONTACT_REQUEST: '[data-action="accept-contact-request"]', + ACTION_CANCEL_CONFIRM: '[data-action="cancel-confirm"]', + ACTION_CANCEL_EDIT_MODE: '[data-action="cancel-edit-mode"]', + ACTION_CONFIRM_ADD_CONTACT: '[data-action="confirm-add-contact"]', + ACTION_CONFIRM_BLOCK: '[data-action="confirm-block"]', + ACTION_CONFIRM_DELETE_SELECTED_MESSAGES: '[data-action="confirm-delete-selected-messages"]', + ACTION_CONFIRM_DELETE_CONVERSATION: '[data-action="confirm-delete-conversation"]', + ACTION_CONFIRM_FAVOURITE: '[data-action="confirm-favourite"]', + ACTION_CONFIRM_UNFAVOURITE: '[data-action="confirm-unfavourite"]', + ACTION_CONFIRM_REMOVE_CONTACT: '[data-action="confirm-remove-contact"]', + ACTION_CONFIRM_UNBLOCK: '[data-action="confirm-unblock"]', + ACTION_DECLINE_CONTACT_REQUEST: '[data-action="decline-contact-request"]', + ACTION_REQUEST_ADD_CONTACT: '[data-action="request-add-contact"]', + ACTION_REQUEST_BLOCK: '[data-action="request-block"]', + ACTION_REQUEST_DELETE_CONVERSATION: '[data-action="request-delete-conversation"]', + ACTION_REQUEST_DELETE_SELECTED_MESSAGES: '[data-action="delete-selected-messages"]', + ACTION_REQUEST_REMOVE_CONTACT: '[data-action="request-remove-contact"]', + ACTION_REQUEST_UNBLOCK: '[data-action="request-unblock"]', + ACTION_VIEW_CONTACT: '[data-action="view-contact"]', + ACTION_VIEW_GROUP_INFO: '[data-action="view-group-info"]', + CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]', + CONFIRM_DIALOGUE_BUTTON_TEXT: '[data-region="dialogue-button-text"]', + CONFIRM_DIALOGUE_CANCEL_BUTTON: '[data-action="cancel-confirm"]', + CONFIRM_DIALOGUE_CONTAINER: '[data-region="confirm-dialogue-container"]', + CONFIRM_DIALOGUE_HEADER: '[data-region="dialogue-header"]', + CONFIRM_DIALOGUE_TEXT: '[data-region="dialogue-text"]', + CONTACT_REQUEST_SENT_MESSAGE_CONTAINER: '[data-region="contact-request-sent-message-container"]', + CONTENT_PLACEHOLDER_CONTAINER: '[data-region="content-placeholder"]', + CONTENT_CONTAINER: '[data-region="content-container"]', + CONTENT_MESSAGES_CONTAINER: '[data-region="content-message-container"]', + CONTENT_MESSAGES_FOOTER_CONTAINER: '[data-region="content-messages-footer-container"]', + CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER: '[data-region="content-messages-footer-edit-mode-container"]', + CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER: '[data-region="content-messages-footer-require-contact-container"]', + CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER: '[data-region="content-messages-footer-require-unblock-container"]', + CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER: '[data-region="content-messages-footer-unable-to-message"]', + DAY_MESSAGES_CONTAINER: '[data-region="day-messages-container"]', + FAVOURITE_ICON_CONTAINER: '[data-region="favourite-icon-container"]', + FOOTER_CONTAINER: '[data-region="content-messages-footer-container"]', + HEADER: '[data-region="header-content"]', + HEADER_EDIT_MODE: '[data-region="header-edit-mode"]', + HEADER_PLACEHOLDER_CONTAINER: '[data-region="header-placeholder"]', + LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', + MESSAGE: '[data-region="message"]', + MESSAGE_NOT_SELECTED: '[data-region="message"][aria-checked="false"]', + MESSAGE_NOT_SELECTED_ICON: '[data-region="not-selected-icon"]', + MESSAGE_SELECTED_ICON: '[data-region="selected-icon"]', + MESSAGES: '[data-region="content-message-container"]', + MESSAGES_CONTAINER: '[data-region="content-message-container"]', + MESSAGES_SELECTED_COUNT: '[data-region="message-selected-court"]', + MESSAGE_TEXT_AREA: '[data-region="send-message-txt"]', + MORE_MESSAGES_LOADING_ICON_CONTAINER: '[data-region="more-messages-loading-icon-container"]', + PLACEHOLDER_CONTAINER: '[data-region="placeholder-container"]', + SEND_MESSAGE_BUTTON: '[data-action="send-message"]', + SEND_MESSAGE_ICON_CONTAINER: '[data-region="send-icon-container"]', + TEXT: '[data-region="text"]', + TITLE: '[data-region="title"]' + }; + + var TEMPLATES = { + HEADER_PRIVATE: 'core_message/message_drawer_view_conversation_header_content_type_private', + HEADER_PRIVATE_NO_CONTROLS: 'core_message/message_drawer_view_conversation_header_content_type_private_no_controls', + HEADER_PUBLIC: 'core_message/message_drawer_view_conversation_header_content_type_public', + DAY: 'core_message/message_drawer_view_conversation_body_day', + MESSAGE: 'core_message/message_drawer_view_conversation_body_message', + MESSAGES: 'core_message/message_drawer_view_conversation_body_messages' + }; + + var CONVERSATION_TYPES = { + PRIVATE: 1, + PUBLIC: 2 + }; + + return { + SELECTORS: SELECTORS, + TEMPLATES: TEMPLATES, + CONVERSATION_TYPES: CONVERSATION_TYPES, + NEWEST_MESSAGES_FIRST: true, + LOAD_MESSAGE_LIMIT: 100, + INITIAL_NEW_MESSAGE_POLL_TIMEOUT: 1000 + }; +}); diff --git a/message/amd/src/message_drawer_view_conversation_patcher.js b/message/amd/src/message_drawer_view_conversation_patcher.js new file mode 100644 index 00000000000..f1a5df97bda --- /dev/null +++ b/message/amd/src/message_drawer_view_conversation_patcher.js @@ -0,0 +1,1160 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * This module will take 2 view states from the message_drawer_view_conversation + * module and generate a patch that can be given to the + * message_drawer_view_conversation_renderer module to update the UI. + * + * This module should never modify either state. It's purely a read only + * module. + * + * @module core_message/message_drawer_view_conversation_patcher + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/user_date', + 'core_message/message_drawer_view_conversation_constants' +], +function( + $, + UserDate, + Constants +) { + /** + * Sort messages by day. + * + * @param {Array} messages The list of messages to sort. + * @param {Number} midnight User's midnight timestamp. + * @return {Array} messages sorted by day. + */ + var sortMessagesByDay = function(messages, midnight) { + var messagesByDay = messages.reduce(function(carry, message) { + var dayTimestamp = UserDate.getUserMidnightForTimestamp(message.timeCreated, midnight); + + if (carry.hasOwnProperty(dayTimestamp)) { + carry[dayTimestamp].push(message); + } else { + carry[dayTimestamp] = [message]; + } + + return carry; + }, {}); + + return Object.keys(messagesByDay).map(function(dayTimestamp) { + return { + timestamp: dayTimestamp, + messages: messagesByDay[dayTimestamp] + }; + }); + }; + + /** + * Diff 2 arrays using a match function + * + * @param {Array} a The first array. + * @param {Array} b The second array. + * @param {Function} matchFunction Function used for matching array items. + * @return {Object} Object containing array items missing from a, array items missing from b + * and matches + */ + var diffArrays = function(a, b, matchFunction) { + // Make copy of it. + b = b.slice(); + var missingFromA = []; + var missingFromB = []; + var matches = []; + + a.forEach(function(current) { + var found = false; + var index = 0; + + for (; index < b.length; index++) { + var next = b[index]; + + if (matchFunction(current, next)) { + found = true; + matches.push({ + a: current, + b: next + }); + break; + } + } + + if (found) { + // This day has been processed so removed it from the list. + b.splice(index, 1); + } else { + // If we couldn't find it in the next messages then it means + // it needs to be added. + missingFromB.push(current); + } + }); + + missingFromA = b; + + return { + missingFromA: missingFromA, + missingFromB: missingFromB, + matches: matches + }; + }; + + /** + * Find an element in a array based on a matching function. + * + * @param {array} array Array to search. + * @param {Function} breakFunction Function to run on array item. + * @return {*} The array item. + */ + var findPositionInArray = function(array, breakFunction) { + var before = null; + + for (var i = 0; i < array.length; i++) { + var candidate = array[i]; + + if (breakFunction(candidate)) { + return candidate; + } + } + + return before; + }; + + /** + * Check if 2 arrays are equal. + * + * @param {Array} a The first array. + * @param {Array} b The second array. + * @return {Boolean} Are arrays equal. + */ + var isArrayEqual = function(a, b) { + a.sort(); + b.sort(); + var aLength = a.length; + var bLength = b.length; + + if (aLength < 1 && bLength < 1) { + return true; + } + + if (aLength != bLength) { + return false; + } + + return a.every(function(item, index) { + return item == b[index]; + }); + }; + + /** + * Build a patch based on days. + * + * @param {Object} current Current list current items. + * @param {Object} daysDiff Difference between current and new. + * @return {Object} Patch with elements to add and remove. + */ + var buildDaysPatch = function(current, daysDiff) { + return { + remove: daysDiff.missingFromB, + add: daysDiff.missingFromA.map(function(day) { + // Any days left over in the "next" list weren't in the "current" list + // so they will need to be added. + var before = findPositionInArray(current, function(candidate) { + return day.timestamp < candidate.timestamp; + }); + + return { + before: before, + value: day + }; + }) + }; + }; + + /** + * Build the messages patch for each day. + * + * @param {Array} matchingDays Array of old and new messages sorted by day. + * @return {Object} patch. + */ + var buildMessagesPatch = function(matchingDays) { + var remove = []; + var add = []; + + matchingDays.forEach(function(days) { + var dayCurrent = days.a; + var dayNext = days.b; + var messagesDiff = diffArrays(dayCurrent.messages, dayNext.messages, function(messageCurrent, messageNext) { + return messageCurrent.id == messageNext.id; + }); + + remove = remove.concat(messagesDiff.missingFromB); + + messagesDiff.missingFromA.forEach(function(message) { + var before = findPositionInArray(dayCurrent.messages, function(candidate) { + return message.timeCreated < candidate.timeCreated; + }); + + add.push({ + before: before, + value: message, + day: dayCurrent + }); + }); + }); + + return { + add: add, + remove: remove + }; + }; + + /** + * Build a patch for this conversation. + * + * @param {Object} state, The current state of this conversation. + * @param {Object} newState, The new state of this conversation. + * @return {Object} Patch with days and messsages for each day. + */ + var buildConversationPatch = function(state, newState) { + var oldMessageIds = state.messages.map(function(message) { + return message.id; + }); + var newMessageIds = newState.messages.map(function(message) { + return message.id; + }); + + if (!isArrayEqual(oldMessageIds, newMessageIds)) { + var current = sortMessagesByDay(state.messages, state.midnight); + var next = sortMessagesByDay(newState.messages, newState.midnight); + var daysDiff = diffArrays(current, next, function(dayCurrent, dayNext) { + return dayCurrent.timestamp == dayNext.timestamp; + }); + + return { + days: buildDaysPatch(current, daysDiff), + messages: buildMessagesPatch(daysDiff.matches) + }; + } else { + return null; + } + }; + + /** + * Build a patch for the header of this conversation. Check if this conversation + * is a group conversation. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object} patch + */ + var buildHeaderPatchTypePrivate = function(state, newState) { + var requireAddContact = buildRequireAddContact(state, newState); + var confirmContactRequest = buildConfirmContactRequest(state, newState); + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + var requiresAddContact = requireAddContact && requireAddContact.show && !requireAddContact.hasMessages; + var requiredAddContact = requireAddContact && !requireAddContact.show; + // Render the header once we've got a user. + var shouldRenderHeader = !oldOtherUser && newOtherUser; + // We should also re-render the header if the other user requires + // being added as a contact or if they did but no longer do. + shouldRenderHeader = shouldRenderHeader || requiresAddContact || requiredAddContact; + // Finally, we should re-render if the other user has sent this user + // a contact request that is waiting for approval or if it's been approved/declined. + shouldRenderHeader = shouldRenderHeader || confirmContactRequest !== null; + + if (shouldRenderHeader) { + return { + type: Constants.CONVERSATION_TYPES.PRIVATE, + // We can show controls if the other user doesn't require add contact + // and we aren't waiting for this user to respond to a contact request. + showControls: !requiresAddContact && !confirmContactRequest, + context: { + id: newState.id, + name: newState.name, + subname: newState.subname, + totalmembercount: newState.totalMemberCount, + imageurl: newState.imageUrl, + isfavourite: newState.isFavourite, + userid: newOtherUser.id, + showonlinestatus: newOtherUser.showonlinestatus, + isonline: newOtherUser.isonline, + isblocked: newOtherUser.isblocked, + iscontact: newOtherUser.iscontact + } + }; + } + + return null; + }; + + + /** + * Build a patch for the header of this conversation. Check if this conversation + * is a group conversation. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object} patch + */ + var buildHeaderPatchTypePublic = function(state, newState) { + var totalMemberCount = newState.totalMemberCount; + + if (totalMemberCount === null) { + return null; + } else { + return { + type: Constants.CONVERSATION_TYPES.PUBLIC, + showControls: true, + context: { + id: newState.id, + name: newState.name, + subname: newState.subname, + totalmembercount: totalMemberCount, + imageurl: newState.imageUrl, + isfavourite: newState.isFavourite + } + }; + } + }; + + /** + * Find the newest or oldest message. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Number} Oldest or newest message id. + */ + var buildScrollToMessagePatch = function(state, newState) { + var oldMessages = state.messages; + var newMessages = newState.messages; + + if (newMessages.length < 1) { + return null; + } + + if (oldMessages.length < 1) { + return newMessages[newMessages.length - 1].id; + } + + var previousNewest = oldMessages[state.messages.length - 1]; + var currentNewest = newMessages[newMessages.length - 1]; + var previousOldest = oldMessages[0]; + var currentOldest = newMessages[0]; + + if (previousNewest.id != currentNewest.id) { + return currentNewest.id; + } else if (previousOldest.id != currentOldest.id) { + return previousOldest.id; + } + + return null; + }; + + /** + * Check if members should be loaded. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildLoadingMembersPatch = function(state, newState) { + if (!state.loadingMembers && newState.loadingMembers) { + return true; + } else if (state.loadingMembers && !newState.loadingMembers) { + return false; + } else { + return null; + } + }; + + /** + * Check if the messages are being loaded for the first time. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildLoadingFirstMessages = function(state, newState) { + if (state.hasTriedToLoadMessages === newState.hasTriedToLoadMessages) { + return null; + } else if (!newState.hasTriedToLoadMessages && newState.loadingMessages) { + return true; + } else if (newState.hasTriedToLoadMessages && !newState.loadingMessages) { + return false; + } else { + return null; + } + }; + + /** + * Check if the messages are still being loaded + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildLoadingMessages = function(state, newState) { + if (!state.loadingMessages && newState.loadingMessages) { + return true; + } else if (state.loadingMessages && !newState.loadingMessages) { + return false; + } else { + return null; + } + }; + + /** + * Check if the messages are still being send + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} User Object if Object. + */ + var buildSendingMessage = function(state, newState) { + if (!state.sendingMessage && newState.sendingMessage) { + return true; + } else if (state.sendingMessage && !newState.sendingMessage) { + return false; + } else { + return null; + } + }; + + /** + * Get the user Object of user to be blocked if pending. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object|Bool|Null} User Object if Object. + */ + var buildConfirmBlockUser = function(state, newState) { + if (newState.pendingBlockUserIds.length) { + // We currently only support a single user; + var userId = newState.pendingBlockUserIds[0]; + return newState.members[userId]; + } else if (state.pendingBlockUserIds.length) { + return false; + } + + return null; + }; + + /** + * Get the user Object of user to be unblocked if pending. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object|Bool|Null} User Object if Object. + */ + var buildConfirmUnblockUser = function(state, newState) { + if (newState.pendingUnblockUserIds.length) { + // We currently only support a single user; + var userId = newState.pendingUnblockUserIds[0]; + return newState.members[userId]; + } else if (state.pendingUnblockUserIds.length) { + return false; + } + + return null; + }; + + /** + * Get the user Object of user to be added as contact if pending. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object|Bool|Null} User Object if Object. + */ + var buildConfirmAddContact = function(state, newState) { + if (newState.pendingAddContactIds.length) { + // We currently only support a single user; + var userId = newState.pendingAddContactIds[0]; + return newState.members[userId]; + } else if (state.pendingAddContactIds.length) { + return false; + } + + return null; + }; + + /** + * Get the user Object of user to be removed as contact if pending. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object|Bool|Null} User Object if Object. + */ + var buildConfirmRemoveContact = function(state, newState) { + if (newState.pendingRemoveContactIds.length) { + // We currently only support a single user; + var userId = newState.pendingRemoveContactIds[0]; + return newState.members[userId]; + } else if (state.pendingRemoveContactIds.length) { + return false; + } + + return null; + }; + + /** + * Check if there are any messages to be deleted. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildConfirmDeleteSelectedMessages = function(state, newState) { + if (newState.pendingDeleteMessageIds.length) { + return true; + } else if (state.pendingDeleteMessageIds.length) { + return false; + } + + return null; + }; + + /** + * Check if there is a conversation to be deleted. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildConfirmDeleteConversation = function(state, newState) { + if (!state.pendingDeleteConversation && newState.pendingDeleteConversation) { + return true; + } else if (state.pendingDeleteConversation && !newState.pendingDeleteConversation) { + return false; + } + + return null; + }; + + /** + * Check if there is a pending contact request to accept or decline. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildConfirmContactRequest = function(state, newState) { + var loggedInUserId = state.loggedInUserId; + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + var oldReceivedRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) { + return request.requesteduserid == loggedInUserId && request.userid == oldOtherUser.id; + }); + var newReceivedRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) { + return request.requesteduserid == loggedInUserId && request.userid == newOtherUser.id; + }); + var oldRequest = oldReceivedRequests.length ? oldReceivedRequests[0] : null; + var newRequest = newReceivedRequests.length ? newReceivedRequests[0] : null; + + if (!oldRequest && newRequest) { + return newOtherUser; + } else if (oldRequest && !newRequest) { + return false; + } else { + return null; + } + }; + + /** + * Check if there are any changes in blocked users. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildIsBlocked = function(state, newState) { + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + + if (!oldOtherUser && !newOtherUser) { + return null; + } else if (!oldOtherUser && newOtherUser) { + return newOtherUser.isblocked ? true : null; + } else if (!newOtherUser && oldOtherUser) { + return oldOtherUser.isblocked ? false : null; + } else if (oldOtherUser.isblocked && !newOtherUser.isblocked) { + return false; + } else if (!oldOtherUser.isblocked && newOtherUser.isblocked) { + return true; + } else { + return null; + } + }; + + /** + * Check if there are any changes the conversation favourite state. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildIsFavourite = function(state, newState) { + var oldIsFavourite = state.isFavourite; + var newIsFavourite = newState.isFavourite; + + if (oldIsFavourite == newIsFavourite) { + // No change. + return null; + } else if (!oldIsFavourite && newIsFavourite) { + return true; + } else if (oldIsFavourite && !newIsFavourite) { + return false; + } else { + return null; + } + }; + + /** + * Check if there are any changes in the contact status of the current user + * and other user. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildIsContact = function(state, newState) { + var loggedInUserId = state.loggedInUserId; + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + var oldContactRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) { + return (request.userid == loggedInUserId && request.requesteduserid == oldOtherUser.id) || + (request.userid == oldOtherUser.id && request.requesteduserid == loggedInUserId); + }); + var newContactRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) { + return (request.userid == loggedInUserId && request.requesteduserid == newOtherUser.id) || + (request.userid == newOtherUser.id && request.requesteduserid == loggedInUserId); + }); + var oldHasContactRequests = oldContactRequests.length > 0; + var newHasContactRequests = newContactRequests.length > 0; + + if (!oldOtherUser && !newOtherUser) { + return null; + } else if (oldHasContactRequests && newHasContactRequests) { + return null; + } else if (!oldHasContactRequests && newHasContactRequests && !newOtherUser.iscontact) { + return 'pending-contact'; + } else if (!oldOtherUser && newOtherUser) { + return newOtherUser.iscontact ? 'contact' : null; + } else if (!newOtherUser && oldOtherUser) { + return oldOtherUser.iscontact ? 'non-contact' : null; + } else if (oldOtherUser.iscontact && !newOtherUser.iscontact) { + return newHasContactRequests ? 'pending-contact' : 'non-contact'; + } else if (!oldOtherUser.iscontact && newOtherUser.iscontact) { + return 'contact'; + } else { + return null; + } + }; + + /** + * Check if a confirm action is active. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildLoadingConfirmationAction = function(state, newState) { + if (!state.loadingConfirmAction && newState.loadingConfirmAction) { + return true; + } else if (state.loadingConfirmAction && !newState.loadingConfirmAction) { + return false; + } else { + return null; + } + }; + + /** + * Check if a edit mode is active. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildInEditMode = function(state, newState) { + var oldHasSelectedMessages = state.selectedMessageIds.length > 0; + var newHasSelectedMessages = newState.selectedMessageIds.length > 0; + var numberOfMessagesHasChanged = state.messages.length != newState.messages.length; + + if (!oldHasSelectedMessages && newHasSelectedMessages) { + return true; + } else if (oldHasSelectedMessages && !newHasSelectedMessages) { + return false; + } else if (oldHasSelectedMessages && numberOfMessagesHasChanged) { + return true; + } else { + return null; + } + }; + + /** + * Build a patch for the messages selected. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object} patch + */ + var buildSelectedMessages = function(state, newState) { + var oldSelectedMessages = state.selectedMessageIds; + var newSelectedMessages = newState.selectedMessageIds; + + if (isArrayEqual(oldSelectedMessages, newSelectedMessages)) { + return null; + } + + var diff = diffArrays(oldSelectedMessages, newSelectedMessages, function(a, b) { + return a == b; + }); + + return { + count: newSelectedMessages.length, + add: diff.missingFromA, + remove: diff.missingFromB + }; + }; + + /** + * Get a list of users from the state that are not the logged in user. Use to find group + * message members or the other user in a conversation. + * + * @param {Object} state State + * @return {Array} List of users. + */ + var getOtherUserFromState = function(state) { + return Object.keys(state.members).reduce(function(carry, userId) { + if (userId != state.loggedInUserId && !carry) { + carry = state.members[userId]; + } + + return carry; + }, null); + }; + + /** + * Check if the given user requires a contact request from the logged in user. + * + * @param {Integer} loggedInUserId The logged in user id + * @param {Object} user User record + * @return {Bool} + */ + var requiresContactRequest = function(loggedInUserId, user) { + var contactRequests = user.contactrequests.filter(function(request) { + return request.userid == loggedInUserId || request.requesteduserid; + }); + var hasSentContactRequest = contactRequests.length > 0; + return user.requirescontact && !user.iscontact && !hasSentContactRequest; + }; + + /** + * Check if other users are required to be added as contact. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object} Object controlling the required to add contact dialog variables. + */ + var buildRequireAddContact = function(state, newState) { + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + var hadMessages = state.messages.length > 0; + var hasMessages = newState.messages.length > 0; + var loggedInUserId = newState.loggedInUserId; + var prevRequiresContactRequest = oldOtherUser && requiresContactRequest(loggedInUserId, oldOtherUser); + var nextRequiresContactRequest = newOtherUser && requiresContactRequest(loggedInUserId, newOtherUser); + var confirmAddContact = buildConfirmAddContact(state, newState); + var finishedAddContact = confirmAddContact === false; + + // Still doing first load. + if (!state.hasTriedToLoadMessages && !newState.hasTriedToLoadMessages) { + return null; + } + + // No users yet. + if (!oldOtherUser && !newOtherUser) { + return null; + } + + // We've loaded a new user and they require a contact request. + if (!oldOtherUser && nextRequiresContactRequest) { + return { + show: true, + hasMessages: hasMessages, + user: newOtherUser + }; + } + + // The logged in user has completed the confirm contact request dialogue + // but the other user still requires a contact request which means the logged + // in user either declined the confirmation or it failed. + if (finishedAddContact && nextRequiresContactRequest) { + return { + show: true, + hasMessages: hasMessages, + user: newOtherUser + }; + } + + // Everything is loaded. + if (state.hasTriedToLoadMessages && newState.hasTriedToLoadMessages) { + if (!prevRequiresContactRequest && nextRequiresContactRequest) { + return { + show: true, + hasMessages: hasMessages, + user: newOtherUser + }; + } + + if (prevRequiresContactRequest && !nextRequiresContactRequest) { + return { + show: false, + hasMessages: hasMessages + }; + } + } + + // First load just completed. + if (!state.hasTriedToLoadMessages && newState.hasTriedToLoadMessages) { + if (nextRequiresContactRequest) { + return { + show: true, + hasMessages: hasMessages, + user: newOtherUser + }; + } + } + + // Being reset. + if (state.hasTriedToLoadMessages && !newState.hasTriedToLoadMessages) { + if (prevRequiresContactRequest) { + return { + show: false, + hasMessages: hadMessages + }; + } + } + + return null; + }; + + /** + * Check if other users are required to be unblocked. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildRequireUnblock = function(state, newState) { + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + + if (!oldOtherUser && !newOtherUser) { + return null; + } else if (oldOtherUser && !newOtherUser) { + return oldOtherUser.isblocked ? false : null; + } else if (!oldOtherUser && newOtherUser) { + return newOtherUser.isblocked ? true : null; + } else if (!oldOtherUser.isblocked && newOtherUser.isblocked) { + return true; + } else if (oldOtherUser.isblocked && !newOtherUser.isblocked) { + return false; + } + + return null; + }; + + /** + * Check if other users can be messaged. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Bool|Null} + */ + var buildUnableToMessage = function(state, newState) { + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + + if (!oldOtherUser && !newOtherUser) { + return null; + } else if (oldOtherUser && !newOtherUser) { + return oldOtherUser.canmessage ? null : true; + } else if (!oldOtherUser && newOtherUser) { + return newOtherUser.canmessage ? null : true; + } else if (!oldOtherUser.canmessage && newOtherUser.canmessage) { + return false; + } else if (oldOtherUser.canmessage && !newOtherUser.canmessage) { + return true; + } + + return null; + }; + + /** + * Build patch for footer information for a private conversation. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object} containing footer state type. + */ + var buildFooterPatchTypePrivate = function(state, newState) { + var loadingFirstMessages = buildLoadingFirstMessages(state, newState); + var inEditMode = buildInEditMode(state, newState); + var requireAddContact = buildRequireAddContact(state, newState); + var requireUnblock = buildRequireUnblock(state, newState); + var unableToMessage = buildUnableToMessage(state, newState); + var showRequireAddContact = requireAddContact !== null ? requireAddContact.show && requireAddContact.hasMessages : null; + var otherUser = getOtherUserFromState(newState); + var generateReturnValue = function(checkValue, successReturn) { + if (checkValue) { + return successReturn; + } else if (checkValue !== null && !checkValue) { + if (!otherUser) { + return {type: 'content'}; + } else if (otherUser.isblocked) { + return {type: 'unblock'}; + } else if (newState.messages.length && requiresContactRequest(newState.loggedInUserId, otherUser)) { + return { + type: 'add-contact', + user: otherUser + }; + } else if (!otherUser.canmessage || (otherUser.requirescontact && !otherUser.iscontact)) { + return {type: 'unable-to-message'}; + } + } + + return null; + }; + + if ( + loadingFirstMessages === null && + inEditMode === null && + requireAddContact === null && + requireUnblock === null + ) { + return null; + } + + var checks = [ + [loadingFirstMessages, {type: 'placeholder'}], + [inEditMode, {type: 'edit-mode'}], + [unableToMessage, {type: 'unable-to-message'}], + [requireUnblock, {type: 'unblock'}], + [showRequireAddContact, {type: 'add-contact', user: otherUser}] + ]; + + for (var i = 0; i < checks.length; i++) { + var checkValue = checks[i][0]; + var successReturn = checks[i][1]; + var result = generateReturnValue(checkValue, successReturn); + + if (result !== null) { + return result; + } + } + + return { + type: 'content' + }; + }; + + /** + * Build patch for footer information for a public conversation. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object} containing footer state type. + */ + var buildFooterPatchTypePublic = function(state, newState) { + var loadingFirstMessages = buildLoadingFirstMessages(state, newState); + var inEditMode = buildInEditMode(state, newState); + + if (loadingFirstMessages === null && inEditMode === null) { + return null; + } + + if (loadingFirstMessages) { + return {type: 'placeholder'}; + } + + if (inEditMode) { + return {type: 'edit-mode'}; + } + + return { + type: 'content' + }; + }; + + /** + * Check if we're viewing a different conversation. If so then we need to + * reset the UI. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {bool|null} If a reset needs to occur + */ + var buildReset = function(state, newState) { + var oldType = state.type; + var newType = newState.type; + var oldConversationId = state.id; + var newConversationId = newState.id; + var oldMemberIds = Object.keys(state.members); + var newMemberIds = Object.keys(newState.members); + + oldMemberIds.sort(); + newMemberIds.sort(); + + var membersUnchanged = oldMemberIds.every(function(id, index) { + return id == newMemberIds[index]; + }); + + if (oldType != newType) { + // If we've changed conversation type then we need to reset. + return true; + } else if (oldConversationId && !newConversationId) { + // We previously had a conversation id but no longer do. This likely means + // the user is viewing the conversation with someone they've never spoken to + // before. + return true; + } else if (oldConversationId && newConversationId && oldConversationId != newConversationId) { + // If we had a conversation id and it's changed then we need to reset. + return true; + } else if (!oldConversationId && !newConversationId && !membersUnchanged) { + // If we never had a conversation id but the members of the conversation have + // changed then we need to reset. This can happen if the user goes from viewing + // a user they've never had a conversation with to viewing a different user that + // they've never had a conversation with. + return true; + } + + return null; + }; + + /** + * We should show the contact request sent message if the user just sent + * a contact request to the other user and there are no messages in the + * conversation. + * + * The messages should be hidden when there are messages in the conversation. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {string|false|null} + */ + var buildContactRequestSent = function(state, newState) { + var loggedInUserId = newState.loggedInUserId; + var oldOtherUser = getOtherUserFromState(state); + var newOtherUser = getOtherUserFromState(newState); + var oldSentRequests = !oldOtherUser ? [] : oldOtherUser.contactrequests.filter(function(request) { + return request.userid == loggedInUserId; + }); + var newSentRequests = !newOtherUser ? [] : newOtherUser.contactrequests.filter(function(request) { + return request.userid == loggedInUserId; + }); + var oldRequest = oldSentRequests.length > 0; + var newRequest = newSentRequests.length > 0; + var hadMessages = state.messages.length > 0; + var hasMessages = state.messages.length > 0; + + if (!oldRequest && newRequest && !newOtherUser.iscontact && !hasMessages) { + return newOtherUser.fullname; + } else if (oldOtherUser && !oldOtherUser.iscontact && newRequest && newOtherUser.iscontact) { + // Contact request accepted. + return false; + } else if (oldRequest && !newRequest) { + return false; + } else if (!hadMessages && hasMessages) { + return false; + } else { + return null; + } + }; + + /** + * Build the full patch comparing the current state and the new state. This patch is used by + * the conversation renderer to render the UI on any update. + * + * @param {Object} state The current state. + * @param {Object} newState The new state. + * @return {Object} Patch containing all information changed. + */ + var buildPatch = function(state, newState) { + var config = { + all: { + reset: buildReset, + conversation: buildConversationPatch, + scrollToMessage: buildScrollToMessagePatch, + loadingMembers: buildLoadingMembersPatch, + loadingFirstMessages: buildLoadingFirstMessages, + loadingMessages: buildLoadingMessages, + sendingMessage: buildSendingMessage, + confirmDeleteSelectedMessages: buildConfirmDeleteSelectedMessages, + inEditMode: buildInEditMode, + selectedMessages: buildSelectedMessages, + isFavourite: buildIsFavourite + } + }; + // These build functions are only applicable to private conversations. + config[Constants.CONVERSATION_TYPES.PRIVATE] = { + header: buildHeaderPatchTypePrivate, + footer: buildFooterPatchTypePrivate, + confirmBlockUser: buildConfirmBlockUser, + confirmUnblockUser: buildConfirmUnblockUser, + confirmAddContact: buildConfirmAddContact, + confirmRemoveContact: buildConfirmRemoveContact, + confirmContactRequest: buildConfirmContactRequest, + confirmDeleteConversation: buildConfirmDeleteConversation, + isBlocked: buildIsBlocked, + isContact: buildIsContact, + loadingConfirmAction: buildLoadingConfirmationAction, + requireAddContact: buildRequireAddContact, + contactRequestSent: buildContactRequestSent + }; + // These build functions are only applicable to public (group) conversations. + config[Constants.CONVERSATION_TYPES.PUBLIC] = { + header: buildHeaderPatchTypePublic, + footer: buildFooterPatchTypePublic, + }; + + var patchConfig = $.extend({}, config.all); + if (newState.type && newState.type in config) { + // Add the type specific builders to the patch config. + patchConfig = $.extend(patchConfig, config[newState.type]); + } + + return Object.keys(patchConfig).reduce(function(patch, key) { + var buildFunc = patchConfig[key]; + var value = buildFunc(state, newState); + + if (value !== null) { + patch[key] = value; + } + + return patch; + }, {}); + }; + + return { + buildPatch: buildPatch + }; +}); diff --git a/message/amd/src/message_drawer_view_conversation_renderer.js b/message/amd/src/message_drawer_view_conversation_renderer.js new file mode 100644 index 00000000000..a1b4b0fd20f --- /dev/null +++ b/message/amd/src/message_drawer_view_conversation_renderer.js @@ -0,0 +1,1518 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * This module updates the UI for the conversation page in the message + * drawer. + * + * The module will take a patch from the message_drawer_view_conversation_patcher + * module and update the UI to reflect the changes. + * + * This is the only module that ever modifies the UI of the conversation page. + * + * @module core_message/message_drawer_view_conversation_renderer + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/notification', + 'core/str', + 'core/templates', + 'core/user_date', + 'core_message/message_drawer_view_conversation_constants' +], +function( + $, + Notification, + Str, + Templates, + UserDate, + Constants +) { + var SELECTORS = Constants.SELECTORS; + var TEMPLATES = Constants.TEMPLATES; + var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES; + + /** + * Get the messages container element. + * + * @param {Object} body Conversation body container element. + * @return {Object} The messages container element. + */ + var getMessagesContainer = function(body) { + return body.find(SELECTORS.CONTENT_MESSAGES_CONTAINER); + }; + + /** + * Show the messages container element. + * + * @param {Object} body Conversation body container element. + */ + var showMessagesContainer = function(body) { + getMessagesContainer(body).removeClass('hidden'); + }; + + /** + * Hide the messages container element. + * + * @param {Object} body Conversation body container element. + */ + var hideMessagesContainer = function(body) { + getMessagesContainer(body).addClass('hidden'); + }; + + /** + * Get the contact request sent container element. + * + * @param {Object} body Conversation body container element. + * @return {Object} The messages container element. + */ + var getContactRequestSentContainer = function(body) { + return body.find(SELECTORS.CONTACT_REQUEST_SENT_MESSAGE_CONTAINER); + }; + + /** + * Hide the contact request sent container element. + * + * @param {Object} body Conversation body container element. + * @return {Object} The messages container element. + */ + var hideContactRequestSentContainer = function(body) { + return getContactRequestSentContainer(body).addClass('hidden'); + }; + + /** + * Get the footer container element. + * + * @param {Object} footer Conversation footer container element. + * @return {Object} The footer container element. + */ + var getFooterContentContainer = function(footer) { + return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_CONTAINER); + }; + + /** + * Show the footer container element. + * + * @param {Object} footer Conversation footer container element. + */ + var showFooterContent = function(footer) { + getFooterContentContainer(footer).removeClass('hidden'); + }; + + /** + * Hide the footer container element. + * + * @param {Object} footer Conversation footer container element. + */ + var hideFooterContent = function(footer) { + getFooterContentContainer(footer).addClass('hidden'); + }; + + /** + * Get the footer edit mode container element. + * + * @param {Object} footer Conversation footer container element. + * @return {Object} The footer container element. + */ + var getFooterEditModeContainer = function(footer) { + return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER); + }; + + /** + * Show the footer edit mode container element. + * + * @param {Object} footer Conversation footer container element. + */ + var showFooterEditMode = function(footer) { + getFooterEditModeContainer(footer).removeClass('hidden'); + }; + + /** + * Hide the footer edit mode container element. + * + * @param {Object} footer Conversation footer container element. + */ + var hideFooterEditMode = function(footer) { + getFooterEditModeContainer(footer).addClass('hidden'); + }; + + /** + * Get the footer placeholder. + * + * @param {Object} footer Conversation footer container element. + * @return {Object} The footer placeholder container element. + */ + var getFooterPlaceholderContainer = function(footer) { + return footer.find(SELECTORS.PLACEHOLDER_CONTAINER); + }; + + /** + * Show the footer placeholder + * + * @param {Object} footer Conversation footer container element. + */ + var showFooterPlaceholder = function(footer) { + getFooterPlaceholderContainer(footer).removeClass('hidden'); + }; + + /** + * Hide the footer placeholder + * + * @param {Object} footer Conversation footer container element. + */ + var hideFooterPlaceholder = function(footer) { + getFooterPlaceholderContainer(footer).addClass('hidden'); + }; + + /** + * Get the footer Require add as contact container element. + * + * @param {Object} footer Conversation footer container element. + * @return {Object} The footer Require add as contact container element. + */ + var getFooterRequireContactContainer = function(footer) { + return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER); + }; + + /** + * Show the footer add as contact dialogue. + * + * @param {Object} footer Conversation footer container element. + */ + var showFooterRequireContact = function(footer) { + getFooterRequireContactContainer(footer).removeClass('hidden'); + }; + + /** + * Hide the footer add as contact dialogue. + * + * @param {Object} footer Conversation footer container element. + */ + var hideFooterRequireContact = function(footer) { + getFooterRequireContactContainer(footer).addClass('hidden'); + }; + + /** + * Get the footer Required to unblock contact container element. + * + * @param {Object} footer Conversation footer container element. + * @return {Object} The footer Required to unblock contact container element. + */ + var getFooterRequireUnblockContainer = function(footer) { + return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER); + }; + + /** + * Show the footer Required to unblock contact container element. + * + * @param {Object} footer Conversation footer container element. + */ + var showFooterRequireUnblock = function(footer) { + getFooterRequireUnblockContainer(footer).removeClass('hidden'); + }; + + /** + * Hide the footer Required to unblock contact container element. + * + * @param {Object} footer Conversation footer container element. + */ + var hideFooterRequireUnblock = function(footer) { + getFooterRequireUnblockContainer(footer).addClass('hidden'); + }; + + /** + * Get the footer Unable to message contact container element. + * + * @param {Object} footer Conversation footer container element. + * @return {Object} The footer Unable to message contact container element. + */ + var getFooterUnableToMessageContainer = function(footer) { + return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER); + }; + + /** + * Show the footer Unable to message contact container element. + * + * @param {Object} footer Conversation footer container element. + */ + var showFooterUnableToMessage = function(footer) { + getFooterUnableToMessageContainer(footer).removeClass('hidden'); + }; + + /** + * Hide the footer Unable to message contact container element. + * + * @param {Object} footer Conversation footer container element. + */ + var hideFooterUnableToMessage = function(footer) { + getFooterUnableToMessageContainer(footer).addClass('hidden'); + }; + + /** + * Hide all header elements. + * + * @param {Object} header Conversation header container element. + */ + var hideAllHeaderElements = function(header) { + hideHeaderContent(header); + hideHeaderEditMode(header); + hideHeaderPlaceholder(header); + }; + + /** + * Hide all footer dialogues and messages. + * + * @param {Object} footer Conversation footer container element. + */ + var hideAllFooterElements = function(footer) { + hideFooterContent(footer); + hideFooterEditMode(footer); + hideFooterPlaceholder(footer); + hideFooterRequireContact(footer); + hideFooterRequireUnblock(footer); + hideFooterUnableToMessage(footer); + }; + + /** + * Get the content placeholder container element. + * + * @param {Object} body Conversation body container element. + * @return {Object} The body placeholder container element. + */ + var getContentPlaceholderContainer = function(body) { + return body.find(SELECTORS.CONTENT_PLACEHOLDER_CONTAINER); + }; + + /** + * Show the content placeholder. + * + * @param {Object} body Conversation body container element. + */ + var showContentPlaceholder = function(body) { + getContentPlaceholderContainer(body).removeClass('hidden'); + }; + + /** + * Hide the content placeholder. + * + * @param {Object} body Conversation body container element. + */ + var hideContentPlaceholder = function(body) { + getContentPlaceholderContainer(body).addClass('hidden'); + }; + + /** + * Get the header content container element. + * + * @param {Object} header Conversation header container element. + * @return {Object} The header content container element. + */ + var getHeaderContent = function(header) { + return header.find(SELECTORS.HEADER); + }; + + /** + * Show the header content. + * + * @param {Object} header Conversation header container element. + */ + var showHeaderContent = function(header) { + getHeaderContent(header).removeClass('hidden'); + }; + + /** + * Hide the header content. + * + * @param {Object} header Conversation header container element. + */ + var hideHeaderContent = function(header) { + getHeaderContent(header).addClass('hidden'); + }; + + /** + * Get the header edit mode container element. + * + * @param {Object} header Conversation header container element. + * @return {Object} The header content container element. + */ + var getHeaderEditMode = function(header) { + return header.find(SELECTORS.HEADER_EDIT_MODE); + }; + + /** + * Show the header edit mode container. + * + * @param {Object} header Conversation header container element. + */ + var showHeaderEditMode = function(header) { + getHeaderEditMode(header).removeClass('hidden'); + }; + + /** + * Hide the header edit mode container. + * + * @param {Object} header Conversation header container element. + */ + var hideHeaderEditMode = function(header) { + getHeaderEditMode(header).addClass('hidden'); + }; + + /** + * Get the header placeholder container element. + * + * @param {Object} header Conversation header container element. + * @return {Object} The header placeholder container element. + */ + var getHeaderPlaceholderContainer = function(header) { + return header.find(SELECTORS.HEADER_PLACEHOLDER_CONTAINER); + }; + + /** + * Show the header placeholder. + * + * @param {Object} header Conversation header container element. + */ + var showHeaderPlaceholder = function(header) { + getHeaderPlaceholderContainer(header).removeClass('hidden'); + }; + + /** + * Hide the header placeholder. + * + * @param {Object} header Conversation header container element. + */ + var hideHeaderPlaceholder = function(header) { + getHeaderPlaceholderContainer(header).addClass('hidden'); + }; + + /** + * Get the text input area element. + * + * @param {Object} footer Conversation footer container element. + * @return {Object} The footer placeholder container element. + */ + var getMessageTextArea = function(footer) { + return footer.find(SELECTORS.MESSAGE_TEXT_AREA); + }; + + /** + * Get a message element. + * + * @param {Object} body Conversation body container element. + * @param {Number} messageId the Message id. + * @return {Object} A message element from the conversation. + */ + var getMessageElement = function(body, messageId) { + var messagesContainer = getMessagesContainer(body); + return messagesContainer.find('[data-message-id="' + messageId + '"]'); + }; + + /** + * Get the day container element. The day container element holds a list of messages for that day. + * + * @param {Object} body Conversation body container element. + * @param {Number} dayTimeCreated Midnight timestamp for the day. + * @return {Object} jQuery object + */ + var getDayElement = function(body, dayTimeCreated) { + var messagesContainer = getMessagesContainer(body); + return messagesContainer.find('[data-day-id="' + dayTimeCreated + '"]'); + }; + + /** + * Get the more messages loading icon container element. + * + * @param {Object} body Conversation body container element. + * @return {Object} The more messages loading container element. + */ + var getMoreMessagesLoadingIconContainer = function(body) { + return body.find(SELECTORS.MORE_MESSAGES_LOADING_ICON_CONTAINER); + }; + + /** + * Show the more messages loading icon. + * + * @param {Object} body Conversation body container element. + */ + var showMoreMessagesLoadingIcon = function(body) { + getMoreMessagesLoadingIconContainer(body).removeClass('hidden'); + }; + + /** + * Hide the more messages loading icon. + * + * @param {Object} body Conversation body container element. + */ + var hideMoreMessagesLoadingIcon = function(body) { + getMoreMessagesLoadingIconContainer(body).addClass('hidden'); + }; + + /** + * Disable the message controls for sending a message. + * + * @param {Object} footer Conversation footer container element. + */ + var disableSendMessage = function(footer) { + footer.find(SELECTORS.SEND_MESSAGE_BUTTON).prop('disabled', true); + getMessageTextArea(footer).prop('disabled', true); + }; + + /** + * Enable the message controls for sending a message. + * + * @param {Object} footer Conversation footer container element. + */ + var enableSendMessage = function(footer) { + footer.find(SELECTORS.SEND_MESSAGE_BUTTON).prop('disabled', false); + getMessageTextArea(footer).prop('disabled', false); + }; + + /** + * Show the sending message loading icon and disable sending more. + * + * @param {Object} footer Conversation footer container element. + */ + var startSendMessageLoading = function(footer) { + disableSendMessage(footer); + footer.find(SELECTORS.SEND_MESSAGE_ICON_CONTAINER).addClass('hidden'); + footer.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden'); + }; + + /** + * Hide the sending message loading icon and allow sending new messages. + * + * @param {Object} footer Conversation footer container element. + */ + var stopSendMessageLoading = function(footer) { + enableSendMessage(footer); + footer.find(SELECTORS.SEND_MESSAGE_ICON_CONTAINER).removeClass('hidden'); + footer.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden'); + }; + + /** + * Clear out message text input and focus the input element. + * + * @param {Object} footer Conversation footer container element. + */ + var hasSentMessage = function(footer) { + var textArea = getMessageTextArea(footer); + textArea.val(''); + textArea.focus(); + }; + + /** + * Get the confirm dialogue container element. + * + * @param {Object} root The container element to search. + * @return {Object} The confirm dialogue container element. + */ + var getConfirmDialogueContainer = function(root) { + return root.find(SELECTORS.CONFIRM_DIALOGUE_CONTAINER); + }; + + /** + * Show the confirm dialogue container element. + * + * @param {Object} root The container element containing a dialogue. + */ + var showConfirmDialogueContainer = function(root) { + var container = getConfirmDialogueContainer(root); + var siblings = container.siblings(':not(.hidden)'); + siblings.attr('aria-hidden', true); + siblings.attr('tabindex', -1); + siblings.attr('data-confirm-dialogue-hidden', true); + + container.removeClass('hidden'); + }; + + /** + * Hide the confirm dialogue container element. + * + * @param {Object} root The container element containing a dialogue. + */ + var hideConfirmDialogueContainer = function(root) { + var container = getConfirmDialogueContainer(root); + var siblings = container.siblings('[data-confirm-dialogue-hidden="true"]'); + siblings.removeAttr('aria-hidden'); + siblings.removeAttr('tabindex'); + siblings.removeAttr('data-confirm-dialogue-hidden'); + + container.addClass('hidden'); + }; + + /** + * Set the number of selected messages. + * + * @param {Object} header The header container element. + * @param {Number} value The new number to display. + */ + var setMessagesSelectedCount = function(header, value) { + getHeaderEditMode(header).find(SELECTORS.MESSAGES_SELECTED_COUNT).text(value); + }; + + /** + * Format message for the mustache template, transform camelCase properties to lowercase properties. + * + * @param {Array} messages Array of message objects. + * @param {Object} datesCache Cache timestamps and their formatted date string. + * @return {Array} Messages formated for mustache template. + */ + var formatMessagesForTemplate = function(messages, datesCache) { + return messages.map(function(message) { + return { + id: message.id, + isread: message.isRead, + fromloggedinuser: message.fromLoggedInUser, + userfrom: message.userFrom, + text: message.text, + formattedtime: datesCache[message.timeCreated] + }; + }); + }; + + /** + * Create rendering promises for each day containing messages. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Array} days Array of days containing messages. + * @param {Object} datesCache Cache timestamps and their formatted date string. + * @return {Promise} Days rendering promises. + */ + var renderAddDays = function(header, body, footer, days, datesCache) { + var messagesContainer = getMessagesContainer(body); + var daysRenderPromises = days.map(function(data) { + return Templates.render(TEMPLATES.DAY, { + timestamp: data.value.timestamp, + messages: formatMessagesForTemplate(data.value.messages, datesCache) + }); + }); + + return $.when.apply($, daysRenderPromises).then(function() { + // Wait until all of the rendering is done for each of the days + // to ensure they are added to the page in the correct order. + days.forEach(function(data, index) { + daysRenderPromises[index] + .then(function(html) { + if (data.before) { + var element = getDayElement(body, data.before.timestamp); + return $(html).insertBefore(element); + } else { + return messagesContainer.append(html); + } + }) + .catch(function() { + // Fail silently. + }); + }); + + return; + }); + }; + + /** + * Add (more) messages to day containers. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Array} messages List of messages. + * @param {Object} datesCache Cache timestamps and their formatted date string. + * @return {Promise} Messages rendering promises. + */ + var renderAddMessages = function(header, body, footer, messages, datesCache) { + var messagesRenderPromises = messages.map(function(data) { + var formattedMessages = formatMessagesForTemplate([data.value], datesCache); + return Templates.render(TEMPLATES.MESSAGE, formattedMessages[0]); + }); + + return $.when.apply($, messagesRenderPromises).then(function() { + // Wait until all of the rendering is done for each of the messages + // to ensure they are added to the page in the correct order. + messages.forEach(function(data, index) { + messagesRenderPromises[index] + .then(function(html) { + if (data.before) { + var element = getMessageElement(body, data.before.id); + return $(html).insertBefore(element); + } else { + var dayContainer = getDayElement(body, data.day.timestamp); + var dayMessagesContainer = dayContainer.find(SELECTORS.DAY_MESSAGES_CONTAINER); + return dayMessagesContainer.append(html); + } + }) + .catch(function() { + // Silently ignore failed renders. + }); + }); + + return; + }); + }; + + /** + * Remove days from conversation. + * + * @param {Object} body The body container element. + * @param {Array} days Array of days to be removed. + */ + var renderRemoveDays = function(body, days) { + days.forEach(function(data) { + getDayElement(body, data.timestamp).remove(); + }); + }; + + /** + * Remove messages from conversation. + * + * @param {Object} body The body container element. + * @param {Array} messages Array of messages to be removed. + */ + var renderRemoveMessages = function(body, messages) { + messages.forEach(function(data) { + getMessageElement(body, data.id).remove(); + }); + }; + + /** + * Render the full conversation base on input from the statemanager. + * + * This will pre-load all of the formatted timestamps for each message that + * needs to render to reduce the number of networks requests. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} data The conversation diff. + * @return {Object} jQuery promise. + */ + var renderConversation = function(header, body, footer, data) { + var renderingPromises = []; + var hasAddDays = data.days.add.length > 0; + var hasAddMessages = data.messages.add.length > 0; + var timestampsToFormat = []; + var datesCachePromise = $.Deferred().resolve({}).promise(); + + if (hasAddDays) { + // Search for all of the timeCreated values in all of the messages in all of + // the days that we need to render. + timestampsToFormat = timestampsToFormat.concat(data.days.add.reduce(function(carry, day) { + return carry.concat(day.value.messages.map(function(message) { + return message.timeCreated; + })); + }, [])); + } + + if (hasAddMessages) { + // Search for all of the timeCreated values in all of the messages that we + // need to render. + timestampsToFormat = timestampsToFormat.concat(data.messages.add.map(function(message) { + return message.value.timeCreated; + })); + } + + if (timestampsToFormat.length) { + // If we have timestamps then pre-load the formatted version of each of them + // in a single request to the server. This saves the templates doing multiple + // individual requests. + datesCachePromise = Str.get_string('strftimetime24', 'core_langconfig') + .then(function(format) { + var requests = timestampsToFormat.map(function(timestamp) { + return { + timestamp: timestamp, + format: format + }; + }); + + return UserDate.get(requests); + }) + .then(function(formattedTimes) { + return timestampsToFormat.reduce(function(carry, timestamp, index) { + carry[timestamp] = formattedTimes[index]; + return carry; + }, {}); + }); + } + + if (hasAddDays) { + renderingPromises.push(datesCachePromise.then(function(datesCache) { + return renderAddDays(header, body, footer, data.days.add, datesCache); + })); + } + + if (hasAddMessages) { + renderingPromises.push(datesCachePromise.then(function(datesCache) { + return renderAddMessages(header, body, footer, data.messages.add, datesCache); + })); + } + + if (data.days.remove.length > 0) { + renderRemoveDays(body, data.days.remove); + } + + if (data.messages.remove.length > 0) { + renderRemoveMessages(body, data.messages.remove); + } + + return $.when.apply($, renderingPromises); + }; + + /** + * Render the conversation header. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} data Data for header. + * @return {Object} jQuery promise + */ + var renderHeader = function(header, body, footer, data) { + var headerContainer = getHeaderContent(header); + var template = TEMPLATES.HEADER_PUBLIC; + + if (data.type == CONVERSATION_TYPES.PRIVATE) { + template = data.showControls ? TEMPLATES.HEADER_PRIVATE : TEMPLATES.HEADER_PRIVATE_NO_CONTROLS; + } + + return Templates.render(template, data.context) + .then(function(html, js) { + Templates.replaceNodeContents(headerContainer, html, js); + return; + }); + }; + + /** + * Render the conversation footer. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} data Data for footer. + * @return {Object} jQuery promise. + */ + var renderFooter = function(header, body, footer, data) { + hideAllFooterElements(footer); + + switch (data.type) { + case 'placeholder': + return showFooterPlaceholder(footer); + case 'add-contact': + return Str.get_strings([ + { + key: 'requirecontacttomessage', + component: 'core_message', + param: data.user.fullname + }, + { + key: 'isnotinyourcontacts', + component: 'core_message', + param: data.user.fullname + } + ]) + .then(function(strings) { + var title = strings[1]; + var text = strings[0]; + var footerContainer = getFooterRequireContactContainer(footer); + footerContainer.find(SELECTORS.TITLE).text(title); + footerContainer.find(SELECTORS.TEXT).text(text); + showFooterRequireContact(footer); + return strings; + }); + case 'edit-mode': + return showFooterEditMode(footer); + case 'content': + return showFooterContent(footer); + case 'unblock': + return showFooterRequireUnblock(footer); + case 'unable-to-message': + return showFooterUnableToMessage(footer); + } + + return true; + }; + + /** + * Scroll to a message in the conversation. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Number} messageId Message id. + */ + var renderScrollToMessage = function(header, body, footer, messageId) { + var messagesContainer = getMessagesContainer(body); + var messageElement = getMessageElement(body, messageId); + var position = messageElement.position(); + // Scroll the message container down to the top of the message element. + if (position) { + var scrollTop = messagesContainer.scrollTop() + position.top; + messagesContainer.scrollTop(scrollTop); + } + }; + + /** + * Hide or show the conversation header. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} isLoadingMembers Members loading. + */ + var renderLoadingMembers = function(header, body, footer, isLoadingMembers) { + if (isLoadingMembers) { + hideHeaderContent(header); + showHeaderPlaceholder(header); + } else { + showHeaderContent(header); + hideHeaderPlaceholder(header); + } + }; + + /** + * Hide or show loading conversation messages. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} isLoadingFirstMessages Messages loading. + */ + var renderLoadingFirstMessages = function(header, body, footer, isLoadingFirstMessages) { + if (isLoadingFirstMessages) { + hideMessagesContainer(body); + showContentPlaceholder(body); + } else { + showMessagesContainer(body); + hideContentPlaceholder(body); + } + }; + + /** + * Hide or show loading more messages. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} isLoading Messages loading. + */ + var renderLoadingMessages = function(header, body, footer, isLoading) { + if (isLoading) { + showMoreMessagesLoadingIcon(body); + } else { + hideMoreMessagesLoadingIcon(body); + } + }; + + /** + * Activate or deactivate send message controls. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} isSending Message sending. + */ + var renderSendingMessage = function(header, body, footer, isSending) { + if (isSending) { + startSendMessageLoading(footer); + } else { + stopSendMessageLoading(footer); + hasSentMessage(footer); + } + }; + + /** + * Show a confirmation dialogue + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {String} buttonSelectors Selectors for the buttons to show. + * @param {String} bodyText Text to show in dialogue. + * @param {String} headerText Text to show in dialogue header. + * @param {Bool} canCancel Can this dialogue be cancelled. + * @param {Bool} skipHeader Skip blanking out the header + */ + var showConfirmDialogue = function( + header, + body, + footer, + buttonSelectors, + bodyText, + headerText, + canCancel, + skipHeader + ) { + var dialogue = getConfirmDialogueContainer(body); + var buttons = buttonSelectors.map(function(selector) { + return dialogue.find(selector); + }); + var cancelButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_CANCEL_BUTTON); + var text = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_TEXT); + var dialogueHeader = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_HEADER); + + dialogue.find('button').addClass('hidden'); + + if (canCancel) { + cancelButton.removeClass('hidden'); + } else { + cancelButton.addClass('hidden'); + } + + if (headerText) { + dialogueHeader.removeClass('hidden'); + dialogueHeader.text(headerText); + } else { + dialogueHeader.addClass('hidden'); + dialogueHeader.text(''); + } + + buttons.forEach(function(button) { + button.removeClass('hidden'); + }); + text.text(bodyText); + showConfirmDialogueContainer(footer); + showConfirmDialogueContainer(body); + + if (!skipHeader) { + showConfirmDialogueContainer(header); + } + + dialogue.find(SELECTORS.CAN_RECEIVE_FOCUS).first().focus(); + }; + + /** + * Hide the dialogue + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @return {Bool} always true. + */ + var hideConfirmDialogue = function(header, body, footer) { + var dialogue = getConfirmDialogueContainer(body); + var cancelButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_CANCEL_BUTTON); + var text = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_TEXT); + var dialogueHeader = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_HEADER); + + hideConfirmDialogueContainer(body); + hideConfirmDialogueContainer(footer); + hideConfirmDialogueContainer(header); + dialogue.find('button').addClass('hidden'); + cancelButton.removeClass('hidden'); + text.text(''); + dialogueHeader.addClass('hidden'); + dialogueHeader.text(''); + + header.find(SELECTORS.CAN_RECEIVE_FOCUS).first().focus(); + return true; + }; + + /** + * Render the confirm block user dialogue. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} user User to block. + * @return {Object} jQuery promise + */ + var renderConfirmBlockUser = function(header, body, footer, user) { + if (user) { + return Str.get_string('blockuserconfirm', 'core_message', user.fullname) + .then(function(string) { + return showConfirmDialogue(header, body, footer, [SELECTORS.ACTION_CONFIRM_BLOCK], string, '', true, false); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Render the confirm unblock user dialogue. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} user User to unblock. + * @return {Object} jQuery promise + */ + var renderConfirmUnblockUser = function(header, body, footer, user) { + if (user) { + return Str.get_string('unblockuserconfirm', 'core_message', user.fullname) + .then(function(string) { + return showConfirmDialogue(header, body, footer, [SELECTORS.ACTION_CONFIRM_UNBLOCK], string, '', true, false); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Render the add user as contact dialogue. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} user User to add as contact. + * @return {Object} jQuery promise + */ + var renderConfirmAddContact = function(header, body, footer, user) { + if (user) { + return Str.get_string('addcontactconfirm', 'core_message', user.fullname) + .then(function(string) { + return showConfirmDialogue( + header, + body, + footer, + [SELECTORS.ACTION_CONFIRM_ADD_CONTACT], + string, + '', + true, + false + ); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Render the remove user from contacts dialogue. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} user User to remove from contacts. + * @return {Object} jQuery promise + */ + var renderConfirmRemoveContact = function(header, body, footer, user) { + if (user) { + return Str.get_string('removecontactconfirm', 'core_message', user.fullname) + .then(function(string) { + return showConfirmDialogue( + header, + body, + footer, + [SELECTORS.ACTION_CONFIRM_REMOVE_CONTACT], + string, + '', + true, + false + ); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Render the delete selected messages dialogue. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} show If the dialogue should show. + * @return {Object} jQuery promise + */ + var renderConfirmDeleteSelectedMessages = function(header, body, footer, show) { + if (show) { + return Str.get_string('deleteselectedmessagesconfirm', 'core_message') + .then(function(string) { + return showConfirmDialogue( + header, + body, + footer, + [SELECTORS.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES], + string, + '', + true, + false + ); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Render the confirm delete conversation dialogue. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} show If the dialogue should show + * @return {Object} jQuery promise + */ + var renderConfirmDeleteConversation = function(header, body, footer, show) { + if (show) { + return Str.get_string('deleteallconfirm', 'core_message') + .then(function(string) { + return showConfirmDialogue( + header, + body, + footer, + [SELECTORS.ACTION_CONFIRM_DELETE_CONVERSATION], + string, + '', + true, + false + ); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Render the confirm delete conversation dialogue. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} user The other user object. + * @return {Object} jQuery promise + */ + var renderConfirmContactRequest = function(header, body, footer, user) { + if (user) { + return Str.get_string('userwouldliketocontactyou', 'core_message', user.fullname) + .then(function(string) { + var buttonSelectors = [ + SELECTORS.ACTION_ACCEPT_CONTACT_REQUEST, + SELECTORS.ACTION_DECLINE_CONTACT_REQUEST + ]; + return showConfirmDialogue(header, body, footer, buttonSelectors, string, '', false, true); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Show or hide the block / unblock option in the header dropdown menu. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} isBlocked is user blocked. + */ + var renderIsBlocked = function(header, body, footer, isBlocked) { + if (isBlocked) { + header.find(SELECTORS.ACTION_REQUEST_BLOCK).addClass('hidden'); + header.find(SELECTORS.ACTION_REQUEST_UNBLOCK).removeClass('hidden'); + } else { + header.find(SELECTORS.ACTION_REQUEST_BLOCK).removeClass('hidden'); + header.find(SELECTORS.ACTION_REQUEST_UNBLOCK).addClass('hidden'); + } + }; + + /** + * Show or hide the favourite / unfavourite option in the header dropdown menu + * and the favourite star in the header title. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} isFavourite is this conversation a favourite. + */ + var renderIsFavourite = function(header, body, footer, isFavourite) { + if (isFavourite) { + header.find(SELECTORS.FAVOURITE_ICON_CONTAINER).removeClass('hidden'); + header.find(SELECTORS.ACTION_CONFIRM_FAVOURITE).addClass('hidden'); + header.find(SELECTORS.ACTION_CONFIRM_UNFAVOURITE).removeClass('hidden'); + } else { + header.find(SELECTORS.FAVOURITE_ICON_CONTAINER).addClass('hidden'); + header.find(SELECTORS.ACTION_CONFIRM_FAVOURITE).removeClass('hidden'); + header.find(SELECTORS.ACTION_CONFIRM_UNFAVOURITE).addClass('hidden'); + } + }; + + /** + * Show or hide the add / remove user as contact option in the header dropdown menu. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} state the contact state. + */ + var renderIsContact = function(header, body, footer, state) { + var addContact = header.find(SELECTORS.ACTION_REQUEST_ADD_CONTACT); + var removeContact = header.find(SELECTORS.ACTION_REQUEST_REMOVE_CONTACT); + + switch (state) { + case 'pending-contact': + addContact.addClass('hidden'); + removeContact.addClass('hidden'); + break; + case 'contact': + addContact.addClass('hidden'); + removeContact.removeClass('hidden'); + break; + case 'non-contact': + addContact.removeClass('hidden'); + removeContact.addClass('hidden'); + break; + } + }; + + /** + * Show or hide confirm action from confirm dialogue is loading. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} isLoading confirm action is loading. + */ + var renderLoadingConfirmAction = function(header, body, footer, isLoading) { + var dialogue = getConfirmDialogueContainer(body); + var buttons = dialogue.find('button'); + var buttonText = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_BUTTON_TEXT); + var loadingIcon = dialogue.find(SELECTORS.LOADING_ICON_CONTAINER); + + if (isLoading) { + buttons.prop('disabled', true); + buttonText.addClass('hidden'); + loadingIcon.removeClass('hidden'); + } else { + buttons.prop('disabled', false); + buttonText.removeClass('hidden'); + loadingIcon.addClass('hidden'); + } + }; + + /** + * Show or hide the header and footer content for edit mode. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Bool} inEditMode In edit mode or not. + */ + var renderInEditMode = function(header, body, footer, inEditMode) { + var messages = null; + + if (inEditMode) { + messages = body.find(SELECTORS.MESSAGE_NOT_SELECTED); + messages.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).removeClass('hidden'); + hideHeaderContent(header); + showHeaderEditMode(header); + } else { + messages = getMessagesContainer(body); + messages.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).addClass('hidden'); + messages.find(SELECTORS.MESSAGE_SELECTED_ICON).addClass('hidden'); + showHeaderContent(header); + hideHeaderEditMode(header); + } + }; + + /** + * Select or unselect messages. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} data The messages to select or unselect. + */ + var renderSelectedMessages = function(header, body, footer, data) { + var hasSelectedMessages = data.count > 0; + + if (data.add.length) { + data.add.forEach(function(messageId) { + var message = getMessageElement(body, messageId); + message.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).addClass('hidden'); + message.find(SELECTORS.MESSAGE_SELECTED_ICON).removeClass('hidden'); + message.attr('aria-checked', true); + }); + } + + if (data.remove.length) { + data.remove.forEach(function(messageId) { + var message = getMessageElement(body, messageId); + + if (hasSelectedMessages) { + message.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).removeClass('hidden'); + } + + message.find(SELECTORS.MESSAGE_SELECTED_ICON).addClass('hidden'); + message.attr('aria-checked', false); + }); + } + + setMessagesSelectedCount(header, data.count); + }; + + /** + * Show or hide the require add contact panel. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} data Whether the user has to be added a a contact. + * @return {Object} jQuery promise + */ + var renderRequireAddContact = function(header, body, footer, data) { + if (data.show && !data.hasMessages) { + return Str.get_strings([ + { + key: 'requirecontacttomessage', + component: 'core_message', + param: data.user.fullname + }, + { + key: 'isnotinyourcontacts', + component: 'core_message', + param: data.user.fullname + } + ]) + .then(function(strings) { + var title = strings[1]; + var text = strings[0]; + return showConfirmDialogue( + header, + body, + footer, + [SELECTORS.ACTION_REQUEST_ADD_CONTACT], + text, + title, + false, + true + ); + }); + } else { + return hideConfirmDialogue(header, body, footer); + } + }; + + /** + * Show or hide the require add contact panel. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @param {Object} userFullName Full name of the other user. + * @return {Object|true} jQuery promise + */ + var renderContactRequestSent = function(header, body, footer, userFullName) { + var container = getContactRequestSentContainer(body); + if (userFullName) { + return Str.get_string('yourcontactrequestpending', 'core_message', userFullName) + .then(function(string) { + container.find(SELECTORS.TEXT).text(string); + container.removeClass('hidden'); + return string; + }); + } else { + container.addClass('hidden'); + return true; + } + }; + + /** + * Reset the UI to the initial state. + * + * @param {Object} header The header container element. + * @param {Object} body The body container element. + * @param {Object} footer The footer container element. + * @return {Bool} + */ + var renderReset = function(header, body, footer) { + hideConfirmDialogue(header, body, footer); + hideContactRequestSentContainer(body); + hideAllHeaderElements(header); + showHeaderPlaceholder(header); + hideAllFooterElements(footer); + showFooterPlaceholder(footer); + return true; + }; + + var render = function(header, body, footer, patch) { + var configs = [ + { + // Resetting the UI needs to come first, if it's required. + reset: renderReset + }, + { + // Any async rendering (stuff that requires templates, strings etc) should + // go in here. + conversation: renderConversation, + header: renderHeader, + footer: renderFooter, + confirmBlockUser: renderConfirmBlockUser, + confirmUnblockUser: renderConfirmUnblockUser, + confirmAddContact: renderConfirmAddContact, + confirmRemoveContact: renderConfirmRemoveContact, + confirmDeleteSelectedMessages: renderConfirmDeleteSelectedMessages, + confirmDeleteConversation: renderConfirmDeleteConversation, + confirmContactRequest: renderConfirmContactRequest, + requireAddContact: renderRequireAddContact, + contactRequestSent: renderContactRequestSent + }, + { + loadingMembers: renderLoadingMembers, + loadingFirstMessages: renderLoadingFirstMessages, + loadingMessages: renderLoadingMessages, + sendingMessage: renderSendingMessage, + isBlocked: renderIsBlocked, + isContact: renderIsContact, + isFavourite: renderIsFavourite, + loadingConfirmAction: renderLoadingConfirmAction, + inEditMode: renderInEditMode + }, + { + // Scrolling should be last to make sure everything + // on the page is visible. + scrollToMessage: renderScrollToMessage, + selectedMessages: renderSelectedMessages + } + ]; + // Helper function to process each of the configs above. + var processConfig = function(config) { + var results = []; + + for (var key in patch) { + if (config.hasOwnProperty(key)) { + var renderFunc = config[key]; + var patchValue = patch[key]; + results.push(renderFunc(header, body, footer, patchValue)); + } + } + + return results; + }; + + // The first config is special because it resets the UI. + var renderingPromises = processConfig(configs[0]); + // The second config is special because it contains async rendering. + renderingPromises = renderingPromises.concat(processConfig(configs[1])); + + // Wait for the async rendering to complete before processing the + // rest of the configs, in order. + return $.when.apply($, renderingPromises) + .then(function() { + for (var i = 2; i < configs.length; i++) { + processConfig(configs[i]); + } + + return; + }) + .catch(Notification.exception); + }; + + return { + render: render, + }; +}); diff --git a/message/amd/src/message_drawer_view_conversation_state_manager.js b/message/amd/src/message_drawer_view_conversation_state_manager.js new file mode 100644 index 00000000000..5c429e56f24 --- /dev/null +++ b/message/amd/src/message_drawer_view_conversation_state_manager.js @@ -0,0 +1,651 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * This module operates on the view states from the message_drawer_view_conversation module. + * It exposes functions that can be used to generate new version of the state. + * + * Important notes for this module: + * 1.) The existing state is always immutable. It should never be modified. + * 2.) All functions that operate on the state should always clone the state and + * modify the cloned state before returning it. + * + * It's important that the states remain immutable because they are diff'd in + * the message_drawer_view_conversation_patcher module in order to work out what + * has changed. + * + * @module core_message/message_drawer_view_conversation_state_manager + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery'], function($) { + + /** + * Clone a state, a state is a collection of information about the variables required to build + * the conversation user interface. + * + * @param {Object} state State to clone + * @return {Object} newstate A copy of the state to clone. + */ + var cloneState = function(state) { + var newState = $.extend({}, state); + newState.messages = state.messages.map(function(message) { + return $.extend({}, message); + }); + newState.members = Object.keys(state.members).reduce(function(carry, id) { + carry[id] = $.extend({}, state.members[id]); + carry[id].contactrequests = state.members[id].contactrequests.map(function(request) { + return $.extend({}, request); + }); + return carry; + }, {}); + return newState; + }; + + /** + * Format messages to be used in a state. + * + * @param {Array} messages The messages to format. + * @param {Number} loggedInUserId The logged in user id. + * @param {Array} members The converstation members. + * @return {Array} Formatted messages. + */ + var formatMessages = function(messages, loggedInUserId, members) { + return messages.map(function(message) { + var fromLoggedInUser = message.useridfrom == loggedInUserId; + return { + id: parseInt(message.id, 10), + isRead: message.isread, + fromLoggedInUser: fromLoggedInUser, + userFrom: members[message.useridfrom], + text: message.text, + timeCreated: parseInt(message.timecreated, 10) + }; + }); + }; + + /** + * Create an initial (blank) state. + * + * @param {Number} midnight Midnight time. + * @param {Number} loggedInUserId The logged in user id. + * @param {Number} id The conversation id. + * @return {Object} Initial state. + */ + var buildInitialState = function(midnight, loggedInUserId, id) { + return { + midnight: midnight, + loggedInUserId: loggedInUserId, + id: id, + name: null, + subname: null, + type: null, + totalMemberCount: null, + imageUrl: null, + isFavourite: null, + members: {}, + messages: [], + hasTriedToLoadMessages: false, + loadingMessages: true, + sendingMessage: false, + loadingMembers: true, + loadingConfirmAction: false, + pendingBlockUserIds: [], + pendingUnblockUserIds: [], + pendingRemoveContactIds: [], + pendingAddContactIds: [], + pendingDeleteMessageIds: [], + pendingDeleteConversation: false, + selectedMessageIds: [] + }; + }; + + /** + * Add messages to a state and sort them by timecreated. + * + * @param {Object} state Current state. + * @param {Array} messages Messages to add to state. + * @return {Object} state New state with added messages. + */ + var addMessages = function(state, messages) { + var newState = cloneState(state); + var formattedMessages = formatMessages(messages, state.loggedInUserId, state.members); + var allMessages = state.messages.concat(formattedMessages); + // Sort the messages. Oldest to newest. + allMessages.sort(function(a, b) { + if (a.timeCreated < b.timeCreated) { + return -1; + } else if (a.timeCreated > b.timeCreated) { + return 1; + } else { + return 0; + } + }); + + // Filter out any duplicate messages. + newState.messages = allMessages.filter(function(message, index, sortedMessages) { + return !index || message.id !== sortedMessages[index - 1].id; + }); + + return newState; + }; + + /** + * Remove messages from state. + * + * @param {Object} state Current state. + * @param {Array} messages Messages to remove from state. + * @return {Object} state New state with removed messages. + */ + var removeMessages = function(state, messages) { + var newState = cloneState(state); + var removeMessageIds = messages.map(function(message) { + return message.id; + }); + newState.messages = newState.messages.filter(function(message) { + return removeMessageIds.indexOf(message.id) < 0; + }); + + return newState; + }; + + /** + * Remove messages from state by message id. + * + * @param {Object} state Current state. + * @param {Array} messagesIds Message ids to remove from state. + * @return {Object} state New state with removed messages. + */ + var removeMessagesById = function(state, messagesIds) { + var newState = cloneState(state); + newState.messages = newState.messages.filter(function(message) { + return messagesIds.indexOf(message.id) < 0; + }); + + return newState; + }; + + /** + * Add conversation member to state. + * + * @param {Object} state Current state. + * @param {Array} members Conversation members to be added to state. + * @return {Object} New state with added members. + */ + var addMembers = function(state, members) { + var newState = cloneState(state); + members.forEach(function(member) { + newState.members[member.id] = member; + }); + return newState; + }; + + /** + * Remove members from state. + * + * @param {Object} state Current state. + * @param {Array} members Members to be removed from state. + * @return {Object} New state with removed members. + */ + var removeMembers = function(state, members) { + var newState = cloneState(state); + members.forEach(function(member) { + delete newState.members[member.id]; + }); + return newState; + }; + + /** + * Set the state loading messages attribute. + * + * @param {Object} state Current state. + * @param {Bool} value New loading messages value. + * @return {Object} New state with loading messages attribute. + */ + var setLoadingMessages = function(state, value) { + var newState = cloneState(state); + newState.loadingMessages = value; + if (state.loadingMessages && !value) { + // If we're going from loading to not loading then + // it means we've tried to load. + newState.hasTriedToLoadMessages = true; + } + return newState; + }; + + /** + * Set the state sending message attribute. + * + * @param {Object} state Current state. + * @param {Bool} value New sending message value. + * @return {Object} New state with sending message attribute. + */ + var setSendingMessage = function(state, value) { + var newState = cloneState(state); + newState.sendingMessage = value; + return newState; + }; + + /** + * Set the state loading members attribute. + * + * @param {Object} state Current state. + * @param {Bool} value New loading members value. + * @return {Object} New state with loading members attribute. + */ + var setLoadingMembers = function(state, value) { + var newState = cloneState(state); + newState.loadingMembers = value; + return newState; + }; + + /** + * Set the conversation id. + * + * @param {Object} state Current state. + * @param {String} value The ID. + * @return {Object} New state. + */ + var setId = function(state, value) { + var newState = cloneState(state); + newState.id = value; + return newState; + }; + + /** + * Set the state name attribute. + * + * @param {Object} state Current state. + * @param {String} value New name value. + * @return {Object} New state with name attribute. + */ + var setName = function(state, value) { + var newState = cloneState(state); + newState.name = value; + return newState; + }; + + /** + * Set the state subname attribute. + * + * @param {Object} state Current state. + * @param {String} value New subname value. + * @return {Object} New state. + */ + var setSubname = function(state, value) { + var newState = cloneState(state); + newState.subname = value; + return newState; + }; + + /** + * Set the conversation type. + * + * @param {Object} state Current state. + * @param {Int} type Conversation type. + * @return {Object} New state. + */ + var setType = function(state, type) { + var newState = cloneState(state); + newState.type = type; + return newState; + }; + + /** + * Set whether the conversation is a favourite conversation. + * + * @param {Object} state Current state. + * @param {Bool} isFavourite If it's a favourite. + * @return {Object} New state. + */ + var setIsFavourite = function(state, isFavourite) { + var newState = cloneState(state); + newState.isFavourite = isFavourite; + return newState; + }; + + /** + * Set the total member count. + * + * @param {Object} state Current state. + * @param {String} count The count. + * @return {Object} New state. + */ + var setTotalMemberCount = function(state, count) { + var newState = cloneState(state); + newState.totalMemberCount = count; + return newState; + }; + + /** + * Set the conversation image url. + * + * @param {Object} state Current state. + * @param {String} url The url to the image. + * @return {Object} New state. + */ + var setImageUrl = function(state, url) { + var newState = cloneState(state); + newState.imageUrl = url; + return newState; + }; + + /** + * Set the state loading confirm action attribute. + * + * @param {Object} state Current state. + * @param {Bool} value New loading confirm action value. + * @return {Object} New state with loading confirm action attribute. + */ + var setLoadingConfirmAction = function(state, value) { + var newState = cloneState(state); + newState.loadingConfirmAction = value; + return newState; + }; + + /** + * Set the state pending delete conversation attribute. + * + * @param {Object} state Current state. + * @param {Bool} value New pending delete conversation value. + * @return {Object} New state with pending delete conversation attribute. + */ + var setPendingDeleteConversation = function(state, value) { + var newState = cloneState(state); + newState.pendingDeleteConversation = value; + return newState; + }; + + /** + * Set the state pending block userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to block. + * @return {Object} New state with array of pending block userids. + */ + var addPendingBlockUsersById = function(state, userIds) { + var newState = cloneState(state); + userIds.forEach(function(id) { + newState.pendingBlockUserIds.push(id); + }); + return newState; + }; + + /** + * Set the state pending remove userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to remove. + * @return {Object} New state with array of pending remove userids. + */ + var addPendingRemoveContactsById = function(state, userIds) { + var newState = cloneState(state); + userIds.forEach(function(id) { + newState.pendingRemoveContactIds.push(id); + }); + return newState; + }; + + /** + * Set the state pending unblock userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to unblock. + * @return {Object} New state with array of pending unblock userids. + */ + var addPendingUnblockUsersById = function(state, userIds) { + var newState = cloneState(state); + userIds.forEach(function(id) { + newState.pendingUnblockUserIds.push(id); + }); + return newState; + }; + + /** + * Set the state pending add users to contacts userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to add users to contacts. + * @return {Object} New state with array of pending add users to contacts userids. + */ + var addPendingAddContactsById = function(state, userIds) { + var newState = cloneState(state); + userIds.forEach(function(id) { + newState.pendingAddContactIds.push(id); + }); + return newState; + }; + + /** + * Set the state pending delete messages. + * + * @param {Object} state Current state. + * @param {Array} messageIds Messages to delete. + * @return {Object} New state with array of pending delete message ids. + */ + var addPendingDeleteMessagesById = function(state, messageIds) { + var newState = cloneState(state); + messageIds.forEach(function(id) { + newState.pendingDeleteMessageIds.push(id); + }); + return newState; + }; + + + /** + * Update the state pending block userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to remove from the list of user ids to block. + * @return {Object} New state with array of pending block userids. + */ + var removePendingBlockUsersById = function(state, userIds) { + var newState = cloneState(state); + newState.pendingBlockUserIds = newState.pendingBlockUserIds.filter(function(id) { + return userIds.indexOf(id) < 0; + }); + return newState; + }; + + /** + * Update the state pending remove userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to remove from the list of user ids to remove. + * @return {Object} New state with array of pending remove userids. + */ + var removePendingRemoveContactsById = function(state, userIds) { + var newState = cloneState(state); + newState.pendingRemoveContactIds = newState.pendingRemoveContactIds.filter(function(id) { + return userIds.indexOf(id) < 0; + }); + return newState; + }; + + /** + * Update the state pending unblock userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to remove from the list of user ids to unblock. + * @return {Object} New state with array of pending unblock userids. + */ + var removePendingUnblockUsersById = function(state, userIds) { + var newState = cloneState(state); + newState.pendingUnblockUserIds = newState.pendingUnblockUserIds.filter(function(id) { + return userIds.indexOf(id) < 0; + }); + return newState; + }; + + /** + * Update the state pending add to contacts userids. + * + * @param {Object} state Current state. + * @param {Array} userIds User ids to remove from the list of user ids to add to contacts. + * @return {Object} New state with array of pending add to contacts userids. + */ + var removePendingAddContactsById = function(state, userIds) { + var newState = cloneState(state); + newState.pendingAddContactIds = newState.pendingAddContactIds.filter(function(id) { + return userIds.indexOf(id) < 0; + }); + return newState; + }; + + /** + * Update the state pending delete messages userids. + * + * @param {Object} state Current state. + * @param {Array} messageIds Message ids to remove from the list of messages to delete. + * @return {Object} New state with array of messages to delete. + */ + var removePendingDeleteMessagesById = function(state, messageIds) { + var newState = cloneState(state); + newState.pendingDeleteMessageIds = newState.pendingDeleteMessageIds.filter(function(id) { + return messageIds.indexOf(id) < 0; + }); + return newState; + }; + + /** + * Add messages to state selected messages. + * + * @param {Object} state Current state. + * @param {Array} messageIds Messages that are selected. + * @return {Object} New state with array of not blocked members. + */ + var addSelectedMessagesById = function(state, messageIds) { + var newState = cloneState(state); + newState.selectedMessageIds = newState.selectedMessageIds.concat(messageIds); + return newState; + }; + + /** + * Remove messages from the state selected messages. + * + * @param {Object} state Current state. + * @param {Array} messageIds Messages to remove from selected messages. + * @return {Object} New state with array of selected messages. + */ + var removeSelectedMessagesById = function(state, messageIds) { + var newState = cloneState(state); + newState.selectedMessageIds = newState.selectedMessageIds.filter(function(id) { + return messageIds.indexOf(id) < 0; + }); + return newState; + }; + + /** + * Mark messages as read. + * + * @param {Object} state Current state. + * @param {Array} readMessages Messages that are read. + * @return {Object} New state with array of messages that have the isread attribute set. + */ + var markMessagesAsRead = function(state, readMessages) { + var newState = cloneState(state); + var readMessageIds = readMessages.map(function(message) { + return message.id; + }); + newState.messages = newState.messages.map(function(message) { + if (readMessageIds.indexOf(message.id) >= 0) { + message.isRead = true; + } + + return message; + }); + return newState; + }; + + /** + * Add a contact request to each of the members that the request is for. + * + * @param {Object} state Current state. + * @param {Array} requests The contact requests + * @return {Object} New state + */ + var addContactRequests = function(state, requests) { + var newState = cloneState(state); + + requests.forEach(function(request) { + var fromUserId = request.userid; + var toUserId = request.requesteduserid; + newState.members[fromUserId].contactrequests.push(request); + newState.members[toUserId].contactrequests.push(request); + }); + + return newState; + }; + + /** + * Remove a contact request from the members of that request. + * + * @param {Object} state Current state. + * @param {Array} requests The contact requests + * @return {Object} New state + */ + var removeContactRequests = function(state, requests) { + var newState = cloneState(state); + requests.forEach(function(request) { + var fromUserId = request.userid; + var toUserId = request.requesteduserid; + + newState.members[fromUserId].contactrequests = newState.members[fromUserId].contactrequests.filter(function(existing) { + return existing.userid != fromUserId; + }); + newState.members[toUserId].contactrequests = newState.members[toUserId].contactrequests.filter(function(existing) { + return existing.requesteduserid != toUserId; + }); + }); + + return newState; + }; + + return { + buildInitialState: buildInitialState, + addMessages: addMessages, + removeMessages: removeMessages, + removeMessagesById: removeMessagesById, + addMembers: addMembers, + removeMembers: removeMembers, + setLoadingMessages: setLoadingMessages, + setSendingMessage: setSendingMessage, + setLoadingMembers: setLoadingMembers, + setId: setId, + setName: setName, + setSubname: setSubname, + setType: setType, + setIsFavourite: setIsFavourite, + setTotalMemberCount: setTotalMemberCount, + setImageUrl: setImageUrl, + setLoadingConfirmAction: setLoadingConfirmAction, + setPendingDeleteConversation: setPendingDeleteConversation, + addPendingBlockUsersById: addPendingBlockUsersById, + addPendingRemoveContactsById: addPendingRemoveContactsById, + addPendingUnblockUsersById: addPendingUnblockUsersById, + addPendingAddContactsById: addPendingAddContactsById, + addPendingDeleteMessagesById: addPendingDeleteMessagesById, + removePendingBlockUsersById: removePendingBlockUsersById, + removePendingRemoveContactsById: removePendingRemoveContactsById, + removePendingUnblockUsersById: removePendingUnblockUsersById, + removePendingAddContactsById: removePendingAddContactsById, + removePendingDeleteMessagesById: removePendingDeleteMessagesById, + addSelectedMessagesById: addSelectedMessagesById, + removeSelectedMessagesById: removeSelectedMessagesById, + markMessagesAsRead: markMessagesAsRead, + addContactRequests: addContactRequests, + removeContactRequests: removeContactRequests + }; +}); diff --git a/message/amd/src/message_drawer_view_group_info.js b/message/amd/src/message_drawer_view_group_info.js new file mode 100644 index 00000000000..d14cd9bc28a --- /dev/null +++ b/message/amd/src/message_drawer_view_group_info.js @@ -0,0 +1,170 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the group info page of the message drawer. + * + * @module core_message/message_drawer_view_group_info + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/str', + 'core/templates', + 'core_message/message_repository', + 'core_message/message_drawer_lazy_load_list', +], +function( + $, + Str, + Templates, + Repository, + LazyLoadList +) { + + var LOAD_MEMBERS_LIMIT = 50; + + var SELECTORS = { + CONTENT_CONTAINER: '[data-region="group-info-content-container"]', + MEMBERS_LIST: '[data-region="members-list"]', + }; + + var TEMPLATES = { + CONTENT: 'core_message/message_drawer_view_group_info_body_content', + MEMBERS_LIST: 'core_message/message_drawer_view_group_info_participants_list' + }; + + /** + * Get the content container of the group info view container. + * + * @param {Object} root Contact container element. + * @return {Object} jQuery object + */ + var getContentContainer = function(root) { + return root.find(SELECTORS.CONTENT_CONTAINER); + }; + + /** + * Render the group info page. + * + * @param {Object} root Container element. + * @param {Object} conversation The group conversation. + * @param {Number} loggedInUserId The logged in user's id. + * @return {Object} jQuery promise + */ + var render = function(root, conversation, loggedInUserId) { + var placeholderCount = conversation.totalMemberCount > 50 ? 50 : conversation.totalMemberCount; + var placeholders = Array.apply(null, Array(placeholderCount)).map(function() { + return true; + }); + var templateContext = { + name: conversation.name, + subname: conversation.subname, + imageurl: conversation.imageUrl, + placeholders: placeholders, + loggedinuser: { + id: loggedInUserId + } + }; + + return Templates.render(TEMPLATES.CONTENT, templateContext) + .then(function(html) { + getContentContainer(root).append(html); + return html; + }); + }; + + /** + * Get the callback to load members of the conversation. + * + * @param {Object} conversation The conversation + * @param {Number} limit How many members to load + * @param {Number} offset How many memebers to skip + * @return {Function} the callback. + */ + var getLoadMembersCallback = function(conversation, limit, offset) { + return function(root, userId) { + return Repository.getConversationMembers(conversation.id, userId, limit + 1, offset) + .then(function(members) { + if (members.length > limit) { + members = members.slice(0, -1); + } else { + LazyLoadList.setLoadedAll(root, true); + } + + offset = offset + limit; + + return members; + }); + }; + }; + + /** + * Function to render the members in the list. + * + * @param {Object} contentContainer The list content container. + * @param {Array} members The list of members to render + * @return {Object} jQuery promise + */ + var renderMembersCallback = function(contentContainer, members) { + return Templates.render(TEMPLATES.MEMBERS_LIST, {contacts: members}) + .then(function(html) { + contentContainer.append(html); + return html; + }); + }; + + /** + * Setup the contact page. + * + * @param {Object} root Contact container element. + * @param {Number} conversation The conversation + * @param {Number} loggedInUserId The logged in user id + * @return {Object} jQuery promise + */ + var show = function(root, conversation, loggedInUserId) { + root = $(root); + + getContentContainer(root).empty(); + return render(root, conversation, loggedInUserId) + .then(function() { + var listRoot = LazyLoadList.getRoot(root); + LazyLoadList.show( + listRoot, + getLoadMembersCallback(conversation, LOAD_MEMBERS_LIMIT, 0), + renderMembersCallback + ); + return; + }); + }; + + /** + * String describing this page used for aria-labels. + * + * @param {Object} root Contact container element. + * @param {Number} conversation The conversation + * @return {Object} jQuery promise + */ + var description = function(root, conversation) { + return Str.get_string('messagedrawerviewgroupinfo', 'core_message', conversation.name); + }; + + return { + show: show, + description: description + }; +}); diff --git a/message/amd/src/message_drawer_view_overview.js b/message/amd/src/message_drawer_view_overview.js new file mode 100644 index 00000000000..c68486fdc26 --- /dev/null +++ b/message/amd/src/message_drawer_view_overview.js @@ -0,0 +1,145 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the overview page of the message drawer. + * + * @module core_message/message_drawer_view_overview + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/key_codes', + 'core/pubsub', + 'core/str', + 'core_message/message_drawer_view_overview_section_favourites', + 'core_message/message_drawer_view_overview_section_group_messages', + 'core_message/message_drawer_view_overview_section_messages', + 'core_message/message_drawer_router', + 'core_message/message_drawer_routes', + 'core_message/message_drawer_events' +], +function( + $, + KeyCodes, + PubSub, + Str, + Favourites, + GroupMessages, + Messages, + Router, + Routes, + MessageDrawerEvents +) { + + var SELECTORS = { + CONTACT_REQUEST_COUNT: '[data-region="contact-request-count"]', + FAVOURITES: '[data-region="view-overview-favourites"]', + GROUP_MESSAGES: '[data-region="view-overview-group-messages"]', + MESSAGES: '[data-region="view-overview-messages"]', + SEARCH_INPUT: '[data-region="view-overview-search-input"]' + }; + + /** + * Get the search input text element. + * + * @param {Object} header Overview header container element. + * @return {Object} The search input element. + */ + var getSearchInput = function(header) { + return header.find(SELECTORS.SEARCH_INPUT); + }; + + /** + * Decrement the contact request count. If the count is zero or below then + * hide the count. + * + * @param {Object} header Conversation header container element. + * @return {Function} A function to handle decrementing the count. + */ + var decrementContactRequestCount = function(header) { + return function() { + var countContainer = header.find(SELECTORS.CONTACT_REQUEST_COUNT); + var count = parseInt(countContainer.text(), 10); + count = isNaN(count) ? 0 : count - 1; + + if (count <= 0) { + countContainer.addClass('hidden'); + } else { + countContainer.text(count); + } + }; + }; + + /** + * Listen to, and handle event in the overview header. + * + * @param {Object} header Conversation header container element. + */ + var registerEventListeners = function(header) { + var searchInput = getSearchInput(header); + var ignoredKeys = [KeyCodes.tab, KeyCodes.shift, KeyCodes.ctrl, KeyCodes.alt]; + + searchInput.on('click', function() { + Router.go(Routes.VIEW_SEARCH); + }); + searchInput.on('keydown', function(e) { + if (ignoredKeys.indexOf(e.keyCode) < 0 && e.key != 'Meta') { + Router.go(Routes.VIEW_SEARCH); + } + }); + + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(header)); + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(header)); + }; + + /** + * Setup the overview page. + * + * @param {Object} header Overview header container element. + * @param {Object} body Overview body container element. + * @return {Object} jQuery promise + */ + var show = function(header, body) { + if (!header.attr('data-init')) { + registerEventListeners(header); + header.attr('data-init', true); + } + + getSearchInput(header).val(''); + + return $.when( + Favourites.show(body.find(SELECTORS.FAVOURITES)), + GroupMessages.show(body.find(SELECTORS.GROUP_MESSAGES)), + Messages.show(body.find(SELECTORS.MESSAGES)) + ); + }; + + /** + * String describing this page used for aria-labels. + * + * @return {Object} jQuery promise + */ + var description = function() { + return Str.get_string('messagedrawerviewoverview', 'core_message'); + }; + + return { + show: show, + description: description + }; +}); diff --git a/message/amd/src/message_drawer_view_overview_section.js b/message/amd/src/message_drawer_view_overview_section.js new file mode 100644 index 00000000000..f10eeaffc09 --- /dev/null +++ b/message/amd/src/message_drawer_view_overview_section.js @@ -0,0 +1,529 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls a section of the overview page in the message drawer. + * + * @module core_message/message_drawer_view_overview_section + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/custom_interaction_events', + 'core/notification', + 'core/pubsub', + 'core/str', + 'core/templates', + 'core/user_date', + 'core_message/message_repository', + 'core_message/message_drawer_events', + 'core_message/message_drawer_router', + 'core_message/message_drawer_routes', + 'core_message/message_drawer_lazy_load_list', + 'core_message/message_drawer_view_conversation_constants' +], +function( + $, + CustomEvents, + Notification, + PubSub, + Str, + Templates, + UserDate, + MessageRepository, + MessageDrawerEvents, + MessageDrawerRouter, + MessageDrawerRoutes, + LazyLoadList, + MessageDrawerViewConversationContants +) { + + var SELECTORS = { + TOGGLE: '[data-region="toggle"]', + CONVERSATION: '[data-conversation-id]', + BLOCKED_ICON_CONTAINER: '[data-region="contact-icon-blocked"]', + LAST_MESSAGE: '[data-region="last-message"]', + LAST_MESSAGE_DATE: '[data-region="last-message-date"]', + UNREAD_COUNT: '[data-region="unread-count"]', + SECTION_TOTAL_COUNT: '[data-region="section-total-count"]', + SECTION_UNREAD_COUNT: '[data-region="section-unread-count"]' + }; + + var TEMPLATES = { + CONVERSATIONS_LIST: 'core_message/message_drawer_conversations_list' + }; + + var LOAD_LIMIT = 50; + var loadedConversationsById = {}; + + /** + * Get the section visibility status. + * + * @param {Object} root The section container element. + * @return {Bool} Is section visible. + */ + var isVisible = function(root) { + return LazyLoadList.getRoot(root).hasClass('show'); + }; + + /** + * Set this section as expanded. + * + * @param {Object} root The section container element. + */ + var setExpanded = function(root) { + root.addClass('expanded'); + }; + + /** + * Set this section as collapsed. + * + * @param {Object} root The section container element. + */ + var setCollapsed = function(root) { + root.removeClass('expanded'); + }; + + /** + * Render the messages in the overview page. + * + * @param {Object} contentContainer Conversations content container. + * @param {Array} conversations List of conversations to render. + * @param {Number} userId Logged in user id. + * @return {Object} jQuery promise. + */ + var render = function(contentContainer, conversations, userId) { + var formattedConversations = conversations.map(function(conversation) { + var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null; + var formattedConversation = { + id: conversation.id, + imageurl: conversation.imageurl, + name: conversation.name, + subname: conversation.subname, + unreadcount: conversation.unreadcount, + lastmessagedate: lastMessage ? lastMessage.timecreated : null, + sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null, + lastmessage: lastMessage ? $(lastMessage.text).text() || lastMessage.text : null + }; + + if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) { + var otherUser = conversation.members.reduce(function(carry, member) { + if (!carry && member.id != userId) { + carry = member; + } + return carry; + }, null); + + formattedConversation.showonlinestatus = otherUser.showonlinestatus; + formattedConversation.isonline = otherUser.isosnline; + formattedConversation.isblocked = otherUser.isblocked; + } + + return formattedConversation; + }); + + return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: formattedConversations}) + .then(function(html) { + contentContainer.append(html); + return html; + }) + .catch(Notification.exception); + }; + + /** + * Build the callback to load conversations. + * + * @param {Number} type The conversation type. + * @param {Bool} includeFavourites Include/exclude favourites. + * @param {Number} offset Result offset + * @return {Function} + */ + var getLoadCallback = function(type, includeFavourites, offset) { + return function(root, userId) { + return MessageRepository.getConversations( + userId, + type, + LOAD_LIMIT + 1, + offset, + includeFavourites + ) + .then(function(response) { + var conversations = response.conversations; + + if (conversations.length > LOAD_LIMIT) { + conversations = conversations.slice(0, -1); + } else { + LazyLoadList.setLoadedAll(root, true); + } + + offset = offset + LOAD_LIMIT; + + conversations.forEach(function(conversation) { + loadedConversationsById[conversation.id] = conversation; + }); + + return conversations; + }) + .catch(Notification.exception); + }; + }; + + /** + * Get the total count container element. + * + * @param {Object} root Overview messages container element. + * @return {Object} Total count container element. + */ + var getTotalConversationCountElement = function(root) { + return root.find(SELECTORS.SECTION_TOTAL_COUNT); + }; + + /** + * Get the unread conversations count container element. + * + * @param {Object} root Overview messages container element. + * @return {Object} Unread conversations count container element. + */ + var getTotalUnreadConversationCountElement = function(root) { + return root.find(SELECTORS.SECTION_UNREAD_COUNT); + }; + + /** + * Increment the total conversations count. + * + * @param {Object} root Overview messages container element. + */ + var incrementTotalConversationCount = function(root) { + var element = getTotalConversationCountElement(root); + var count = parseInt(element.text()); + count = count + 1; + element.text(count); + }; + + /** + * Decrement the total conversations count. + * + * @param {Object} root Overview messages container element. + */ + var decrementTotalConversationCount = function(root) { + var element = getTotalConversationCountElement(root); + var count = parseInt(element.text()); + count = count - 1; + element.text(count); + }; + + /** + * Decrement the total unread conversations count. + * + * @param {Object} root Overview messages container element. + */ + var decrementTotalUnreadConversationCount = function(root) { + var element = getTotalUnreadConversationCountElement(root); + var count = parseInt(element.text()); + count = count - 1; + element.text(count); + + if (count < 1) { + element.addClass('hidden'); + } + }; + + /** + * Get a contact / conversation element. + * + * @param {Object} root Overview messages container element. + * @param {Number} conversationId The conversation id. + * @return {Object} Conversation element. + */ + var getConversationElement = function(root, conversationId) { + return root.find('[data-conversation-id="' + conversationId + '"]'); + }; + + /** + * Show the contact is blocked icon. + * + * @param {Object} conversationElement The conversation element. + */ + var blockContact = function(conversationElement) { + conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).removeClass('hidden'); + }; + + /** + * Hide the contact is blocked icon. + * + * @param {Object} conversationElement The conversation element. + */ + var unblockContact = function(conversationElement) { + conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).addClass('hidden'); + }; + + /** + * Update the last message from / to a contact. + * + * @param {Object} element Conversation element. + * @param {Object} conversation The conversation. + * @return {Object} jQuery promise + */ + var updateLastMessage = function(element, conversation) { + var message = conversation.messages[conversation.messages.length - 1]; + var youString = ''; + var stringRequests = [ + {key: 'you', component: 'core_message'}, + {key: 'strftimetime24', component: 'core_langconfig'}, + ]; + return Str.get_strings(stringRequests) + .then(function(strings) { + youString = strings[0]; + return UserDate.get([{timestamp: message.timeCreated, format: strings[1]}]); + }) + .then(function(dates) { + return dates[0]; + }) + .then(function(dateString) { + var lastMessage = $(message.text).text(); + + if (message.fromLoggedInUser) { + lastMessage = youString + ' ' + lastMessage; + } + + element.find(SELECTORS.LAST_MESSAGE).html(lastMessage); + element.find(SELECTORS.LAST_MESSAGE_DATE).text(dateString).removeClass('hidden'); + return dateString; + }) + .catch(Notification.exception); + }; + + /** + * Create an render new conversation element in the list of conversations. + * + * @param {Object} root Overview messages container element. + * @param {Object} conversation The conversation. + * @return {Object} jQuery promise + */ + var createNewConversation = function(root, conversation) { + var existingConversations = root.find(SELECTORS.CONVERSATION); + var text = ''; + + if (!existingConversations.length) { + // If we didn't have any conversations then we need to show + // the content of the list and hide the empty message. + var listRoot = LazyLoadList.getRoot(root); + LazyLoadList.showContent(listRoot); + LazyLoadList.hideEmptyMessage(listRoot); + } + + var messageCount = conversation.messages.length; + var lastMessage = messageCount ? conversation.messages[messageCount - 1] : null; + + if (lastMessage) { + text = $(lastMessage.text).text() || lastMessage.text; + } + + var formattedConversation = { + id: conversation.id, + name: conversation.name, + subname: conversation.subname, + lastmessagedate: lastMessage ? lastMessage.timeCreated : null, + sentfromcurrentuser: lastMessage ? lastMessage.fromLoggedInUser : null, + lastmessage: text, + imageurl: conversation.imageUrl, + }; + + return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: [formattedConversation]}) + .then(function(html) { + var contentContainer = LazyLoadList.getContentContainer(root); + return contentContainer.prepend(html); + }) + .then(function() { + return incrementTotalConversationCount(root); + }) + .catch(Notification.exception); + }; + + /** + * Delete a conversation from the list of conversations. + * + * @param {Object} root Overview messages container element. + * @param {Object} conversationElement The conversation element. + */ + var deleteConversation = function(root, conversationElement) { + conversationElement.remove(); + decrementTotalConversationCount(root); + + var conversations = root.find(SELECTORS.CONVERSATION); + if (!conversations.length) { + // If we don't have any conversations then we need to hide + // the content of the list and show the empty message. + var listRoot = LazyLoadList.getRoot(root); + LazyLoadList.hideContent(listRoot); + LazyLoadList.showEmptyMessage(listRoot); + } + }; + + /** + * Mark a conversation as read. + * + * @param {Object} root Overview messages container element. + * @param {Object} conversationElement The conversation element. + */ + var markConversationAsRead = function(root, conversationElement) { + var unreadCount = conversationElement.find(SELECTORS.UNREAD_COUNT); + unreadCount.text('0'); + unreadCount.addClass('hidden'); + decrementTotalUnreadConversationCount(root); + }; + + /** + * Listen to, and handle events in this section. + * + * @param {Object} root The section container element. + * @param {Function} loadCallback The callback to load items. + * @param {Number} type The conversation type for this section + * @param {Bool} includeFavourites If this section includes favourites + */ + var registerEventListeners = function(root, loadCallback, type, includeFavourites) { + var listRoot = LazyLoadList.getRoot(root); + + // Set the minimum height of the section to the height of the toggle. This + // smooths out the collapse animation. + var toggle = root.find(SELECTORS.TOGGLE); + root.css('min-height', toggle.outerHeight()); + + root.on('show.bs.collapse', function() { + setExpanded(root); + LazyLoadList.show(listRoot, loadCallback, render); + }); + + root.on('hidden.bs.collapse', function() { + setCollapsed(root); + }); + + PubSub.subscribe(MessageDrawerEvents.CONTACT_BLOCKED, function(conversationId) { + var conversationElement = getConversationElement(root, conversationId); + if (conversationElement.length) { + blockContact(conversationElement); + } + }); + + PubSub.subscribe(MessageDrawerEvents.CONTACT_UNBLOCKED, function(conversationId) { + var conversationElement = getConversationElement(root, conversationId); + if (conversationElement.length) { + unblockContact(conversationElement); + } + }); + + PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) { + if ( + (type && conversation.type != type) || + (includeFavourites && !conversation.isFavourite) || + (!includeFavourites && conversation.isFavourite) + ) { + return; + } + + var conversationId = conversation.id; + var element = getConversationElement(root, conversationId); + if (element.length) { + updateLastMessage(element, conversation); + } else { + createNewConversation(root, conversation); + } + }); + + PubSub.subscribe(MessageDrawerEvents.CONVERSATION_DELETED, function(conversationId) { + var conversationElement = getConversationElement(root, conversationId); + if (conversationElement.length) { + deleteConversation(root, conversationElement); + } + }); + + PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, function(conversationId) { + var conversationElement = getConversationElement(root, conversationId); + if (conversationElement.length) { + markConversationAsRead(root, conversationElement); + } + }); + + PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, function(conversation) { + var conversationElement = null; + if (includeFavourites && (!type || type == conversation.type)) { + conversationElement = getConversationElement(root, conversation.id); + if (!conversationElement.length) { + createNewConversation(root, conversation); + } + } else if (type == conversation.type) { + conversationElement = getConversationElement(root, conversation.id); + if (conversationElement.length) { + deleteConversation(root, conversationElement); + } + } + }); + + PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, function(conversation) { + var conversationElement = null; + if (includeFavourites) { + conversationElement = getConversationElement(root, conversation.id); + if (conversationElement.length) { + deleteConversation(root, conversationElement); + } + } else if (type == conversation.type) { + conversationElement = getConversationElement(root, conversation.id); + if (!conversationElement.length) { + createNewConversation(root, conversation); + } + } + }); + + CustomEvents.define(root, [CustomEvents.events.activate]); + root.on(CustomEvents.events.activate, SELECTORS.CONVERSATION, function(e, data) { + var conversationElement = $(e.target).closest(SELECTORS.CONVERSATION); + var conversationId = conversationElement.attr('data-conversation-id'); + var conversation = loadedConversationsById[conversationId]; + MessageDrawerRouter.go(MessageDrawerRoutes.VIEW_CONVERSATION, conversation); + + data.originalEvent.preventDefault(); + }); + }; + + /** + * Setup the section. + * + * @param {Object} root The section container element. + * @param {Number} type The conversation type for this section + * @param {Bool} includeFavourites If this section includes favourites + */ + var show = function(root, type, includeFavourites) { + root = $(root); + + if (!root.attr('data-init')) { + var loadCallback = getLoadCallback(type, includeFavourites, 0); + registerEventListeners(root, loadCallback, type, includeFavourites); + + if (isVisible(root)) { + setExpanded(root); + var listRoot = LazyLoadList.getRoot(root); + LazyLoadList.show(listRoot, loadCallback, render); + } + + root.attr('data-init', true); + } + }; + + return { + show: show + }; +}); diff --git a/message/amd/src/message_drawer_view_overview_section_favourites.js b/message/amd/src/message_drawer_view_overview_section_favourites.js new file mode 100644 index 00000000000..163401015d6 --- /dev/null +++ b/message/amd/src/message_drawer_view_overview_section_favourites.js @@ -0,0 +1,48 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the favourites section of the overview page in the message drawer. + * + * @module core_message/message_drawer_view_overview_section_favourites + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core_message/message_drawer_view_overview_section' +], +function( + $, + Section +) { + // All conversation types. + var CONVERSATION_TYPE = null; + var INCLUDE_FAVOURITES = true; + + /** + * Show the overview page conversations. + * + * @param {Object} root Overview messages container element. + */ + var show = function(root) { + Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES); + }; + + return { + show: show, + }; +}); diff --git a/message/amd/src/message_drawer_view_overview_section_group_messages.js b/message/amd/src/message_drawer_view_overview_section_group_messages.js new file mode 100644 index 00000000000..4713d971cdd --- /dev/null +++ b/message/amd/src/message_drawer_view_overview_section_group_messages.js @@ -0,0 +1,49 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the group overview section of the overview page in the message drawer. + * + * @module core_message/message_drawer_view_overview_section_group_messages + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core_message/message_drawer_view_overview_section' +], +function( + $, + Section +) { + // Public conversations. + var CONVERSATION_TYPE = 2; + var INCLUDE_FAVOURITES = false; + + /** + * Show the overview page conversations. + * + * @param {Object} root Overview messages container element. + */ + var show = function(root) { + root = $(root); + Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES); + }; + + return { + show: show, + }; +}); diff --git a/message/amd/src/message_drawer_view_overview_section_messages.js b/message/amd/src/message_drawer_view_overview_section_messages.js new file mode 100644 index 00000000000..75be3704ad5 --- /dev/null +++ b/message/amd/src/message_drawer_view_overview_section_messages.js @@ -0,0 +1,48 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the messages section of the overview page in the message drawer. + * + * @module core_message/message_drawer_view_overview_section_messages + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core_message/message_drawer_view_overview_section' +], +function( + $, + Section +) { + // Private conversations. + var CONVERSATION_TYPE = 1; + var INCLUDE_FAVOURITES = false; + + /** + * Show the overview page conversations. + * + * @param {Object} root Overview messages container element. + */ + var show = function(root) { + Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES); + }; + + return { + show: show, + }; +}); diff --git a/message/amd/src/message_drawer_view_search.js b/message/amd/src/message_drawer_view_search.js new file mode 100644 index 00000000000..5fa0ae82e02 --- /dev/null +++ b/message/amd/src/message_drawer_view_search.js @@ -0,0 +1,837 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the search page of the message drawer. + * + * @module core_message/message_drawer_view_search + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/custom_interaction_events', + 'core/notification', + 'core/pubsub', + 'core/str', + 'core/templates', + 'core_message/message_repository', + 'core_message/message_drawer_events', +], +function( + $, + CustomEvents, + Notification, + PubSub, + Str, + Templates, + Repository, + Events +) { + + var MESSAGE_SEARCH_LIMIT = 50; + var USERS_SEARCH_LIMIT = 50; + var USERS_INITIAL_SEARCH_LIMIT = 3; + + var SELECTORS = { + BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]', + CANCEL_SEARCH_BUTTON: '[data-action="cancel-search"]', + CONTACTS_CONTAINER: '[data-region="contacts-container"]', + CONTACTS_LIST: '[data-region="contacts-container"] [data-region="list"]', + EMPTY_MESSAGE_CONTAINER: '[data-region="empty-message-container"]', + LIST: '[data-region="list"]', + LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', + LOADING_PLACEHOLDER: '[data-region="loading-placeholder"]', + MESSAGES_LIST: '[data-region="messages-container"] [data-region="list"]', + MESSAGES_CONTAINER: '[data-region="messages-container"]', + NON_CONTACTS_CONTAINER: '[data-region="non-contacts-container"]', + NON_CONTACTS_LIST: '[data-region="non-contacts-container"] [data-region="list"]', + SEARCH_ICON_CONTAINER: '[data-region="search-icon-container"]', + SEARCH_ACTION: '[data-action="search"]', + SEARCH_INPUT: '[data-region="search-input"]', + SEARCH_RESULTS_CONTAINER: '[data-region="search-results-container"]', + LOAD_MORE_USERS: '[data-action="load-more-users"]', + LOAD_MORE_MESSAGES: '[data-action="load-more-messages"]', + BUTTON_TEXT: '[data-region="button-text"]', + NO_RESULTS_CONTAINTER: '[data-region="no-results-container"]', + }; + + var TEMPLATES = { + CONTACTS_LIST: 'core_message/message_drawer_contacts_list', + NON_CONTACTS_LIST: 'core_message/message_drawer_non_contacts_list', + MESSAGES_LIST: 'core_message/message_drawer_messages_list' + }; + + /** + * Get the logged in user id. + * + * @param {Object} body Search body container element. + * @return {Number} User id. + */ + var getLoggedInUserId = function(body) { + return body.attr('data-user-id'); + }; + + /** + * Show the no messages container element. + * + * @param {Object} body Search body container element. + * @return {Object} No messages container element. + */ + var getEmptyMessageContainer = function(body) { + return body.find(SELECTORS.EMPTY_MESSAGE_CONTAINER); + }; + + /** + * Get the search loading icon. + * + * @param {Object} header Search header container element. + * @return {Object} Loading icon element. + */ + var getLoadingIconContainer = function(header) { + return header.find(SELECTORS.LOADING_ICON_CONTAINER); + }; + + /** + * Get the loading container element. + * + * @param {Object} body Search body container element. + * @return {Object} Loading container element. + */ + var getLoadingPlaceholder = function(body) { + return body.find(SELECTORS.LOADING_PLACEHOLDER); + }; + + /** + * Get the search icon container. + * + * @param {Object} header Search header container element. + * @return {Object} Search icon container. + */ + var getSearchIconContainer = function(header) { + return header.find(SELECTORS.SEARCH_ICON_CONTAINER); + }; + + /** + * Get the search input container. + * + * @param {Object} header Search header container element. + * @return {Object} Search input container. + */ + var getSearchInput = function(header) { + return header.find(SELECTORS.SEARCH_INPUT); + }; + + /** + * Get the search results container. + * + * @param {Object} body Search body container element. + * @return {Object} Search results container. + */ + var getSearchResultsContainer = function(body) { + return body.find(SELECTORS.SEARCH_RESULTS_CONTAINER); + }; + + /** + * Get the search contacts container. + * + * @param {Object} body Search body container element. + * @return {Object} Search contacts container. + */ + var getContactsContainer = function(body) { + return body.find(SELECTORS.CONTACTS_CONTAINER); + }; + + /** + * Get the search non contacts container. + * + * @param {Object} body Search body container element. + * @return {Object} Search non contacts container. + */ + var getNonContactsContainer = function(body) { + return body.find(SELECTORS.NON_CONTACTS_CONTAINER); + }; + + /** + * Get the search messages container. + * + * @param {Object} body Search body container element. + * @return {Object} Search messages container. + */ + var getMessagesContainer = function(body) { + return body.find(SELECTORS.MESSAGES_CONTAINER); + }; + + + /** + * Show the messages empty container. + * + * @param {Object} body Search body container element. + */ + var showEmptyMessage = function(body) { + getEmptyMessageContainer(body).removeClass('hidden'); + }; + + /** + * Hide the messages empty container. + * + * @param {Object} body Search body container element. + */ + var hideEmptyMessage = function(body) { + getEmptyMessageContainer(body).addClass('hidden'); + }; + + + /** + * Show the loading icon. + * + * @param {Object} header Search header container element. + */ + var showLoadingIcon = function(header) { + getLoadingIconContainer(header).removeClass('hidden'); + }; + + /** + * Hide the loading icon. + * + * @param {Object} header Search header container element. + */ + var hideLoadingIcon = function(header) { + getLoadingIconContainer(header).addClass('hidden'); + }; + + /** + * Show loading placeholder. + * + * @param {Object} body Search body container element. + */ + var showLoadingPlaceholder = function(body) { + getLoadingPlaceholder(body).removeClass('hidden'); + }; + + /** + * Hide loading placeholder. + * + * @param {Object} body Search body container element. + */ + var hideLoadingPlaceholder = function(body) { + getLoadingPlaceholder(body).addClass('hidden'); + }; + + /** + * Show search icon. + * + * @param {Object} header Search header container element. + */ + var showSearchIcon = function(header) { + getSearchIconContainer(header).removeClass('hidden'); + }; + + /** + * Hide search icon. + * + * @param {Object} header Search header container element. + */ + var hideSearchIcon = function(header) { + getSearchIconContainer(header).addClass('hidden'); + }; + + /** + * Show search results. + * + * @param {Object} body Search body container element. + */ + var showSearchResults = function(body) { + getSearchResultsContainer(body).removeClass('hidden'); + }; + + /** + * Hide search results. + * + * @param {Object} body Search body container element. + */ + var hideSearchResults = function(body) { + getSearchResultsContainer(body).addClass('hidden'); + }; + + /** + * Disable the search input. + * + * @param {Object} header Search header container element. + */ + var disableSearchInput = function(header) { + getSearchInput(header).prop('disabled', true); + }; + + /** + * Enable the search input. + * + * @param {Object} header Search header container element. + */ + var enableSearchInput = function(header) { + getSearchInput(header).prop('disabled', false); + }; + + /** + * Clear the search input. + * + * @param {Object} header Search header container element. + */ + var clearSearchInput = function(header) { + getSearchInput(header).val(''); + }; + + /** + * Clear all search results + * + * @param {Object} body Search body container element. + */ + var clearAllSearchResults = function(body) { + body.find(SELECTORS.CONTACTS_LIST).empty(); + body.find(SELECTORS.NON_CONTACTS_LIST).empty(); + body.find(SELECTORS.MESSAGES_LIST).empty(); + body.find(SELECTORS.NO_RESULTS_CONTAINTER).addClass('hidden'); + showLoadMoreUsersButton(body); + showLoadMoreMessagesButton(body); + }; + + /** + * Update the body and header to indicate the search is loading. + * + * @param {Object} header Search header container element. + * @param {Object} body Search body container element. + */ + var startLoading = function(header, body) { + hideSearchIcon(header); + hideEmptyMessage(body); + hideSearchResults(body); + showLoadingIcon(header); + showLoadingPlaceholder(body); + disableSearchInput(header); + }; + + /** + * Update the body and header to indicate the search has stopped loading. + * + * @param {Object} header Search header container element. + * @param {Object} body Search body container element. + */ + var stopLoading = function(header, body) { + showSearchIcon(header); + hideEmptyMessage(body); + showSearchResults(body); + hideLoadingIcon(header); + hideLoadingPlaceholder(body); + enableSearchInput(header); + }; + + /** + * Show the more users loading icon. + * + * @param {Object} root The more users container element. + */ + var showUsersLoadingIcon = function(root) { + var button = root.find(SELECTORS.LOAD_MORE_USERS); + button.prop('disabled', true); + button.find(SELECTORS.BUTTON_TEXT).addClass('hidden'); + button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden'); + }; + + /** + * Hide the more users loading icon. + * + * @param {Object} root The more users container element. + */ + var hideUsersLoadingIcon = function(root) { + var button = root.find(SELECTORS.LOAD_MORE_USERS); + button.prop('disabled', false); + button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden'); + button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden'); + }; + + /** + * Show the load more users button. + * + * @param {Object} root The users container element. + */ + var showLoadMoreUsersButton = function(root) { + root.find(SELECTORS.LOAD_MORE_USERS).removeClass('hidden'); + }; + + /** + * Hide the load more users button. + * + * @param {Object} root The users container element. + */ + var hideLoadMoreUsersButton = function(root) { + root.find(SELECTORS.LOAD_MORE_USERS).addClass('hidden'); + }; + + /** + * Show the messages are loading icon. + * + * @param {Object} root Messages root element. + */ + var showMessagesLoadingIcon = function(root) { + var button = root.find(SELECTORS.LOAD_MORE_MESSAGES); + button.prop('disabled', true); + button.find(SELECTORS.BUTTON_TEXT).addClass('hidden'); + button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden'); + }; + + /** + * Hide the messages are loading icon. + * + * @param {Object} root Messages root element. + */ + var hideMessagesLoadingIcon = function(root) { + var button = root.find(SELECTORS.LOAD_MORE_MESSAGES); + button.prop('disabled', false); + button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden'); + button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden'); + }; + + /** + * Show the load more messages button. + * + * @param {Object} root The messages container element. + */ + var showLoadMoreMessagesButton = function(root) { + root.find(SELECTORS.LOAD_MORE_MESSAGES).removeClass('hidden'); + }; + + /** + * Hide the load more messages button. + * + * @param {Object} root The messages container element. + */ + var hideLoadMoreMessagesButton = function(root) { + root.find(SELECTORS.LOAD_MORE_MESSAGES).addClass('hidden'); + }; + + /** + * Find a contact in the search results. + * + * @param {Object} root Search results container element. + * @param {Number} userId User id. + * @return {Object} User container element. + */ + var findContact = function(root, userId) { + return root.find('[data-contact-user-id="' + userId + '"]'); + }; + + /** + * Add a contact to the search results. + * + * @param {Object} root Search results container. + * @param {Object} contact User in contacts list. + */ + var addContact = function(root, contact) { + var nonContactsContainer = getNonContactsContainer(root); + var nonContact = findContact(nonContactsContainer, contact.userid); + + if (nonContact.length) { + nonContact.remove(); + var contactsContainer = getContactsContainer(root); + contactsContainer.removeClass('hidden'); + contactsContainer.find(SELECTORS.LIST).append(nonContact); + } + + if (!nonContactsContainer.find(SELECTORS.LIST).children().length) { + nonContactsContainer.addClass('hidden'); + } + }; + + /** + * Remove a contact from the contacts results. + * + * @param {Object} root Search results container. + * @param {Object} userId Contact user id. + */ + var removeContact = function(root, userId) { + var contactsContainer = getContactsContainer(root); + var contact = findContact(contactsContainer, userId); + + if (contact.length) { + contact.remove(); + var nonContactsContainer = getNonContactsContainer(root); + nonContactsContainer.removeClass('hidden'); + nonContactsContainer.find(SELECTORS.LIST).append(contact); + } + + if (!contactsContainer.find(SELECTORS.LIST).children().length) { + contactsContainer.addClass('hidden'); + } + }; + + /** + * Show the contact is blocked icon. + * + * @param {Object} root Search results container. + * @param {Object} userId Contact user id. + */ + var blockContact = function(root, userId) { + var contact = findContact(root, userId); + if (contact.length) { + contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden'); + } + }; + + /** + * Hide the contact is blocked icon. + * + * @param {Object} root Search results container. + * @param {Object} userId Contact user id. + */ + var unblockContact = function(root, userId) { + var contact = findContact(root, userId); + if (contact.length) { + contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden'); + } + }; + + /** + * Render contacts in the contacts search results. + * + * @param {Object} root Search results container. + * @param {Array} contacts List of contacts. + * @return {Promise} Renderer promise. + */ + var renderContacts = function(root, contacts) { + var container = getContactsContainer(root); + var list = container.find(SELECTORS.LIST); + + if (!contacts.length && !list.children().length) { + var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER); + noResultsContainer.removeClass('hidden'); + return $.Deferred().resolve('').promise(); + } else { + return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts}) + .then(function(html) { + list.append(html); + return html; + }); + } + }; + + /** + * Render non contacts in the contacts search results. + * + * @param {Object} root Search results container. + * @param {Array} nonContacts List of non contacts. + * @return {Promise} Renderer promise. + */ + var renderNonContacts = function(root, nonContacts) { + var container = getNonContactsContainer(root); + var list = container.find(SELECTORS.LIST); + + if (!nonContacts.length && !list.children().length) { + var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER); + noResultsContainer.removeClass('hidden'); + return $.Deferred().resolve('').promise(); + } else { + return Templates.render(TEMPLATES.NON_CONTACTS_LIST, {noncontacts: nonContacts}) + .then(function(html) { + list.append(html); + return html; + }); + } + }; + + /** + * Render messages in the messages search results. + * + * @param {Object} root Search results container. + * @param {Array} messages List of messages. + * @return {Promise} Renderer promise. + */ + var renderMessages = function(root, messages) { + var container = getMessagesContainer(root); + var list = container.find(SELECTORS.LIST); + + if (!messages.length && !list.children().length) { + var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER); + noResultsContainer.removeClass('hidden'); + return $.Deferred().resolve('').promise(); + } else { + return Templates.render(TEMPLATES.MESSAGES_LIST, {messages: messages}) + .then(function(html) { + list.append(html); + return html; + }); + } + }; + + /** + * Load more users from the repository and render the results into the users search results. + * + * @param {Object} root Search results container. + * @param {Number} loggedInUserId Current logged in user. + * @param {String} text Search text. + * @param {Number} limit Number of users to get. + * @param {Number} offset Load users from + * @return {Object} jQuery promise + */ + var loadMoreUsers = function(root, loggedInUserId, text, limit, offset) { + var loadedAll = false; + showUsersLoadingIcon(root); + return Repository.searchUsers(loggedInUserId, text, limit + 1, offset) + .then(function(results) { + var contacts = results.contacts; + var noncontacts = results.noncontacts; + + if (contacts.length <= limit && noncontacts.length <= limit) { + loadedAll = true; + return { + contacts: contacts, + noncontacts: noncontacts + }; + } else { + return { + contacts: contacts.slice(0, limit), + noncontacts: noncontacts.slice(0, limit) + }; + } + }) + .then(function(results) { + return $.when( + renderContacts(root, results.contacts), + renderNonContacts(root, results.noncontacts) + ); + }) + .then(function() { + hideUsersLoadingIcon(root); + + if (loadedAll) { + hideLoadMoreUsersButton(root); + } + + return; + }) + .catch(function(error) { + hideUsersLoadingIcon(root); + // Rethrow error for other handlers. + throw error; + }); + }; + + /** + * Load more messages from the repository and render the results into the messages search results. + * + * @param {Object} root Search results container. + * @param {Number} loggedInUserId Current logged in user. + * @param {String} text Search text. + * @param {Number} limit Number of messages to get. + * @param {Number} offset Load messages from + * @return {Object} jQuery promise + */ + var loadMoreMessages = function(root, loggedInUserId, text, limit, offset) { + var loadedAll = false; + showMessagesLoadingIcon(root); + return Repository.searchMessages(loggedInUserId, text, limit + 1, offset) + .then(function(results) { + var messages = results.contacts; + + if (messages.length <= limit) { + loadedAll = true; + return messages; + } else { + return messages.slice(0, limit); + } + }) + .then(function(messages) { + return renderMessages(root, messages); + }) + .then(function() { + hideMessagesLoadingIcon(root); + + if (loadedAll) { + hideLoadMoreMessagesButton(root); + } + + return; + }) + .catch(function(error) { + hideMessagesLoadingIcon(root); + // Rethrow error for other handlers. + throw error; + }); + }; + + /** + * Search for users and messages. + * + * @param {Object} header Search header container element. + * @param {Object} body Search body container element. + * @param {String} searchText Search text. + * @param {Number} usersLimit The users limit. + * @param {Number} usersOffset The users offset. + * @param {Number} messagesLimit The message limit. + * @param {Number} messagesOffset The message offset. + * @return {Object} jQuery promise + */ + var search = function(header, body, searchText, usersLimit, usersOffset, messagesLimit, messagesOffset) { + var loggedInUserId = getLoggedInUserId(body); + startLoading(header, body); + clearAllSearchResults(body); + + return $.when( + loadMoreUsers(body, loggedInUserId, searchText, usersLimit, usersOffset), + loadMoreMessages(body, loggedInUserId, searchText, messagesLimit, messagesOffset) + ) + .then(function() { + stopLoading(header, body); + return; + }); + }; + + + /** + * Listen to and handle events for searching. + * + * @param {Object} header Search header container element. + * @param {Object} body Search body container element. + */ + var registerEventListeners = function(header, body) { + var loggedInUserId = getLoggedInUserId(body); + var searchInput = getSearchInput(header); + var searchText = ''; + var messagesOffset = 0; + var usersOffset = 0; + + var searchEventHandler = function(e, data) { + searchText = searchInput.val().trim(); + + if (searchText !== '') { + messagesOffset = 0; + usersOffset = 0; + search( + header, + body, + searchText, + USERS_INITIAL_SEARCH_LIMIT, + usersOffset, + MESSAGE_SEARCH_LIMIT, + messagesOffset + ) + .then(function() { + searchInput.focus(); + usersOffset = usersOffset + USERS_INITIAL_SEARCH_LIMIT; + messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT; + return; + }) + .catch(Notification.exception); + } + + data.originalEvent.preventDefault(); + }; + + CustomEvents.define(searchInput, [CustomEvents.events.enter]); + CustomEvents.define(header, [CustomEvents.events.activate]); + CustomEvents.define(body, [CustomEvents.events.activate]); + + searchInput.on(CustomEvents.events.enter, searchEventHandler); + + header.on(CustomEvents.events.activate, SELECTORS.SEARCH_ACTION, searchEventHandler); + + body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_MESSAGES, function(e, data) { + if (searchText !== '') { + loadMoreMessages(body, loggedInUserId, searchText, MESSAGE_SEARCH_LIMIT, messagesOffset) + .then(function() { + messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT; + return; + }) + .catch(Notification.exception); + } + data.originalEvent.preventDefault(); + }); + + body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_USERS, function(e, data) { + if (searchText !== '') { + loadMoreUsers(body, loggedInUserId, searchText, USERS_SEARCH_LIMIT, usersOffset) + .then(function() { + usersOffset = usersOffset + USERS_SEARCH_LIMIT; + return; + }) + .catch(Notification.exception); + } + data.originalEvent.preventDefault(); + }); + + header.on(CustomEvents.events.activate, SELECTORS.CANCEL_SEARCH_BUTTON, function() { + clearSearchInput(header); + showEmptyMessage(body); + showSearchIcon(header); + hideSearchResults(body); + hideLoadingIcon(header); + hideLoadingPlaceholder(body); + usersOffset = 0; + messagesOffset = 0; + }); + + PubSub.subscribe(Events.CONTACT_ADDED, function(userId) { + addContact(body, userId); + }); + + PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) { + removeContact(body, userId); + }); + + PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) { + blockContact(body, userId); + }); + + PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) { + unblockContact(body, userId); + }); + }; + + /** + * Setup the search page. + * + * @param {Object} header Contacts header container element. + * @param {Object} body Contacts body container element. + * @return {Object} jQuery promise + */ + var show = function(header, body) { + if (!body.attr('data-init')) { + registerEventListeners(header, body); + body.attr('data-init', true); + } + + var searchInput = getSearchInput(header); + searchInput.focus(); + + return $.Deferred().resolve().promise(); + }; + + /** + * String describing this page used for aria-labels. + * + * @param {Object} header Contacts header container element. + * @return {Object} jQuery promise + */ + var description = function(header) { + var searchInput = getSearchInput(header); + var searchText = searchInput.val().trim(); + return Str.get_string('messagedrawerviewsearch', 'core_message', searchText); + }; + + return { + show: show, + description: description + }; +}); diff --git a/message/amd/src/message_drawer_view_settings.js b/message/amd/src/message_drawer_view_settings.js new file mode 100644 index 00000000000..3df6cb4eb00 --- /dev/null +++ b/message/amd/src/message_drawer_view_settings.js @@ -0,0 +1,142 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the settings page in the message drawer. + * + * @module core_message/message_drawer_view_settings + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/notification', + 'core/str', + 'core_message/message_repository', + 'core/custom_interaction_events', +], +function( + $, + Notification, + Str, + Repository, + CustomEvents +) { + + var SELECTORS = { + SETTINGS: '[data-region="settings"]', + PREFERENCE_CONTROL: '[data-region="preference-control"]', + PRIVACY_PREFERENCE: '[data-preference="blocknoncontacts"] input[type="radio"]', + EMAIL_ENABLED_PREFERENCE: '[data-preference="emailnotifications"] input[type="checkbox"]' + }; + + var PREFERENCES_EMAIL = { + 'message_provider_moodle_instantmessage_loggedoff': { + type: 'emailnotifications', + enabled: 'email', + disabled: 'none' + }, + 'message_provider_moodle_instantmessage_loggedin': { + type: 'emailnotifications', + enabled: 'email', + disabled: 'none' + } + }; + + /** + * Create all of the event listeners for the message preferences page. + * + * @method registerEventListeners + * @param {Object} body The settings body element. + * @param {Number} loggedInUserId The logged in user id. + */ + var registerEventListeners = function(body, loggedInUserId) { + var settingsContainer = body.find(SELECTORS.SETTINGS); + + CustomEvents.define(settingsContainer, [ + CustomEvents.events.activate + ]); + + settingsContainer.on(CustomEvents.events.activate, SELECTORS.EMAIL_ENABLED_PREFERENCE, function(e) { + var checkbox = $(e.target); + var setting = checkbox.closest(SELECTORS.PREFERENCE_CONTROL); + var type = setting.attr('data-preference'); + var isEnabled = checkbox.prop('checked'); + var preferences = Object.keys(PREFERENCES_EMAIL).reduce(function(carry, preference) { + var config = PREFERENCES_EMAIL[preference]; + + if (config.type === type) { + carry.push({ + type: preference, + value: isEnabled ? config.enabled : config.disabled + }); + } + + return carry; + }, []); + + Repository.savePreferences(loggedInUserId, preferences) + .catch(Notification.exception); + } + ); + + settingsContainer.on(CustomEvents.events.activate, SELECTORS.PRIVACY_PREFERENCE, function(e) { + var newValue = $(e.target).val(); + var preferences = [ + { + type: 'message_blocknoncontacts', + value: newValue + } + ]; + + Repository.savePreferences(loggedInUserId, preferences) + .catch(Notification.exception); + } + ); + }; + + /** + * Initialise the settings page by adding event listeners to + * the checkboxes. + * + * @param {Object} header The settings header element. + * @param {Object} body The settings body element. + * @param {Number} loggedInUserId The logged in user id. + * @return {Object} jQuery promise + */ + var show = function(header, body, loggedInUserId) { + if (!body.attr('data-init')) { + registerEventListeners(body, loggedInUserId); + body.attr('data-init', true); + } + + return $.Deferred().resolve().promise(); + }; + + /** + * String describing this page used for aria-labels. + * + * @return {Object} jQuery promise + */ + var description = function() { + return Str.get_string('messagedrawerviewsettings', 'core_message'); + }; + + return { + show: show, + description: description, + }; +}); diff --git a/message/lib.php b/message/lib.php index a85e65ad91c..6877b7bf5a7 100644 --- a/message/lib.php +++ b/message/lib.php @@ -763,3 +763,109 @@ function core_message_user_preferences() { }); return $preferences; } + +/** + * Render the message drawer to be included in the top of the body of + * each page. + * + * @return string HTML + */ +function core_message_before_standard_top_of_body_html() { + global $USER, $CFG, $PAGE; + + // Early bail out conditions. + if (empty($CFG->messaging) || !isloggedin() || isguestuser() || user_not_fully_set_up($USER) || + get_user_preferences('auth_forcepasswordchange') || + (!$USER->policyagreed && !is_siteadmin() && + ($manager = new \core_privacy\local\sitepolicy\manager()) && $manager->is_defined())) { + return ''; + } + + $renderer = $PAGE->get_renderer('core'); + $unreadconversationcount = \core_message\api::count_unread_conversations($USER); + $individualconversationcount = \core_message\api::count_conversations( + $USER, + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + true + ); + $groupconversationcount = \core_message\api::count_conversations( + $USER, + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + true + ); + $systemcontext = \context_system::instance(); + $usercontext = \context_user::instance($USER->id); + $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); + $favouriteconversationcount = $ufservice->count_favourites_by_type('core_message', 'message_conversations', $systemcontext); + $requestcount = \core_message\api::count_received_contact_requests($USER); + $contactscount = \core_message\api::count_contacts($USER->id); + + // Get the privacy settings options for being messaged. + $privacysetting = \core_message\api::get_user_privacy_messaging_preference($USER->id); + $choices = []; + $choices[] = [ + 'value' => \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, + 'text' => get_string('contactableprivacy_onlycontacts', 'message'), + 'checked' => ($privacysetting == \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS) + ]; + $choices[] = [ + 'value' => \core_message\api::MESSAGE_PRIVACY_COURSEMEMBER, + 'text' => get_string('contactableprivacy_coursemember', 'message'), + 'checked' => ($privacysetting == \core_message\api::MESSAGE_PRIVACY_COURSEMEMBER) + ]; + if (!empty($CFG->messagingallusers)) { + // Add the MESSAGE_PRIVACY_SITE option when site-wide messaging between users is enabled. + $choices[] = [ + 'value' => \core_message\api::MESSAGE_PRIVACY_SITE, + 'text' => get_string('contactableprivacy_site', 'message'), + 'checked' => ($privacysetting == \core_message\api::MESSAGE_PRIVACY_SITE) + ]; + } + // Email settings. + $emailloggedin = get_user_preferences('message_provider_moodle_instantmessage_loggedin', 'none', $USER->id); + $emailloggedoff = get_user_preferences('message_provider_moodle_instantmessage_loggedoff', 'none', $USER->id); + $emailenabled = $emailloggedin == 'email' && $emailloggedoff == 'email'; + + return $renderer->render_from_template('core_message/message_drawer', [ + 'contactrequestcount' => $requestcount, + 'loggedinuser' => [ + 'id' => $USER->id, + 'midnight' => usergetmidnight(time()) + ], + 'overview' => [ + 'messages' => [ + 'expanded' => false, + 'count' => [ + 'unread' => $unreadconversationcount, + 'total' => $individualconversationcount + ], + 'placeholders' => array_fill(0, $individualconversationcount, true) + ], + 'groupmessages' => [ + 'expanded' => false, + 'count' => [ + 'unread' => 0, // TODO: fix me. + 'total' => $groupconversationcount + ], + 'placeholders' => array_fill(0, $groupconversationcount, true) + ], + 'favourites' => [ + 'expanded' => true, + 'count' => [ + 'unread' => 0, // TODO: fix me. + 'total' => $favouriteconversationcount + ], + 'placeholders' => array_fill(0, $favouriteconversationcount, true) + ], + ], + 'contacts' => [ + 'sectioncontacts' => [ + 'placeholders' => array_fill(0, $contactscount > 50 ? 50 : $contactscount, true) + ] + ], + 'settings' => [ + 'privacy' => $choices, + 'emailenabled' => $emailenabled + ] + ]); +} diff --git a/message/templates/message_drawer.mustache b/message/templates/message_drawer.mustache new file mode 100644 index 00000000000..a75d721a3fe --- /dev/null +++ b/message/templates/message_drawer.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer + + This template will render the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + +{{#js}} +require(['jquery', 'core_message/message_drawer'], function($, MessageDrawer) { + var root = $('#message-drawer-{{uniqid}}'); + MessageDrawer.init(root); +}); +{{/js}} diff --git a/message/templates/message_drawer_contacts_list.mustache b/message/templates/message_drawer_contacts_list.mustache new file mode 100644 index 00000000000..8f9b3aa6a64 --- /dev/null +++ b/message/templates/message_drawer_contacts_list.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_contacts_list + + This template will render a list of contacts for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{#contacts}} + + + {{#showonlinestatus}} + + {{/showonlinestatus}} +
{{fullname}}
+
+ {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}} +
+
+{{/contacts}} \ No newline at end of file diff --git a/message/templates/message_drawer_contacts_list_item_placeholder.mustache b/message/templates/message_drawer_contacts_list_item_placeholder.mustache new file mode 100644 index 00000000000..fa3d9c51873 --- /dev/null +++ b/message/templates/message_drawer_contacts_list_item_placeholder.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_contacts_list_item_placeholder + + This template will render a placeholder loading item for a contact in + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+
+
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_conversations_list.mustache b/message/templates/message_drawer_conversations_list.mustache new file mode 100644 index 00000000000..71e8a1ef336 --- /dev/null +++ b/message/templates/message_drawer_conversations_list.mustache @@ -0,0 +1,90 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_conversations_list + + This template will render a list of conversations for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{#conversations}} + + {{#imageurl}} + + {{/imageurl}} + {{#showonlinestatus}} + + {{/showonlinestatus}} +
+
+ {{name}} + + {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}} + +
+ {{#lastmessagedate}} + {{#userdate}} {{.}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}} + {{/lastmessagedate}} +
+
+ {{#subname}} +

{{.}}

+ {{/subname}} +

+ {{#sentfromcurrentuser}}{{#str}} you, core_message {{/str}}{{/sentfromcurrentuser}} + {{lastmessage}} +

+
+ + + {{unreadcount}} + + +
+ {{> core_message/message_drawer_icon_forward }} +
+
+{{/conversations}} diff --git a/message/templates/message_drawer_conversations_list_item_placeholder.mustache b/message/templates/message_drawer_conversations_list_item_placeholder.mustache new file mode 100644 index 00000000000..b156a6cb426 --- /dev/null +++ b/message/templates/message_drawer_conversations_list_item_placeholder.mustache @@ -0,0 +1,65 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_conversations_list_item_placeholder + + This template will render a placeholder loading element for a message in + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_icon_back.mustache b/message/templates/message_drawer_icon_back.mustache new file mode 100644 index 00000000000..d9219708a87 --- /dev/null +++ b/message/templates/message_drawer_icon_back.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_icon_back + + This template will render the back icon for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +{{#pix}} i/previous, core {{/pix}} +{{#pix}} i/next, core {{/pix}} \ No newline at end of file diff --git a/message/templates/message_drawer_icon_forward.mustache b/message/templates/message_drawer_icon_forward.mustache new file mode 100644 index 00000000000..0d51eeff0f1 --- /dev/null +++ b/message/templates/message_drawer_icon_forward.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_icon_forward + + This template will render the forward icon for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +{{#pix}} i/next, core {{/pix}} +{{#pix}} i/previous, core {{/pix}} \ No newline at end of file diff --git a/message/templates/message_drawer_lazy_load_list.mustache b/message/templates/message_drawer_lazy_load_list.mustache new file mode 100644 index 00000000000..4079ed18f58 --- /dev/null +++ b/message/templates/message_drawer_lazy_load_list.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_lazy_load_list + + This template will render a lazy loaded list for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+ + +
+ {{$placeholder}}{{/placeholder}} +
+ +
diff --git a/message/templates/message_drawer_messages_list.mustache b/message/templates/message_drawer_messages_list.mustache new file mode 100644 index 00000000000..68c49532a13 --- /dev/null +++ b/message/templates/message_drawer_messages_list.mustache @@ -0,0 +1,97 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_messages_list + + This template will render a list of messages for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{#messages}} + + + {{#showonlinestatus}} + + {{/showonlinestatus}} +
+
+ + {{fullname}} + + + {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}} + + +
+ {{#lastmessagedate}} + {{#userdate}} {{.}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}} + {{/lastmessagedate}} +
+
+

+ {{#sentfromcurrentuser}}{{#str}} you, core_message {{/str}}{{/sentfromcurrentuser}} + {{lastmessage}} +

+
+ + + {{unreadcount}} + + +
+ {{> core_message/message_drawer_icon_forward }} +
+
+{{/messages}} \ No newline at end of file diff --git a/message/templates/message_drawer_messages_list_item_placeholder.mustache b/message/templates/message_drawer_messages_list_item_placeholder.mustache new file mode 100644 index 00000000000..82d060f5628 --- /dev/null +++ b/message/templates/message_drawer_messages_list_item_placeholder.mustache @@ -0,0 +1,65 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_messages_list_item_placeholder + + This template will render a placeholder loading element for a message in + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_non_contacts_list.mustache b/message/templates/message_drawer_non_contacts_list.mustache new file mode 100644 index 00000000000..f3d8ae217f4 --- /dev/null +++ b/message/templates/message_drawer_non_contacts_list.mustache @@ -0,0 +1,65 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_non_contacts_list + + This template will render a list of non contacts for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{#noncontacts}} + + +
{{fullname}}
+ {{#isblocked}} +
+ {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}} +
+ {{/isblocked}} +
+{{/noncontacts}} \ No newline at end of file diff --git a/message/templates/message_drawer_view_contact_body.mustache b/message/templates/message_drawer_view_contact_body.mustache new file mode 100644 index 00000000000..1e402d48d36 --- /dev/null +++ b/message/templates/message_drawer_view_contact_body.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contact_body + + This template will render the body section of the contact page in the + message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_contact_body_content.mustache b/message/templates/message_drawer_view_contact_body_content.mustache new file mode 100644 index 00000000000..c8514deb88e --- /dev/null +++ b/message/templates/message_drawer_view_contact_body_content.mustache @@ -0,0 +1,95 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contact_body_content + + This template will render the content for the body section of the contact + page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + + {{> core_message/message_drawer_icon_back }} + +
+
+ +
+

{{fullname}}

+
+ + + + +
+
diff --git a/message/templates/message_drawer_view_contacts_body.mustache b/message/templates/message_drawer_view_contacts_body.mustache new file mode 100644 index 00000000000..d3c2a6dddd0 --- /dev/null +++ b/message/templates/message_drawer_view_contacts_body.mustache @@ -0,0 +1,96 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contacts_body + + This template will render the body content of the contacts page in the + message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + \ No newline at end of file diff --git a/message/templates/message_drawer_view_contacts_body_section_contacts.mustache b/message/templates/message_drawer_view_contacts_body_section_contacts.mustache new file mode 100644 index 00000000000..f37a202a93b --- /dev/null +++ b/message/templates/message_drawer_view_contacts_body_section_contacts.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contacts_body_section_contacts + + This template will render the contacts section of the body of the contacts + page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +{{< core_message/message_drawer_lazy_load_list }} + {{$emptymessage}}{{#str}} nocontactsgetstarted, core_message {{/str}}{{/emptymessage}} + {{$placeholder}} + {{#contacts}} + {{#sectioncontacts}} + {{#placeholders}} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{/placeholders}} + {{/sectioncontacts}} + {{/contacts}} + {{/placeholder}} +{{/ core_message/message_drawer_lazy_load_list }} diff --git a/message/templates/message_drawer_view_contacts_body_section_requests.mustache b/message/templates/message_drawer_view_contacts_body_section_requests.mustache new file mode 100644 index 00000000000..cb532ef6444 --- /dev/null +++ b/message/templates/message_drawer_view_contacts_body_section_requests.mustache @@ -0,0 +1,81 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contacts_body_section_requests + + This template will render the the requests section of the body of the contacts + page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +{{< core_message/message_drawer_lazy_load_list }} + {{$emptymessage}}{{#str}} nocontactrequests, core_message {{/str}}{{/emptymessage}} + {{$placeholder}} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{> core_message/message_drawer_contacts_list_item_placeholder }} + {{/placeholder}} +{{/ core_message/message_drawer_lazy_load_list }} diff --git a/message/templates/message_drawer_view_contacts_body_section_requests_list.mustache b/message/templates/message_drawer_view_contacts_body_section_requests_list.mustache new file mode 100644 index 00000000000..17a9eeaee66 --- /dev/null +++ b/message/templates/message_drawer_view_contacts_body_section_requests_list.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_contacts_list + + This template will render a list of contacts for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{#requests}} + + +
+
+ + {{fullname}} + +
+

+ {{#str}} wouldliketocontactyou, core_message {{/str}} +

+
+
+{{/requests}} \ No newline at end of file diff --git a/message/templates/message_drawer_view_contacts_header.mustache b/message/templates/message_drawer_view_contacts_header.mustache new file mode 100644 index 00000000000..b4634e462e3 --- /dev/null +++ b/message/templates/message_drawer_view_contacts_header.mustache @@ -0,0 +1,53 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contacts_header + + This template will render the header for the contacts page of + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + \ No newline at end of file diff --git a/message/templates/message_drawer_view_contacts_section.mustache b/message/templates/message_drawer_view_contacts_section.mustache new file mode 100644 index 00000000000..87474facf9f --- /dev/null +++ b/message/templates/message_drawer_view_contacts_section.mustache @@ -0,0 +1,56 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contacts_section + + This template will render a generic "section" on the contacts page of + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+
+ + +
+ {{$placeholder}}{{/placeholder}} +
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_body.mustache b/message/templates/message_drawer_view_conversation_body.mustache new file mode 100644 index 00000000000..a87e91527b0 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_body.mustache @@ -0,0 +1,60 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_conversation_body + + This template will render the body container for the conversation page in + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * + + Example context (json): + {} + +}} + + diff --git a/message/templates/message_drawer_view_conversation_body_confirm_dialogue.mustache b/message/templates/message_drawer_view_conversation_body_confirm_dialogue.mustache new file mode 100644 index 00000000000..8730e8f5572 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_body_confirm_dialogue.mustache @@ -0,0 +1,78 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_body_confirm_dialogue + + This template will render the confirmation dialogue inside the conversation page of + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + diff --git a/message/templates/message_drawer_view_conversation_body_day.mustache b/message/templates/message_drawer_view_conversation_body_day.mustache new file mode 100644 index 00000000000..52baec8c0fd --- /dev/null +++ b/message/templates/message_drawer_view_conversation_body_day.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_body_day + + This template will render a day's worth of messages in the body of the + conversation page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
{{#userdate}} {{timestamp}}, {{#str}} strftimedateshort, core_langconfig {{/str}} {{/userdate}}
+ {{> core_message/message_drawer_view_conversation_body_messages }} +
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_body_day_placeholder.mustache b/message/templates/message_drawer_view_conversation_body_day_placeholder.mustache new file mode 100644 index 00000000000..032cc3ad64b --- /dev/null +++ b/message/templates/message_drawer_view_conversation_body_day_placeholder.mustache @@ -0,0 +1,83 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_covnersation_body_day_placeholder + + This template will render the loading placeholder elements for a day of messages + in the conversation page of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_body_message.mustache b/message/templates/message_drawer_view_conversation_body_message.mustache new file mode 100644 index 00000000000..c6ef486f42f --- /dev/null +++ b/message/templates/message_drawer_view_conversation_body_message.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_body_message + + This template will render a single message for the body of the conversation page + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + \ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_body_messages.mustache b/message/templates/message_drawer_view_conversation_body_messages.mustache new file mode 100644 index 00000000000..b87ecd80358 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_body_messages.mustache @@ -0,0 +1,41 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_body_messages + + This template will render the list of messages in the body of the conversation + page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+ {{#messages}} + {{> core_message/message_drawer_view_conversation_body_message }} + {{/messages}} +
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_body_placeholder.mustache b/message/templates/message_drawer_view_conversation_body_placeholder.mustache new file mode 100644 index 00000000000..337c26eba95 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_body_placeholder.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_body_placeholder + + This template will render the loading placeholder elements for the body of + the conversation page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+ {{> core_message/message_drawer_view_conversation_body_day_placeholder }} + {{> core_message/message_drawer_view_conversation_body_day_placeholder }} + {{> core_message/message_drawer_view_conversation_body_day_placeholder }} + {{> core_message/message_drawer_view_conversation_body_day_placeholder }} + {{> core_message/message_drawer_view_conversation_body_day_placeholder }} +
+
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_footer.mustache b/message/templates/message_drawer_view_conversation_footer.mustache new file mode 100644 index 00000000000..585678a017c --- /dev/null +++ b/message/templates/message_drawer_view_conversation_footer.mustache @@ -0,0 +1,66 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_footer + + This template will render the footer container for the conversation page + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_footer_content.mustache b/message/templates/message_drawer_view_conversation_footer_content.mustache new file mode 100644 index 00000000000..9d429e81d7f --- /dev/null +++ b/message/templates/message_drawer_view_conversation_footer_content.mustache @@ -0,0 +1,59 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_covnersation_footer_content + + This template will render the footer content for the conversation page in + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+ + +
diff --git a/message/templates/message_drawer_view_conversation_footer_edit_mode.mustache b/message/templates/message_drawer_view_conversation_footer_edit_mode.mustache new file mode 100644 index 00000000000..c1f2bdea6b2 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_footer_edit_mode.mustache @@ -0,0 +1,50 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_footer_edit_mode + + This template will render the foot while the conversation page is in edit mode + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+ +
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_footer_placeholder.mustache b/message/templates/message_drawer_view_conversation_footer_placeholder.mustache new file mode 100644 index 00000000000..b05821271aa --- /dev/null +++ b/message/templates/message_drawer_view_conversation_footer_placeholder.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_footer_placeholder + + This template will render the loading placeholder elements in the footer of + the conversation page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_footer_require_contact.mustache b/message/templates/message_drawer_view_conversation_footer_require_contact.mustache new file mode 100644 index 00000000000..139d531ad50 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_footer_require_contact.mustache @@ -0,0 +1,45 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_footer_require_contact + + This template will render the footer when the user must add the other user as + a contact in a conversation in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+

+

+ +
diff --git a/message/templates/message_drawer_view_conversation_footer_require_unblock.mustache b/message/templates/message_drawer_view_conversation_footer_require_unblock.mustache new file mode 100644 index 00000000000..1902ef322e3 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_footer_require_unblock.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_footer_require_unblock + + This template will render the footer content when the user must unblock the other + user in a conversation in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+

{{#str}} youhaveblockeduser, core_message {{/str}}

+ +
diff --git a/message/templates/message_drawer_view_conversation_footer_unable_to_message.mustache b/message/templates/message_drawer_view_conversation_footer_unable_to_message.mustache new file mode 100644 index 00000000000..8a203a22554 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_footer_unable_to_message.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_unable_to_message + + This template will render the footer content when the user is unable to message + in a conversation in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+

{{#str}} unabletomessage, core_message {{/str}}

+
diff --git a/message/templates/message_drawer_view_conversation_header.mustache b/message/templates/message_drawer_view_conversation_header.mustache new file mode 100644 index 00000000000..64d2039c04d --- /dev/null +++ b/message/templates/message_drawer_view_conversation_header.mustache @@ -0,0 +1,54 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_header + + This template will render the header for the conversation page of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_header_content_type_private.mustache b/message/templates/message_drawer_view_conversation_header_content_type_private.mustache new file mode 100644 index 00000000000..fb428de7f96 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_header_content_type_private.mustache @@ -0,0 +1,104 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_heeader_content + + This template will render the header content of the conversation page in + the message message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache b/message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache new file mode 100644 index 00000000000..e2390e5fd05 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_heeader_content_type_private_no_controls + + This template will render the header content of the conversation page without controls in + the message message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+ +
+ {{#imageurl}} +
+ +
+ {{/imageurl}} +
+
+ {{name}} + + {{#pix}} i/star-rating, core {{/pix}} + +
+ {{#showonlinestatus}} +

+ {{#isonline}}{{#str}} loggedin, core_message {{/str}}{{/isonline}} + {{^isonline}}{{#str}} loggedoff, core_message {{/str}}{{/isonline}} +

+ {{/showonlinestatus}} +
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_header_content_type_public.mustache b/message/templates/message_drawer_view_conversation_header_content_type_public.mustache new file mode 100644 index 00000000000..69ecc3f3692 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_header_content_type_public.mustache @@ -0,0 +1,87 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_heeader_content + + This template will render the header content of the conversation page in + the message message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_conversation_header_edit_mode.mustache b/message/templates/message_drawer_view_conversation_header_edit_mode.mustache new file mode 100644 index 00000000000..3470764c456 --- /dev/null +++ b/message/templates/message_drawer_view_conversation_header_edit_mode.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_header_edit_mode + + This template will render the header while the conversation is in edit mode on + the conversation page of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+ {{#str}} messagesselected:, core_message {{/str}} + 1 + +
diff --git a/message/templates/message_drawer_view_conversation_header_placeholder.mustache b/message/templates/message_drawer_view_conversation_header_placeholder.mustache new file mode 100644 index 00000000000..1327ec1979a --- /dev/null +++ b/message/templates/message_drawer_view_conversation_header_placeholder.mustache @@ -0,0 +1,60 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_header_placeholder + + This template will render the loading placeholder elements for the header + of the conversation page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + diff --git a/message/templates/message_drawer_view_group_info_body.mustache b/message/templates/message_drawer_view_group_info_body.mustache new file mode 100644 index 00000000000..09f74ef3ad4 --- /dev/null +++ b/message/templates/message_drawer_view_group_info_body.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_group_info_body + + This template will render the body section of the group info page in the + message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_group_info_body_content.mustache b/message/templates/message_drawer_view_group_info_body_content.mustache new file mode 100644 index 00000000000..1c2bd8930cd --- /dev/null +++ b/message/templates/message_drawer_view_group_info_body_content.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_group_info_body_content + + This template will render the content for the body section of the group + info page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + + {{> core_message/message_drawer_icon_back }} + +
+
+ {{#imageurl}} + + {{/imageurl}} +
+

{{name}}

+ {{#subname}}

{{.}}

{{/subname}} +
+

{{#str}} participants, core_message {{/str}}

+
+ {{< core_message/message_drawer_lazy_load_list }} + {{$rootattributes}} + data-region="members-list" + {{/rootattributes}} + {{$emptymessage}}{{#str}} noparticipants, core_message {{/str}}{{/emptymessage}} + {{$placeholder}} + {{#placeholders}} + {{> core_message/message_drawer_view_group_info_participants_list_item_placeholder }} + {{/placeholders}} + {{/placeholder}} + {{/ core_message/message_drawer_lazy_load_list }} +
diff --git a/message/templates/message_drawer_view_group_info_participants_list.mustache b/message/templates/message_drawer_view_group_info_participants_list.mustache new file mode 100644 index 00000000000..c863e35e3c9 --- /dev/null +++ b/message/templates/message_drawer_view_group_info_participants_list.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_group_info_participants_list + + This template will render a list of contacts for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{#contacts}} + + + {{#showonlinestatus}} + + {{/showonlinestatus}} +
{{fullname}}
+
+ {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}} +
+
+{{/contacts}} \ No newline at end of file diff --git a/message/templates/message_drawer_view_group_info_participants_list_item_placeholder.mustache b/message/templates/message_drawer_view_group_info_participants_list_item_placeholder.mustache new file mode 100644 index 00000000000..c1fc391a2aa --- /dev/null +++ b/message/templates/message_drawer_view_group_info_participants_list_item_placeholder.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_group_info_participants_list_item_placeholder + + This template will render a placeholder loading item for a contact in + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+
+
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_view_overview_body.mustache b/message/templates/message_drawer_view_overview_body.mustache new file mode 100644 index 00000000000..6c93a13bccb --- /dev/null +++ b/message/templates/message_drawer_view_overview_body.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_body + + This template will render the body of the overview section of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
+
+ {{#overview.favourites}} + {{> core_message/message_drawer_view_overview_section_favourites }} + {{/overview.favourites}} + {{#overview.groupmessages}} + {{> core_message/message_drawer_view_overview_section_group_messages }} + {{/overview.groupmessages}} + {{#overview.messages}} + {{> core_message/message_drawer_view_overview_section_messages }} + {{/overview.messages}} +
+
diff --git a/message/templates/message_drawer_view_overview_header.mustache b/message/templates/message_drawer_view_overview_header.mustache new file mode 100644 index 00000000000..7779456a806 --- /dev/null +++ b/message/templates/message_drawer_view_overview_header.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_header + + This template will render the header for the overview page of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + diff --git a/message/templates/message_drawer_view_overview_section.mustache b/message/templates/message_drawer_view_overview_section.mustache new file mode 100644 index 00000000000..2b1e9601950 --- /dev/null +++ b/message/templates/message_drawer_view_overview_section.mustache @@ -0,0 +1,73 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_section + + This template is a base template to render a collapsible "section" on the + overview page of the message drawer, for example the messages section. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+
+ +
+ {{< core_message/message_drawer_lazy_load_list }} + {{$rootclasses}}collapse border-bottom {{#expanded}}show{{/expanded}}{{/rootclasses}} + {{$rootattributes}} + id="{{$region}}{{/region}}-target" + aria-labelledby="{{$region}}{{/region}}-toggle" + data-parent="#message-drawer-view-overview-container" + {{/rootattributes}} + {{/ core_message/message_drawer_lazy_load_list }} +
\ No newline at end of file diff --git a/message/templates/message_drawer_view_overview_section_favourites.mustache b/message/templates/message_drawer_view_overview_section_favourites.mustache new file mode 100644 index 00000000000..e0bf51aaf3e --- /dev/null +++ b/message/templates/message_drawer_view_overview_section_favourites.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_section_favourites + + This template will render the favourites section of the overview page + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{< core_message/message_drawer_view_overview_section }} + {{$region}}view-overview-favourites{{/region}} + {{$title}}{{#str}} favourites {{/str}}{{/title}} + {{$placeholder}} + {{#placeholders}} + {{> core_message/message_drawer_conversations_list_item_placeholder }} + {{/placeholders}} + {{/placeholder}} + {{$emptymessage}} +

{{#str}} nofavourites, core_message {{/str}}

+ {{/emptymessage}} +{{/ core_message/message_drawer_view_overview_section }} \ No newline at end of file diff --git a/message/templates/message_drawer_view_overview_section_group_messages.mustache b/message/templates/message_drawer_view_overview_section_group_messages.mustache new file mode 100644 index 00000000000..25b5cad26ce --- /dev/null +++ b/message/templates/message_drawer_view_overview_section_group_messages.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_section_group_mesages + + This template will render the group messages section of the overview page + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{< core_message/message_drawer_view_overview_section }} + {{$region}}view-overview-group-messages{{/region}} + {{$title}}{{#str}} groupmessages, core_message {{/str}}{{/title}} + {{$placeholder}} + {{#placeholders}} + {{> core_message/message_drawer_conversations_list_item_placeholder }} + {{/placeholders}} + {{/placeholder}} + {{$emptymessage}} +

{{#str}} nogroupmessages, core_message {{/str}}

+ {{/emptymessage}} +{{/ core_message/message_drawer_view_overview_section }} \ No newline at end of file diff --git a/message/templates/message_drawer_view_overview_section_messages.mustache b/message/templates/message_drawer_view_overview_section_messages.mustache new file mode 100644 index 00000000000..dddfc113795 --- /dev/null +++ b/message/templates/message_drawer_view_overview_section_messages.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_section_messages + + This template will render the messages section of the overview page + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +{{< core_message/message_drawer_view_overview_section }} + {{$region}}view-overview-messages{{/region}} + {{$title}}{{#str}} messages, core_message {{/str}}{{/title}} + {{$placeholder}} + {{#placeholders}} + {{> core_message/message_drawer_conversations_list_item_placeholder }} + {{/placeholders}} + {{/placeholder}} + {{$emptymessage}} +

{{#str}} nomessages, core_message {{/str}}

+ {{/emptymessage}} +{{/ core_message/message_drawer_view_overview_section }} diff --git a/message/templates/message_drawer_view_search_body.mustache b/message/templates/message_drawer_view_search_body.mustache new file mode 100644 index 00000000000..26cf5f04399 --- /dev/null +++ b/message/templates/message_drawer_view_search_body.mustache @@ -0,0 +1,55 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_search_body + + This template will render the body of the search page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_search_header.mustache b/message/templates/message_drawer_view_search_header.mustache new file mode 100644 index 00000000000..8ced5316cd6 --- /dev/null +++ b/message/templates/message_drawer_view_search_header.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_search_header + + This template will render the header of the search page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_search_results_content.mustache b/message/templates/message_drawer_view_search_results_content.mustache new file mode 100644 index 00000000000..624ee128b76 --- /dev/null +++ b/message/templates/message_drawer_view_search_results_content.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_search_results_content + + This template will render the search results content in the search page + of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + +
+
+
+

{{#str}} contacts, core_message {{/str}}

+
+ +
+
+

{{#str}} noncontacts, core_message {{/str}}

+
+ +
+
+ +
+
+
+
+

{{#str}} messages, core_message {{/str}}

+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/message/templates/message_drawer_view_search_results_content_placeholder.mustache b/message/templates/message_drawer_view_search_results_content_placeholder.mustache new file mode 100644 index 00000000000..33eb1397c8c --- /dev/null +++ b/message/templates/message_drawer_view_search_results_content_placeholder.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_search_results_content_placeholder + + This template will render the loading placeholder elements while the search + content is being loaded in the search page of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} +
{{> core/loading }}
diff --git a/message/templates/message_drawer_view_settings_body.mustache b/message/templates/message_drawer_view_settings_body.mustache new file mode 100644 index 00000000000..74a838c2abe --- /dev/null +++ b/message/templates/message_drawer_view_settings_body.mustache @@ -0,0 +1,80 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_settings_body + + This template will render the body of the settings page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/message/templates/message_drawer_view_settings_header.mustache b/message/templates/message_drawer_view_settings_header.mustache new file mode 100644 index 00000000000..3411da49aff --- /dev/null +++ b/message/templates/message_drawer_view_settings_header.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_settings_header + + This template will render the header for the settings in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 02208db7c63..9eca2455851 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2168,15 +2168,12 @@ $switch-transition: .2s all !default; position: relative; input { - position: absolute; - height: 1px; + float: left; width: 1px; - background: none; - border: 0; - clip: rect(0 0 0 0); - clip-path: inset(50%); - overflow: hidden; + transform: translateX(1px); padding: 0; + margin: 0; + opacity: 0; + label { position: relative; diff --git a/theme/boost/scss/moodle/message.scss b/theme/boost/scss/moodle/message.scss index 11be4f635ce..0f173587068 100644 --- a/theme/boost/scss/moodle/message.scss +++ b/theme/boost/scss/moodle/message.scss @@ -1191,6 +1191,189 @@ } } +// New styles for the messaging UI. Once MDL-63303 is done all CSS above this line should be removed. +$message-drawer-width: 320px; + +.message-drawer { + position: fixed; + top: $navbar-height; + right: 0; + height: calc(100% - #{$navbar-height}); + width: $message-drawer-width; + z-index: $zindex-fixed; + box-shadow: -2px 2px 4px rgba(0, 0, 0, .08); + display: flex; + flex-direction: column; + @include transition(); + + &.hidden { + display: block; + right: $message-drawer-width * -1; + } + + .header-container { + flex-shrink: 0; + } + + .body-container { + flex: 1; + overflow: hidden; + + & > * { + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; + opacity: 1; + @include transition(); + + &.hidden { + display: block; + left: $message-drawer-width; + right: $message-drawer-width * -1; + opacity: 0; + visibility: hidden; + + &.previous { + left: $message-drawer-width * -1; + right: $message-drawer-width; + } + } + } + } + + .footer-container { + flex-shrink: 0; + overflow-x: hidden; + + & > * { + max-height: 2000px; + opacity: 1; + @include transition(); + + &.hidden { + display: block; + max-height: 0; + opacity: 0; + padding: 0 !important; /* stylelint-disable-line declaration-no-important */ + border: 0 !important; /* stylelint-disable-line declaration-no-important */ + visibility: hidden; + transform: translate(#{$message-drawer-width}); + transition: all .2s ease-in-out, max-height .2s .2s ease-in-out; + + &.previous { + transform: translate(#{($message-drawer-width * -1)}); + } + } + } + } + + .matchtext { + background-color: lighten($primary, 40%); + color: $body-color; + height: 1.5rem; + } + .contact-status { + position: absolute; + left: 39px; + top: 39px; + width: 10px; + height: 10px; + border-radius: 50%; + &.online { + border: 1px solid $body-bg; + background-color: $green; + } + } + + .message { + p { + margin: 0; + } + } + + .clickable { + cursor: pointer; + + &:hover { + box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.05), 3px 3px 5px -2px rgba(0, 0, 0, .1), 1px 1px 5px 0 rgba(0, 0, 0, 0.03); + } + } + + a, + .btn-link { + color: inherit; + } + + .icon { + margin-right: 0; + } + + .overview-section-toggle { + .collapsed-icon-container { + display: none; + } + .expanded-icon-container { + display: inline-block; + } + + &.collapsed { + .collapsed-icon-container { + display: inline-block; + } + .expanded-icon-container { + display: none; + } + } + } + + .btn.btn-link.btn-icon { + height: $icon-width; + width: $icon-width; + padding: 0; + border-radius: 50%; + flex-shrink: 0; + + @include hover-focus { + background-color: $gray-200; + } + + @each $size, $length in $iconsizes { + &.icon-size-#{$size} { + height: ($length + 20px) !important; /* stylelint-disable-line declaration-no-important */ + width: ($length + 20px) !important; /* stylelint-disable-line declaration-no-important */ + } + } + } + + .view-overview-body { + .section { + display: block; + + &.expanded { + display: flex; + } + } + } + + .view-conversation { + .content-message-container { + img { + max-width: 100%; + } + } + } +} + +.dir-rtl { + .message-drawer { + box-shadow: 2px 2px 4px rgba(0, 0, 0, .08); + } +} + +// New styles for the messaging UI. Once MDL-63303 is done all CSS below this line should be removed. + @media (max-width: 480px) { .messaging-area-container { .messaging-area { diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index b45f320579e..29a506c8c34 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -10471,15 +10471,12 @@ div.editor_atto_toolbar button .icon { .switch { position: relative; } .switch input { - position: absolute; - height: 1px; + float: left; width: 1px; - background: none; - border: 0; - clip: rect(0 0 0 0); - clip-path: inset(50%); - overflow: hidden; - padding: 0; } + transform: translateX(1px); + padding: 0; + margin: 0; + opacity: 0; } .switch input + label { position: relative; min-height: 1.725rem; @@ -14074,6 +14071,143 @@ a.ygtvspacer:hover { visibility: visible; transition: right 0.25s; } } +.message-drawer { + position: fixed; + top: 50px; + right: 0; + height: calc(100% - 50px); + width: 320px; + z-index: 1030; + box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.08); + display: flex; + flex-direction: column; + transition: all 0.2s ease-in-out; } + .message-drawer.hidden { + display: block; + right: -320px; } + .message-drawer .header-container { + flex-shrink: 0; } + .message-drawer .body-container { + flex: 1; + overflow: hidden; } + .message-drawer .body-container > * { + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; + opacity: 1; + transition: all 0.2s ease-in-out; } + .message-drawer .body-container > *.hidden { + display: block; + left: 320px; + right: -320px; + opacity: 0; + visibility: hidden; } + .message-drawer .body-container > *.hidden.previous { + left: -320px; + right: 320px; } + .message-drawer .footer-container { + flex-shrink: 0; + overflow-x: hidden; } + .message-drawer .footer-container > * { + max-height: 2000px; + opacity: 1; + transition: all 0.2s ease-in-out; } + .message-drawer .footer-container > *.hidden { + display: block; + max-height: 0; + opacity: 0; + padding: 0 !important; + /* stylelint-disable-line declaration-no-important */ + border: 0 !important; + /* stylelint-disable-line declaration-no-important */ + visibility: hidden; + transform: translate(320px); + transition: all .2s ease-in-out, max-height .2s .2s ease-in-out; } + .message-drawer .footer-container > *.hidden.previous { + transform: translate(-320px); } + .message-drawer .matchtext { + background-color: #b5d9f9; + color: #373a3c; + height: 1.5rem; } + .message-drawer .contact-status { + position: absolute; + left: 39px; + top: 39px; + width: 10px; + height: 10px; + border-radius: 50%; } + .message-drawer .contact-status.online { + border: 1px solid #fff; + background-color: #5cb85c; } + .message-drawer .message p { + margin: 0; } + .message-drawer .clickable { + cursor: pointer; } + .message-drawer .clickable:hover { + box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.05), 3px 3px 5px -2px rgba(0, 0, 0, 0.1), 1px 1px 5px 0 rgba(0, 0, 0, 0.03); } + .message-drawer a, + .message-drawer .btn-link { + color: inherit; } + .message-drawer .icon { + margin-right: 0; } + .message-drawer .overview-section-toggle .collapsed-icon-container { + display: none; } + .message-drawer .overview-section-toggle .expanded-icon-container { + display: inline-block; } + .message-drawer .overview-section-toggle.collapsed .collapsed-icon-container { + display: inline-block; } + .message-drawer .overview-section-toggle.collapsed .expanded-icon-container { + display: none; } + .message-drawer .btn.btn-link.btn-icon, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.action, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.action, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.addcriterion, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.addcriterion { + height: 16px; + width: 16px; + padding: 0; + border-radius: 50%; + flex-shrink: 0; } + .message-drawer .btn.btn-link.btn-icon:hover, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.action:hover, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.action:hover, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon:hover, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon:hover, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.addcriterion:hover, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.addcriterion:hover, .message-drawer .btn.btn-link.btn-icon:focus, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.action:focus, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.action:focus, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon:focus, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon:focus, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.addcriterion:focus, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.addcriterion:focus { + background-color: #e9ecef; } + .message-drawer .btn.btn-link.btn-icon.icon-size-0, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.icon-size-0.action, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.icon-size-0.action, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon.icon-size-0, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon.icon-size-0, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.icon-size-0.addcriterion, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.icon-size-0.addcriterion { + height: 20px !important; + /* stylelint-disable-line declaration-no-important */ + width: 20px !important; + /* stylelint-disable-line declaration-no-important */ } + .message-drawer .btn.btn-link.btn-icon.icon-size-1, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.icon-size-1.action, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.icon-size-1.action, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon.icon-size-1, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon.icon-size-1, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.icon-size-1.addcriterion, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.icon-size-1.addcriterion { + height: 24px !important; + /* stylelint-disable-line declaration-no-important */ + width: 24px !important; + /* stylelint-disable-line declaration-no-important */ } + .message-drawer .btn.btn-link.btn-icon.icon-size-2, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.icon-size-2.action, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.icon-size-2.action, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon.icon-size-2, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon.icon-size-2, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.icon-size-2.addcriterion, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.icon-size-2.addcriterion { + height: 28px !important; + /* stylelint-disable-line declaration-no-important */ + width: 28px !important; + /* stylelint-disable-line declaration-no-important */ } + .message-drawer .btn.btn-link.btn-icon.icon-size-3, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.icon-size-3.action, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.icon-size-3.action, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon.icon-size-3, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon.icon-size-3, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.icon-size-3.addcriterion, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.icon-size-3.addcriterion { + height: 36px !important; + /* stylelint-disable-line declaration-no-important */ + width: 36px !important; + /* stylelint-disable-line declaration-no-important */ } + .message-drawer .btn.btn-link.btn-icon.icon-size-4, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.icon-size-4.action, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.icon-size-4.action, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon.icon-size-4, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon.icon-size-4, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.icon-size-4.addcriterion, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.icon-size-4.addcriterion { + height: 44px !important; + /* stylelint-disable-line declaration-no-important */ + width: 44px !important; + /* stylelint-disable-line declaration-no-important */ } + .message-drawer .btn.btn-link.btn-icon.icon-size-5, .message-drawer #page-grade-grading-manage .actions .btn-link.btn-icon.icon-size-5.action, #page-grade-grading-manage .actions .message-drawer .btn-link.btn-icon.icon-size-5.action, .message-drawer #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel input.btn-link.btn-icon.icon-size-5, #rubric-rubric.gradingform_rubric #rubric-criteria .criterion .addlevel .message-drawer input.btn-link.btn-icon.icon-size-5, .message-drawer #rubric-rubric.gradingform_rubric .btn-link.btn-icon.icon-size-5.addcriterion, #rubric-rubric.gradingform_rubric .message-drawer .btn-link.btn-icon.icon-size-5.addcriterion { + height: 68px !important; + /* stylelint-disable-line declaration-no-important */ + width: 68px !important; + /* stylelint-disable-line declaration-no-important */ } + .message-drawer .view-overview-body .section { + display: block; } + .message-drawer .view-overview-body .section.expanded { + display: flex; } + .message-drawer .view-conversation .content-message-container img { + max-width: 100%; } + +.dir-rtl .message-drawer { + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.08); } + @media (max-width: 480px) { .messaging-area-container .messaging-area .messages-area.editing .messages-header { height: 80px; } From 6f96c063744227ba93735c06effe743def7e622f Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Fri, 26 Oct 2018 14:07:56 +0800 Subject: [PATCH 22/31] MDL-63303 theme_bootstrapbase: add message drawer --- ..._view_conversation_footer_content.mustache | 2 +- .../bootstrapbase/less/moodle/bs4-compat.less | 30 ++- theme/bootstrapbase/less/moodle/message.less | 185 ++++++++++++++++++ theme/bootstrapbase/less/moodle/user.less | 2 +- theme/bootstrapbase/style/moodle.css | 182 ++++++++++++++++- .../core_message/message_drawer.mustache | 93 +++++++++ .../message_drawer_icon_back.mustache | 40 ++++ .../message_drawer_icon_forward.mustache | 40 ++++ ...message_drawer_view_contacts_body.mustache | 101 ++++++++++ ...er_view_conversation_body_message.mustache | 74 +++++++ ..._view_conversation_footer_content.mustache | 62 ++++++ ...iew_conversation_footer_edit_mode.mustache | 53 +++++ ...ation_header_content_type_private.mustache | 125 ++++++++++++ ..._content_type_private_no_controls.mustache | 76 +++++++ ...sation_header_content_type_public.mustache | 97 +++++++++ ...awer_view_group_info_body_content.mustache | 70 +++++++ ...ssage_drawer_view_overview_header.mustache | 74 +++++++ ...sage_drawer_view_overview_section.mustache | 76 +++++++ ...message_drawer_view_search_header.mustache | 74 +++++++ 19 files changed, 1451 insertions(+), 5 deletions(-) create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_icon_back.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_icon_forward.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_contacts_body.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_body_message.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_content.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_edit_mode.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private_no_controls.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_group_info_body_content.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_overview_header.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache create mode 100644 theme/bootstrapbase/templates/core_message/message_drawer_view_search_header.mustache diff --git a/message/templates/message_drawer_view_conversation_footer_content.mustache b/message/templates/message_drawer_view_conversation_footer_content.mustache index 9d429e81d7f..a26501dc9b4 100644 --- a/message/templates/message_drawer_view_conversation_footer_content.mustache +++ b/message/templates/message_drawer_view_conversation_footer_content.mustache @@ -15,7 +15,7 @@ along with Moodle. If not, see . }} {{! - @template core_message/message_drawer_view_covnersation_footer_content + @template core_message/message_drawer_view_conversation_footer_content This template will render the footer content for the conversation page in the message drawer. diff --git a/theme/bootstrapbase/less/moodle/bs4-compat.less b/theme/bootstrapbase/less/moodle/bs4-compat.less index fb8802ab672..1645a32276e 100644 --- a/theme/bootstrapbase/less/moodle/bs4-compat.less +++ b/theme/bootstrapbase/less/moodle/bs4-compat.less @@ -256,7 +256,10 @@ padding-bottom: @baseFontSize * 1.5 !important; } - +.ml-0, +.mx-0 { + margin-left: 0; +} .ml-1, .mx-1 { margin-left: @baseFontSize / 4 !important; @@ -273,6 +276,10 @@ .mx-4 { margin-left: @baseFontSize * 1.5 !important; } +.mr-0, +.mx-0 { + margin-right: 0; +} .mr-1, .mx-1 { margin-right: @baseFontSize / 4 !important; @@ -289,6 +296,10 @@ .mx-4 { margin-right: @baseFontSize * 1.5 !important; } +.mb-0, +.my-0 { + margin-bottom: 0; +} .mb-1, .my-1 { margin-bottom: @baseFontSize / 4 !important; @@ -305,6 +316,10 @@ .my-4 { margin-bottom: @baseFontSize * 1.5 !important; } +.mt-0, +.my-0 { + margin-top: 0; +} .mt-1, .my-1 { margin-top: @baseFontSize / 4 !important; @@ -322,7 +337,6 @@ margin-top: @baseFontSize * 1.5 !important; } - .ml-auto, .mx-auto { margin-left: auto !important; @@ -591,6 +605,7 @@ } .list-group-item { background-color: @white; + position: relative; } .list-group-item-action { &:hover, @@ -616,3 +631,14 @@ } } +.dir-rtl { + .dir-rtl-hide { + display: none; + } +} + +.dir-ltr { + .dir-ltr-hide { + display: none; + } +} diff --git a/theme/bootstrapbase/less/moodle/message.less b/theme/bootstrapbase/less/moodle/message.less index 70aa519cdc2..961ff644033 100644 --- a/theme/bootstrapbase/less/moodle/message.less +++ b/theme/bootstrapbase/less/moodle/message.less @@ -1178,6 +1178,191 @@ } } +@message-drawer-width: 320px; + +.message-drawer { + position: fixed; + top: 0; + height: 100%; + right: 0; + width: @message-drawer-width; + z-index: 999; + background-color: @white; + box-shadow: -2px 2px 4px rgba(0, 0, 0, .08); + display: flex; + flex-direction: column; + + @media (min-width: 980px) { + .drawer-top { + display: none; + } + height: ~"calc(100% - 42px)"; + top: 42px; + } + + .transition(all .2s ease-in-out); + + &.hidden { + display: block; + right: @message-drawer-width * -1; + } + + .header-container { + flex-shrink: 0; + } + + .searchinput { + box-shadow: none; + width: 100%; + } + + [data-region="confirm-dialogue-container"] { + box-sizing: border-box; + * { + box-sizing: border-box; + } + .btn-block { + margin-left: 0; + } + } + + .body-container { + flex: 1; + overflow: hidden; + + & > * { + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; + opacity: 1; + .transition(all .2s ease-in-out); + + &.hidden { + display: block; + left: @message-drawer-width; + right: @message-drawer-width * -1; + opacity: 0; + visibility: hidden; + + &.previous { + left: @message-drawer-width * -1; + right: @message-drawer-width; + } + } + } + } + + .footer-container { + flex-shrink: 0; + overflow-x: hidden; + + & > * { + max-height: 2000px; + opacity: 1; + .transition(all .2s ease-in-out); + + &.hidden { + display: block; + max-height: 0; + opacity: 0; + padding: 0 !important; /* stylelint-disable-line declaration-no-important */ + border: 0 !important; /* stylelint-disable-line declaration-no-important */ + visibility: hidden; + transform: translate(@message-drawer-width); + transition: all .2s ease-in-out, max-height .2s .2s ease-in-out; + + &.previous { + transform: translate(@message-drawer-width * -1); + } + } + } + } + + .matchtext { + background-color: lighten(@blue, 40%); + color: @textColor; + height: 1.5rem; + } + + .contact-status { + position: absolute; + left: 39px; + top: 39px; + width: 10px; + height: 10px; + border-radius: 50%; + &.online { + border: 1px solid @green; + background-color: @green; + } + } + + a, + .btn-link { + text-decoration: none; + color: inherit; + } + + .message { + p { + margin: 0; + } + } + + .clickable { + cursor: pointer; + + &:hover { + box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.05), 3px 3px 5px -2px rgba(0, 0, 0, .1), 1px 1px 5px 0 rgba(0, 0, 0, 0.03); + } + } + + h6, + .h6 { + font-size: 14px; + font-weight: normal; + } + + .overview-section-toggle { + .collapsed-icon-container { + display: none; + } + .expanded-icon-container { + display: inline-block; + } + + &.collapsed { + .collapsed-icon-container { + display: inline-block; + } + .expanded-icon-container { + display: none; + } + } + } + + .view-overview-body { + .section { + display: block; + + &.expanded { + display: flex; + flex-direction: column; + } + } + } + + .view-conversation { + .content-message-container { + img { + max-width: 100%; + } + } + } +} + @media (max-width: 480px) { .messaging-area-container { .messaging-area { diff --git a/theme/bootstrapbase/less/moodle/user.less b/theme/bootstrapbase/less/moodle/user.less index 2df2277b1b6..a4afb237f75 100644 --- a/theme/bootstrapbase/less/moodle/user.less +++ b/theme/bootstrapbase/less/moodle/user.less @@ -199,7 +199,7 @@ position: relative; h3 { - margin-top: 0px; + margin-top: 0; } } diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index 64d8d3c99f1..2261b8a3f72 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -9094,6 +9094,163 @@ a.ygtvspacer:hover { height: 500px; } } +.message-drawer { + position: fixed; + top: 0; + height: 100%; + right: 0; + width: 320px; + z-index: 999; + background-color: #fff; + box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.08); + display: flex; + flex-direction: column; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +@media (min-width: 980px) { + .message-drawer { + height: calc(100% - 42px); + top: 42px; + } + .message-drawer .drawer-top { + display: none; + } +} +.message-drawer.hidden { + display: block; + right: -320px; +} +.message-drawer .header-container { + flex-shrink: 0; +} +.message-drawer .searchinput { + box-shadow: none; + width: 100%; +} +.message-drawer [data-region="confirm-dialogue-container"] { + box-sizing: border-box; +} +.message-drawer [data-region="confirm-dialogue-container"] * { + box-sizing: border-box; +} +.message-drawer [data-region="confirm-dialogue-container"] .btn-block { + margin-left: 0; +} +.message-drawer .body-container { + flex: 1; + overflow: hidden; +} +.message-drawer .body-container > * { + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; + opacity: 1; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.message-drawer .body-container > *.hidden { + display: block; + left: 320px; + right: -320px; + opacity: 0; + visibility: hidden; +} +.message-drawer .body-container > *.hidden.previous { + left: -320px; + right: 320px; +} +.message-drawer .footer-container { + flex-shrink: 0; + overflow-x: hidden; +} +.message-drawer .footer-container > * { + max-height: 2000px; + opacity: 1; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.message-drawer .footer-container > *.hidden { + display: block; + max-height: 0; + opacity: 0; + padding: 0 !important; + /* stylelint-disable-line declaration-no-important */ + border: 0 !important; + /* stylelint-disable-line declaration-no-important */ + visibility: hidden; + transform: translate(320px); + transition: all 0.2s ease-in-out, max-height 0.2s 0.2s ease-in-out; +} +.message-drawer .footer-container > *.hidden.previous { + transform: translate(-320px); +} +.message-drawer .matchtext { + background-color: #ade6fe; + color: #333; + height: 1.5rem; +} +.message-drawer .contact-status { + position: absolute; + left: 39px; + top: 39px; + width: 10px; + height: 10px; + border-radius: 50%; +} +.message-drawer .contact-status.online { + border: 1px solid #46a546; + background-color: #46a546; +} +.message-drawer a, +.message-drawer .btn-link { + text-decoration: none; + color: inherit; +} +.message-drawer .message p { + margin: 0; +} +.message-drawer .clickable { + cursor: pointer; +} +.message-drawer .clickable:hover { + box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.05), 3px 3px 5px -2px rgba(0, 0, 0, 0.1), 1px 1px 5px 0 rgba(0, 0, 0, 0.03); +} +.message-drawer h6, +.message-drawer .h6 { + font-size: 14px; + font-weight: normal; +} +.message-drawer .overview-section-toggle .collapsed-icon-container { + display: none; +} +.message-drawer .overview-section-toggle .expanded-icon-container { + display: inline-block; +} +.message-drawer .overview-section-toggle.collapsed .collapsed-icon-container { + display: inline-block; +} +.message-drawer .overview-section-toggle.collapsed .expanded-icon-container { + display: none; +} +.message-drawer .view-overview-body .section { + display: block; +} +.message-drawer .view-overview-body .section.expanded { + display: flex; + flex-direction: column; +} +.message-drawer .view-conversation .content-message-container img { + max-width: 100%; +} @media (max-width: 480px) { .messaging-area-container .messaging-area .messages-area.editing .messages-header { height: 80px; @@ -9969,7 +10126,7 @@ body.path-question-type .mform fieldset.hidden { border-color: rgba(0, 0, 0, 0.15); } .groupinfobox h3 { - margin-top: 0px; + margin-top: 0; } .groupinfobox .left { padding: 10px; @@ -21881,6 +22038,10 @@ ul.indented-list { .py-4 { padding-bottom: 21px !important; } +.ml-0, +.mx-0 { + margin-left: 0; +} .ml-1, .mx-1 { margin-left: 3.5px !important; @@ -21897,6 +22058,10 @@ ul.indented-list { .mx-4 { margin-left: 21px !important; } +.mr-0, +.mx-0 { + margin-right: 0; +} .mr-1, .mx-1 { margin-right: 3.5px !important; @@ -21913,6 +22078,10 @@ ul.indented-list { .mx-4 { margin-right: 21px !important; } +.mb-0, +.my-0 { + margin-bottom: 0; +} .mb-1, .my-1 { margin-bottom: 3.5px !important; @@ -21929,6 +22098,10 @@ ul.indented-list { .my-4 { margin-bottom: 21px !important; } +.mt-0, +.my-0 { + margin-top: 0; +} .mt-1, .my-1 { margin-top: 3.5px !important; @@ -22165,6 +22338,7 @@ ul.indented-list { } .list-group-item { background-color: #fff; + position: relative; } .list-group-item-action:hover, .list-group-item-action:focus { @@ -22187,3 +22361,9 @@ ul.indented-list { height: 24px !important; width: 24px !important; } +.dir-rtl .dir-rtl-hide { + display: none; +} +.dir-ltr .dir-ltr-hide { + display: none; +} diff --git a/theme/bootstrapbase/templates/core_message/message_drawer.mustache b/theme/bootstrapbase/templates/core_message/message_drawer.mustache new file mode 100644 index 00000000000..f2b5168dfb4 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer.mustache @@ -0,0 +1,93 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer + + This template will render the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + Adding a "div.drawer-top" since bootstrapbase does not have a fixed navbar + for smaller screensizes. + + Example context (json): + {} + +}} + + +{{#js}} +require( +[ + 'jquery', + 'core_message/message_drawer', + 'core_message/message_popover' +], +function( + $, + MessageDrawer, + Popover +) { + + var root = $('#message-drawer-{{uniqid}}'); + MessageDrawer.init(root); + + var toggle = $('#message-drawer-close-{{uniqid}}'); + Popover.init(toggle); +}); +{{/js}} diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_icon_back.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_icon_back.mustache new file mode 100644 index 00000000000..be7ac52a5a6 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_icon_back.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_icon_back + + This template will render the back icon for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + changing icon "i/previous" to "t/collapsed_rtl" + + Example context (json): + {} + +}} +{{#pix}} t/collapsed_rtl, core {{/pix}} +{{#pix}} t/collapsed, core {{/pix}} \ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_icon_forward.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_icon_forward.mustache new file mode 100644 index 00000000000..8e872cf52a4 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_icon_forward.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_icon_forward + + This template will render the forward icon for the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + changing icon "i/next" to "t/collapsed" + + Example context (json): + {} + +}} +{{#pix}} t/collapsed, core {{/pix}} +{{#pix}} t/collapsed_rtl, core {{/pix}} \ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_contacts_body.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_contacts_body.mustache new file mode 100644 index 00000000000..34dea58bed5 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_contacts_body.mustache @@ -0,0 +1,101 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_contacts_body + + This template will render the body content of the contacts page in the + message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + adding ".m-0" to "ul.nav-pills" + adding ".w-50.text-center" to ".nav-item" + changing ".show" to ".in" in ".tab-pane" + + Example context (json): + {} + +}} + \ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_body_message.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_body_message.mustache new file mode 100644 index 00000000000..abaff69874f --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_body_message.mustache @@ -0,0 +1,74 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_body_message + + This template will render a single message for the body of the conversation page + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + changing icon "i/unchecked" to "i/completion-manual-n" + + Example context (json): + {} + +}} + + diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_content.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_content.mustache new file mode 100644 index 00000000000..191547c269f --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_content.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_footer_content + + This template will render the footer content for the conversation page in + the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + adding: ".m-0.flex-grow" to "textarea.form-control" + + Example context (json): + {} + +}} + +
+ + +
diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_edit_mode.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_edit_mode.mustache new file mode 100644 index 00000000000..bae82eed2d5 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_edit_mode.mustache @@ -0,0 +1,53 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_footer_edit_mode + + This template will render the foot while the conversation page is in edit mode + in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + changing icon "i/delete" to "i/trash" + + Example context (json): + {} + +}} + +
+ +
\ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache new file mode 100644 index 00000000000..015a7a4a8a1 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache @@ -0,0 +1,125 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_heeader_content + + This template will render the header content of the conversation page in + the message message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + Changing the dropdown menu items from "div.dropdown-menu > a.dropdown-item" + to "ul.dropdown-menu > li > a.dropdown-item" + assigning the "data-action=" and ".hidden" to the "li" elements. + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private_no_controls.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private_no_controls.mustache new file mode 100644 index 00000000000..5fb4fdc9954 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private_no_controls.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_heeader_content + + This template will render the header content of the conversation page in + the message message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + Changing the dropdown menu items from "div.dropdown-menu > a.dropdown-item" + to "ul.dropdown-menu > li > a.dropdown-item" + assigning the "data-action=" and ".hidden" to the "li" elements. + + Example context (json): + {} + +}} + +
+ +
+ {{#imageurl}} +
+ +
+ {{/imageurl}} +
+
+ {{name}} + + {{#pix}} i/star, core {{/pix}} + +
+ {{#showonlinestatus}} +

+ {{#isonline}}{{#str}} loggedin, core_message {{/str}}{{/isonline}} + {{^isonline}}{{#str}} loggedoff, core_message {{/str}}{{/isonline}} +

+ {{/showonlinestatus}} +
+
+
\ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache new file mode 100644 index 00000000000..8c2034670c7 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache @@ -0,0 +1,97 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_conversation_heeader_content + + This template will render the header content of the conversation page in + the message message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + Changing the dropdown menu items from "div.dropdown-menu > a.dropdown-item" + to "ul.dropdown-menu > li > a.dropdown-item" + + Example context (json): + {} + +}} + + \ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_group_info_body_content.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_group_info_body_content.mustache new file mode 100644 index 00000000000..02f29b1027e --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_group_info_body_content.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_group_info_body_content + + This template will render the content for the body section of the group + info page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + {} + +}} + + + {{> core_message/message_drawer_icon_back }} + +
+
+ {{#imageurl}} + + {{/imageurl}} +
+

{{name}}

+ {{#subname}}

{{.}}

{{/subname}} +
+

{{#str}} participants, core_message {{/str}}

+
+ {{< core_message/message_drawer_lazy_load_list }} + {{$rootattributes}} + data-region="members-list" + {{/rootattributes}} + {{$emptymessage}}{{#str}} noparticipants, core_message {{/str}}{{/emptymessage}} + {{$placeholder}} + {{#placeholders}} + {{> core_message/message_drawer_view_group_info_participants_list_item_placeholder }} + {{/placeholders}} + {{/placeholder}} + {{/ core_message/message_drawer_lazy_load_list }} +
diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_overview_header.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_overview_header.mustache new file mode 100644 index 00000000000..d9d47380beb --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_overview_header.mustache @@ -0,0 +1,74 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_header + + This template will render the header for the overview page of the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + changing "div.input-group" to "div.input-prepend" + changing the "div.input-group-prepend > span" to "span.add-on" + addint ".searchinput" to "input.form-control" + + Example context (json): + {} + +}} + diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache new file mode 100644 index 00000000000..060d2878bfd --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_overview_section + + This template is a base template to render a collapsible "section" on the + overview page of the message drawer, for example the messages section. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + changing icon "t/collapsedcaret" to "t/collapsed" + adding ".in" to ".show" + + Example context (json): + {} + +}} +
+
+ +
+ {{< core_message/message_drawer_lazy_load_list }} + {{$rootclasses}}collapse border-bottom {{#expanded}}show in{{/expanded}}{{/rootclasses}} + {{$rootattributes}} + id="{{$region}}{{/region}}-target" + aria-labelledby="{{$region}}{{/region}}-toggle" + data-parent="#message-drawer-view-overview-container" + {{/rootattributes}} + {{/ core_message/message_drawer_lazy_load_list }} +
\ No newline at end of file diff --git a/theme/bootstrapbase/templates/core_message/message_drawer_view_search_header.mustache b/theme/bootstrapbase/templates/core_message/message_drawer_view_search_header.mustache new file mode 100644 index 00000000000..f6230ec8473 --- /dev/null +++ b/theme/bootstrapbase/templates/core_message/message_drawer_view_search_header.mustache @@ -0,0 +1,74 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_drawer_view_search_header + + This template will render the header of the search page in the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + This is an overridden template + changing "div.input-group" to "div.input-append" + removing the "div.input-group-append" + changing "button.btn.btn-outline-secondary" to "button.btn.m0" + + Example context (json): + {} + +}} + + \ No newline at end of file From 1e3453b7c764e93395533b7dc16c34e2c04a7e07 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Thu, 8 Nov 2018 15:38:33 +0800 Subject: [PATCH 23/31] MDL-63303 message: change nav popover to toggle message drawer --- message/amd/build/message_popover.min.js | 1 + message/amd/src/message_popover.js | 102 ++++++ message/lib.php | 33 ++ .../build/message_popover_controller.min.js | 1 - .../amd/src/message_popover_controller.js | 300 ------------------ message/output/popup/lib.php | 15 - .../popup/templates/message_popover.mustache | 85 ----- message/templates/message_popover.mustache | 58 ++++ 8 files changed, 194 insertions(+), 401 deletions(-) create mode 100644 message/amd/build/message_popover.min.js create mode 100644 message/amd/src/message_popover.js delete mode 100644 message/output/popup/amd/build/message_popover_controller.min.js delete mode 100644 message/output/popup/amd/src/message_popover_controller.js delete mode 100644 message/output/popup/templates/message_popover.mustache create mode 100644 message/templates/message_popover.mustache diff --git a/message/amd/build/message_popover.min.js b/message/amd/build/message_popover.min.js new file mode 100644 index 00000000000..d5a61eb6918 --- /dev/null +++ b/message/amd/build/message_popover.min.js @@ -0,0 +1 @@ +define(["jquery","core/custom_interaction_events","core/pubsub","core_message/message_drawer_events"],function(a,b,c,d){var e={COUNT_CONTAINER:'[data-region="count-container"]'},f=function(){c.publish(d.TOGGLE_VISIBILITY)},g=function(a){return function(){var b=a.find(e.COUNT_CONTAINER),c=parseInt(b.text(),10);isNaN(c)?b.addClass("hidden"):!c||c<2?b.addClass("hidden"):(c-=1,b.text(c))}},h=function(a){b.define(a,[b.events.activate]),a.on(b.events.activate,function(a,b){f(),b.originalEvent.preventDefault()}),c.subscribe(d.CONVERSATION_READ,g(a)),c.subscribe(d.CONTACT_REQUEST_ACCEPTED,g(a)),c.subscribe(d.CONTACT_REQUEST_DECLINED,g(a))},i=function(b){b=a(b),h(b)};return{init:i}}); \ No newline at end of file diff --git a/message/amd/src/message_popover.js b/message/amd/src/message_popover.js new file mode 100644 index 00000000000..556e3415b67 --- /dev/null +++ b/message/amd/src/message_popover.js @@ -0,0 +1,102 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the message popover in the nav bar. + * + * @module core_message/message_popover + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/custom_interaction_events', + 'core/pubsub', + 'core_message/message_drawer_events' +], +function( + $, + CustomEvents, + PubSub, + MessageDrawerEvents +) { + var SELECTORS = { + COUNT_CONTAINER: '[data-region="count-container"]' + }; + + /** + * Toggle the message drawer visibility. + */ + var toggleMessageDrawerVisibility = function() { + PubSub.publish(MessageDrawerEvents.TOGGLE_VISIBILITY); + }; + + /** + * Decrement the unread conversation count in the nav bar if a conversation + * is read. When there are no unread conversations then hide the counter. + * + * @param {Object} root The root element for the popover. + * @return {Function} + */ + var handleDecrementConversationCount = function(root) { + return function() { + var countContainer = root.find(SELECTORS.COUNT_CONTAINER); + var count = parseInt(countContainer.text(), 10); + + if (isNaN(count)) { + countContainer.addClass('hidden'); + } else if (!count || count < 2) { + countContainer.addClass('hidden'); + } else { + count = count - 1; + countContainer.text(count); + } + }; + }; + + /** + * Add events listeners for when the popover icon is clicked and when conversations + * are read. + * + * @param {Object} root The root element for the popover. + */ + var registerEventListeners = function(root) { + CustomEvents.define(root, [CustomEvents.events.activate]); + + root.on(CustomEvents.events.activate, function(e, data) { + toggleMessageDrawerVisibility(); + data.originalEvent.preventDefault(); + }); + + PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, handleDecrementConversationCount(root)); + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, handleDecrementConversationCount(root)); + PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, handleDecrementConversationCount(root)); + }; + + /** + * Initialise the message popover. + * + * @param {Object} root The root element for the popover. + */ + var init = function(root) { + root = $(root); + registerEventListeners(root); + }; + + return { + init: init, + }; +}); diff --git a/message/lib.php b/message/lib.php index 6877b7bf5a7..fc4bebe0d86 100644 --- a/message/lib.php +++ b/message/lib.php @@ -764,6 +764,39 @@ function core_message_user_preferences() { return $preferences; } +/** + * Renders the popup. + * + * @param renderer_base $renderer + * @return string The HTML + */ +function core_message_render_navbar_output(\renderer_base $renderer) { + global $USER, $CFG; + + // Early bail out conditions. + if (!isloggedin() || isguestuser() || user_not_fully_set_up($USER) || + get_user_preferences('auth_forcepasswordchange') || + (!$USER->policyagreed && !is_siteadmin() && + ($manager = new \core_privacy\local\sitepolicy\manager()) && $manager->is_defined())) { + return ''; + } + + $output = ''; + + // Add the messages popover. + if (!empty($CFG->messaging)) { + $unreadcount = \core_message\api::count_unread_conversations($USER); + $requestcount = \core_message\api::count_received_contact_requests($USER); + $context = [ + 'userid' => $USER->id, + 'unreadcount' => $unreadcount + $requestcount + ]; + $output .= $renderer->render_from_template('core_message/message_popover', $context); + } + + return $output; +} + /** * Render the message drawer to be included in the top of the body of * each page. diff --git a/message/output/popup/amd/build/message_popover_controller.min.js b/message/output/popup/amd/build/message_popover_controller.min.js deleted file mode 100644 index 37fea7587b0..00000000000 --- a/message/output/popup/amd/build/message_popover_controller.min.js +++ /dev/null @@ -1 +0,0 @@ -define(["jquery","core/ajax","core/templates","core/str","core/notification","core/custom_interaction_events","core/popover_region_controller","core_message/message_repository","core/url"],function(a,b,c,d,e,f,g,h,i){var j={MARK_ALL_READ_BUTTON:'[data-action="mark-all-read"]',CONTENT:'[data-region="messages"]',CONTENT_ITEM_CONTAINER:'[data-region="message-content-item-container"]',EMPTY_MESSAGE:'[data-region="empty-message"]',COUNT_CONTAINER:'[data-region="count-container"]'},k=function(a){g.call(this,a),this.markAllReadButton=this.root.find(j.MARK_ALL_READ_BUTTON),this.content=this.root.find(j.CONTENT),this.userId=this.root.attr("data-userid"),this.limit=20,this.offset=0,this.loadedAll=!1,this.initialLoad=!1,this.unreadCount=this.root.find(j.COUNT_CONTAINER).html()};return k.prototype=Object.create(g.prototype),k.prototype.constructor=k,k.prototype.getContent=function(){return this.content},k.prototype.incrementOffset=function(){this.offset+=this.limit},k.prototype.updateButtonAriaLabel=function(){this.isMenuOpen()?d.get_string("hidemessagewindow","message").done(function(a){this.menuToggle.attr("aria-label",a)}.bind(this)):this.unreadCount?d.get_string("showmessagewindowwithcount","message",this.unreadCount).done(function(a){this.menuToggle.attr("aria-label",a)}.bind(this)):d.get_string("showmessagewindownonew","message").done(function(a){this.menuToggle.attr("aria-label",a)}.bind(this))},k.prototype.renderUnreadCount=function(){var a=this.root.find(j.COUNT_CONTAINER);this.unreadCount?(a.text(this.unreadCount),a.removeClass("hidden")):a.addClass("hidden")},k.prototype.hideUnreadCount=function(){this.root.find(j.COUNT_CONTAINER).addClass("hidden")},k.prototype.renderMessages=function(b,d){var e=[];return a.each(b,function(a,b){b.contexturl=i.relativeUrl("/message/index.php",{user:this.userId,id:b.userid}),b.profileurl=i.relativeUrl("/user/profile.php",{id:b.userid});var d=c.render("message_popup/message_content_item",b).then(function(a,b){return{html:a,js:b}});e.push(d)}.bind(this)),a.when.apply(a,e).then(function(){a.each(arguments,function(a,b){d.append(b.html),c.runTemplateJS(b.js)})})},k.prototype.loadMoreMessages=function(){if(this.isLoading||this.loadedAll)return a.Deferred().resolve();this.startLoading();var b={userid:this.userId,limit:this.limit,offset:this.offset},c=this.getContent();return h.query(b).then(function(a){var b=a.contacts;return this.loadedAll=!b.length||b.length. - -/** - * Controls the message popover in the nav bar. - * - * See template: message_popup/message_popover - * - * @module message_popup/message_popover_controller - * @class message_popover_controller - * @package message_popup - * @copyright 2016 Ryan Wyllie - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -define(['jquery', 'core/ajax', 'core/templates', 'core/str', - 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller', - 'core_message/message_repository', 'core/url'], - function($, Ajax, Templates, Str, Notification, CustomEvents, - PopoverController, MessageRepo, URL) { - - var SELECTORS = { - MARK_ALL_READ_BUTTON: '[data-action="mark-all-read"]', - CONTENT: '[data-region="messages"]', - CONTENT_ITEM_CONTAINER: '[data-region="message-content-item-container"]', - EMPTY_MESSAGE: '[data-region="empty-message"]', - COUNT_CONTAINER: '[data-region="count-container"]', - }; - - /** - * Constructor for the MessagePopoverController. - * Extends PopoverRegionController. - * - * @param {object} element jQuery object root element of the popover - */ - var MessagePopoverController = function(element) { - // Initialise base class. - PopoverController.call(this, element); - - this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON); - this.content = this.root.find(SELECTORS.CONTENT); - this.userId = this.root.attr('data-userid'); - this.limit = 20; - this.offset = 0; - this.loadedAll = false; - this.initialLoad = false; - - // Let's find out how many unread messages there are. - this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html(); - }; - - /** - * Clone the parent prototype. - */ - MessagePopoverController.prototype = Object.create(PopoverController.prototype); - - /** - * Make sure the constructor is set correctly. - */ - MessagePopoverController.prototype.constructor = MessagePopoverController; - - /** - * Get the element holding the messages. - * - * @method getContent - * @return {object} jQuery element - */ - MessagePopoverController.prototype.getContent = function() { - return this.content; - }; - - /** - * Increment the offset. - * - * @method incrementOffset - */ - MessagePopoverController.prototype.incrementOffset = function() { - this.offset += this.limit; - }; - - /** - * Set the correct aria label on the menu toggle button to be read out by screen - * readers. The message will indicate the state of the unread messages. - * - * @method updateButtonAriaLabel - */ - MessagePopoverController.prototype.updateButtonAriaLabel = function() { - if (this.isMenuOpen()) { - Str.get_string('hidemessagewindow', 'message').done(function(string) { - this.menuToggle.attr('aria-label', string); - }.bind(this)); - } else { - if (this.unreadCount) { - Str.get_string('showmessagewindowwithcount', 'message', this.unreadCount).done(function(string) { - this.menuToggle.attr('aria-label', string); - }.bind(this)); - } else { - Str.get_string('showmessagewindownonew', 'message').done(function(string) { - this.menuToggle.attr('aria-label', string); - }.bind(this)); - } - } - }; - - /** - * Show the unread message count badge on the menu toggle if there - * are unread messages, otherwise hide it. - * - * @method renderUnreadCount - */ - MessagePopoverController.prototype.renderUnreadCount = function() { - var element = this.root.find(SELECTORS.COUNT_CONTAINER); - - if (this.unreadCount) { - element.text(this.unreadCount); - element.removeClass('hidden'); - } else { - element.addClass('hidden'); - } - }; - - /** - * Hide the unread message count badge on the menu toggle. - * - * @method hideUnreadCount - */ - MessagePopoverController.prototype.hideUnreadCount = function() { - this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden'); - }; - - /** - * Render the message data with the appropriate template and add it to the DOM. - * - * @method renderMessages - * @param {array} messages Message data - * @param {object} container jQuery object the container to append the rendered messages - * @return {object} jQuery promise that is resolved when all messages have been - * rendered and added to the DOM - */ - MessagePopoverController.prototype.renderMessages = function(messages, container) { - var promises = []; - - $.each(messages, function(index, message) { - message.contexturl = URL.relativeUrl('/message/index.php', { - user: this.userId, - id: message.userid, - }); - - message.profileurl = URL.relativeUrl('/user/profile.php', { - id: message.userid, - }); - - var promise = Templates.render('message_popup/message_content_item', message) - .then(function(html, js) { - return {html: html, js: js}; - }); - promises.push(promise); - }.bind(this)); - - return $.when.apply($, promises).then(function() { - // Each of the promises in the when will pass its results as an argument to the function. - // The order of the arguments will be the order that the promises are passed to when() - // i.e. the first promise's results will be in the first argument. - $.each(arguments, function(index, argument) { - container.append(argument.html); - Templates.runTemplateJS(argument.js); - }); - return; - }); - }; - - /** - * Send a request for more messages from the server, if we aren't already - * loading some and haven't already loaded all of them. - * - * @method loadMoreMessages - * @return {object} jQuery promise that is resolved when messages have been - * retrieved and added to the DOM - */ - MessagePopoverController.prototype.loadMoreMessages = function() { - if (this.isLoading || this.loadedAll) { - return $.Deferred().resolve(); - } - - this.startLoading(); - var request = { - userid: this.userId, - limit: this.limit, - offset: this.offset, - }; - - var container = this.getContent(); - return MessageRepo.query(request).then(function(result) { - var messages = result.contacts; - this.loadedAll = !messages.length || messages.length < this.limit; - this.initialLoad = true; - this.updateButtonAriaLabel(); - - if (messages.length) { - this.incrementOffset(); - return this.renderMessages(messages, container); - } - - return false; - }.bind(this)) - .always(function() { - this.stopLoading(); - }.bind(this)); - }; - - /** - * Send a request to the server to mark all unread messages as read and update - * the unread count and unread messages elements appropriately. - * - * @method markAllAsRead - * @return {Promise} - */ - MessagePopoverController.prototype.markAllAsRead = function() { - if (this.markAllReadButton.hasClass('loading')) { - return $.Deferred().resolve(); - } - - this.markAllReadButton.addClass('loading'); - - return MessageRepo.markAllAsRead({useridto: this.userId}) - .then(function() { - this.unreadCount = 0; - this.hideUnreadCount(); - this.getContent().find(SELECTORS.CONTENT_ITEM_CONTAINER).removeClass('unread'); - }.bind(this)) - .always(function() { - this.markAllReadButton.removeClass('loading'); - }.bind(this)); - }; - - /** - * Add all of the required event listeners for this messages popover. - * - * @method registerEventListeners - */ - MessagePopoverController.prototype.registerEventListeners = function() { - CustomEvents.define(this.root, [ - CustomEvents.events.keyboardActivate, - ]); - - // Update the message information when the menu is opened. - this.root.on(this.events().menuOpened, function() { - this.hideUnreadCount(); - this.updateButtonAriaLabel(); - - if (!this.initialLoad) { - this.loadMoreMessages(); - } - }.bind(this)); - - // Update the message information when the menu is opened. - this.root.on(this.events().menuClosed, function() { - this.renderUnreadCount(); - this.updateButtonAriaLabel(); - }.bind(this)); - - // Load more messages when we scroll to the bottom of the open menu. - this.root.on(CustomEvents.events.scrollBottom, function() { - this.loadMoreMessages(); - }.bind(this)); - - // Mark all messages as read when button is activated. - this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) { - this.markAllAsRead(); - - e.stopPropagation(); - data.originalEvent.preventDefault(); - }.bind(this)); - - // Stop mouse scroll from propagating to the window element and - // scrolling the page. - CustomEvents.define(this.getContentContainer(), [ - CustomEvents.events.scrollLock - ]); - - // Check if we have marked a conversation as read in the messaging area. - $(document).on('messagearea:conversationselected', function() { - this.unreadCount--; - this.renderUnreadCount(); - }.bind(this)); - }; - - return MessagePopoverController; -}); diff --git a/message/output/popup/lib.php b/message/output/popup/lib.php index 32531091f58..fcfb3696de7 100644 --- a/message/output/popup/lib.php +++ b/message/output/popup/lib.php @@ -43,21 +43,6 @@ function message_popup_render_navbar_output(\renderer_base $renderer) { $output = ''; - // Add the messages popover. - if (!empty($CFG->messaging)) { - $unreadcount = \core_message\api::count_unread_conversations($USER); - $context = [ - 'userid' => $USER->id, - 'unreadcount' => $unreadcount, - 'urls' => [ - 'seeall' => (new moodle_url('/message/index.php'))->out(), - 'writeamessage' => (new moodle_url('/message/index.php', ['contactsfirst' => 1]))->out(), - 'preferences' => (new moodle_url('/message/edit.php', ['id' => $USER->id]))->out(), - ], - ]; - $output .= $renderer->render_from_template('message_popup/message_popover', $context); - } - // Add the notifications popover. $enabled = \core_message\api::is_processor_enabled("popup"); if ($enabled) { diff --git a/message/output/popup/templates/message_popover.mustache b/message/output/popup/templates/message_popover.mustache deleted file mode 100644 index 75b363d8fa2..00000000000 --- a/message/output/popup/templates/message_popover.mustache +++ /dev/null @@ -1,85 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template message_output_popup/message_popover - - This template will render the message popover for the navigation bar. - - Classes required for JS: - * none - - Data attributes required for JS: - * All data attributes are required - - Context variables required for this template: - * userid The logged in user id - * urls The URLs for the popover - - Example context (json): - { - "userid": 3, - "urls": { - "preferences": "http://www.moodle.com" - } - } - -}} -{{< core/popover_region }} - {{$classes}}popover-region-messages{{/classes}} - {{$attributes}}id="nav-message-popover-container" data-userid="{{userid}}"{{/attributes}} - - {{$togglelabel}}{{#str}} showmessagewindownonew, message {{/str}}{{/togglelabel}} - {{$togglecontent}} - {{#pix}} t/message, core, {{#str}} togglemessagemenu, message {{/str}} {{/pix}} -
{{unreadcount}}
- {{/togglecontent}} - - {{$containerlabel}}{{#str}} notificationwindow, message {{/str}}{{/containerlabel}} - - {{$headertext}}{{#str}} messages, message {{/str}}{{/headertext}} - {{$headeractions}} - - - {{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}} - {{> core/loading }} - - - {{#pix}} i/settings, core, {{#str}} messagepreferences, message {{/str}} {{/pix}} - - {{/headeractions}} - - {{$content}} -
-
{{#str}} nomessages, message {{/str}}
- {{/content}} -{{/ core/popover_region }} -{{#js}} -require(['jquery', 'message_popup/message_popover_controller'], function($, controller) { - var container = $('#nav-message-popover-container'); - var controller = new controller(container); - controller.registerEventListeners(); - controller.registerListNavigationEventListeners(); -}); -{{/js}} diff --git a/message/templates/message_popover.mustache b/message/templates/message_popover.mustache new file mode 100644 index 00000000000..74784b326d7 --- /dev/null +++ b/message/templates/message_popover.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_message/message_popover + + This template will render the message popover for the navigation bar. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + { + "unreadcount": 3 + } + +}} + + +{{#js}} +require( +[ + 'jquery', + 'core_message/message_popover' +], +function( + $, + Popover +) { + var toggle = $('#message-drawer-toggle-{{uniqid}}'); + Popover.init(toggle); +}); +{{/js}} From 17d982d751499ee36328ac6902836c3d17d433ef Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:52:54 +0800 Subject: [PATCH 24/31] MDL-63303 message: message/edit.php opens setttings in message drawer --- message/edit.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/message/edit.php b/message/edit.php index 3117d9b52e1..f511b06a4e1 100644 --- a/message/edit.php +++ b/message/edit.php @@ -27,6 +27,7 @@ require_once($CFG->dirroot . '/message/lib.php'); require_once($CFG->dirroot . '/user/lib.php'); $userid = optional_param('id', 0, PARAM_INT); // User id. +$currentuser = true; if (!$userid) { $userid = $USER->id; @@ -58,6 +59,7 @@ if ($user->id == $USER->id) { //editing own message profile require_capability('moodle/user:editownmessageprofile', $systemcontext); } else { + $currentuser = false; // teachers, parents, etc. require_capability('moodle/user:editmessageprofile', $personalcontext); // no editing of guest user account @@ -77,11 +79,16 @@ $strmessaging = get_string('messagepreferences', 'message'); $PAGE->set_title($strmessaging); $PAGE->set_heading(fullname($user)); -// Grab the renderer -$renderer = $PAGE->get_renderer('core', 'message'); -$messagingoptions = $renderer->render_user_message_preferences($user); - echo $OUTPUT->header(); -echo $messagingoptions; +if ($currentuser) { + // Open the message drawer to show the settings. + echo $OUTPUT->heading(get_string('messagepreferences', 'core_message')); + $PAGE->requires->js_call_amd('core_message/message_drawer_helper', 'showSettings'); +} else { + // Viewing another user's preferences so render the old page. + $renderer = $PAGE->get_renderer('core', 'message'); + echo $renderer->render_user_message_preferences($user); +} + echo $OUTPUT->footer(); From 142b042c5ccaa537fb2a59c4773326ac4d1064e6 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 16 Oct 2018 11:53:32 +0800 Subject: [PATCH 25/31] MDL-63303 message: message/index.php open conversation in message drawer --- message/index.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/message/index.php b/message/index.php index 159ebfbb204..c50e881f6e0 100644 --- a/message/index.php +++ b/message/index.php @@ -99,6 +99,25 @@ $usernode->remove(); $settings = $PAGE->settingsnav->find('messages', null); $settings->make_active(); +if ($currentuser) { + // We're in the pprocess of deprecating this page however we haven't replaced the functionality + // for the admin (or user with correct capabilities) to view other user's conversations. For the + // time being this page will simply open the message drawer unless it's the admin user case just + // mentioned. In that case we will render the old UI for backwards compatibility. + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('messages', 'message')); + $conversationid = empty($user2id) ? null : \core_message\api::get_conversation_between_users([$USER->id, $user2id]); + if (empty($conversationid)) { + $PAGE->requires->js_call_amd('core_message/message_drawer_helper', 'show'); + } else { + $PAGE->requires->js_call_amd('core_message/message_drawer_helper', 'showConversation', [$conversationid]); + } + echo $OUTPUT->footer(); + exit(); +} + +// The only time we should get here is if it's an admin type user viewing another user's messages. + // Get the renderer and the information we are going to be use. $renderer = $PAGE->get_renderer('core_message'); $requestedconversation = false; From f074d6f0868fff6586bac09e5db8c79ea6713817 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Wed, 31 Oct 2018 15:40:55 +0800 Subject: [PATCH 26/31] MDL-63303 message: remove behat tests for messaging --- lib/behat/classes/partial_named_selector.php | 6 +- .../behat/message_popover_preferences.feature | 17 ---- .../behat/message_popover_unread.feature | 60 ------------ message/tests/behat/behat_message.php | 35 ++++--- .../tests/behat/delete_all_messages.feature | 37 -------- message/tests/behat/delete_messages.feature | 41 -------- message/tests/behat/manage_contacts.feature | 55 ----------- message/tests/behat/reply_message.feature | 35 ------- message/tests/behat/search_messages.feature | 42 -------- message/tests/behat/search_users.feature | 39 -------- .../update_messaging_preferences.feature | 36 ------- message/tests/behat/view_messages.feature | 34 ------- user/tests/behat/view_full_profile.feature | 95 ++++++++++--------- 13 files changed, 74 insertions(+), 458 deletions(-) delete mode 100644 message/output/popup/tests/behat/message_popover_preferences.feature delete mode 100644 message/output/popup/tests/behat/message_popover_unread.feature delete mode 100644 message/tests/behat/delete_all_messages.feature delete mode 100644 message/tests/behat/delete_messages.feature delete mode 100644 message/tests/behat/manage_contacts.feature delete mode 100644 message/tests/behat/reply_message.feature delete mode 100644 message/tests/behat/search_messages.feature delete mode 100644 message/tests/behat/search_users.feature delete mode 100644 message/tests/behat/update_messaging_preferences.feature delete mode 100644 message/tests/behat/view_messages.feature diff --git a/lib/behat/classes/partial_named_selector.php b/lib/behat/classes/partial_named_selector.php index 67abfef22f4..def514b3550 100644 --- a/lib/behat/classes/partial_named_selector.php +++ b/lib/behat/classes/partial_named_selector.php @@ -185,13 +185,13 @@ XPATH .//*[self::label or self::div[contains(concat(' ', @class, ' '), ' fstaticlabel ')]][contains(., %locator%)]/ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')] XPATH , 'message_area_region' => << << << <<