From 424c6c21387797e2a42acecd7d4453b824b49fb6 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 13 Nov 2018 10:04:45 +0100 Subject: [PATCH 01/15] MOBILE-2620 notifications: Fix type of message displayed --- src/addon/notifications/providers/notifications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/notifications/providers/notifications.ts b/src/addon/notifications/providers/notifications.ts index bbd74caa4..4bc082d81 100644 --- a/src/addon/notifications/providers/notifications.ts +++ b/src/addon/notifications/providers/notifications.ts @@ -48,7 +48,7 @@ export class AddonNotificationsProvider { protected formatNotificationsData(notifications: any[]): void { notifications.forEach((notification) => { // Set message to show. - if (notification.contexturl && notification.contexturl.indexOf('/mod/forum/')) { + if (notification.contexturl && notification.contexturl.indexOf('/mod/forum/') >= 0) { notification.mobiletext = notification.smallmessage; } else { notification.mobiletext = notification.fullmessage; From 3fed57b912644a418e371d9ce86677f8d238a2f9 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 12:54:24 +0100 Subject: [PATCH 02/15] MOBILE-2620 messages: New strings --- scripts/langindex.json | 25 ++++++++++++++++++++++++- src/addon/messages/lang/en.json | 27 +++++++++++++++++++++++++-- src/assets/lang/en.json | 26 +++++++++++++++++++++++++- src/lang/en.json | 1 + 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 015df6814..e42df76d3 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -147,8 +147,11 @@ "addon.files.privatefiles": "moodle", "addon.files.sitefiles": "moodle", "addon.messageoutput_airnotifier.processorsettingsdesc": "local_moodlemobileapp", + "addon.messages.acceptandaddcontact": "message", "addon.messages.addcontact": "message", + "addon.messages.addcontactconfirm": "message", "addon.messages.addtofavourites": "message", + "addon.messages.addtoyourcontacts": "message", "addon.messages.blocknoncontacts": "message", "addon.messages.blockuser": "message", "addon.messages.blockuserconfirm": "message", @@ -159,7 +162,9 @@ "addon.messages.contactblocked": "message", "addon.messages.contactlistempty": "local_moodlemobileapp", "addon.messages.contactname": "local_moodlemobileapp", + "addon.messages.contactrequestsent": "message", "addon.messages.contacts": "message", + "addon.messages.decline": "message", "addon.messages.deleteallconfirm": "message", "addon.messages.deleteconversationq": "message", "addon.messages.deletemessage": "local_moodlemobileapp", @@ -168,34 +173,51 @@ "addon.messages.errorwhileretrievingcontacts": "local_moodlemobileapp", "addon.messages.errorwhileretrievingdiscussions": "local_moodlemobileapp", "addon.messages.errorwhileretrievingmessages": "local_moodlemobileapp", + "addon.messages.errorwhileretrievingusers": "local_moodlemobileapp", "addon.messages.groupinfo": "message", "addon.messages.groupmessages": "message", "addon.messages.info": "message", + "addon.messages.isnotinyourcontacts": "message", "addon.messages.message": "message", "addon.messages.messagenotsent": "local_moodlemobileapp", "addon.messages.messagepreferences": "message", "addon.messages.messages": "message", "addon.messages.newmessage": "message", "addon.messages.newmessages": "local_moodlemobileapp", + "addon.messages.nocontactrequests": "message", + "addon.messages.noncontacts": "message", + "addon.messages.nocontactsgetstarted": "message", "addon.messages.nofavourites": "message", "addon.messages.nogroupmessages": "message", "addon.messages.nomessages": "message", "addon.messages.nousersfound": "local_moodlemobileapp", "addon.messages.numparticipants": "message", "addon.messages.removecontact": "message", - "addon.messages.removecontactconfirm": "local_moodlemobileapp", + "addon.messages.removecontactconfirm": "message", + "addon.messages.removefromyourcontacts": "message", "addon.messages.removefromfavourites": "message", + "addon.messages.requests": "moodle", + "addon.messages.requirecontacttomessage": "message", + "addon.messages.searchcombined": "message", + "addon.messages.searchnocontactsfound": "message", + "addon.messages.searchnomessagesfound": "message", + "addon.messages.searchnononcontactsfound": "message", "addon.messages.showdeletemessages": "local_moodlemobileapp", + "addon.messages.sendcontactrequest": "message", "addon.messages.type_blocked": "local_moodlemobileapp", "addon.messages.type_offline": "local_moodlemobileapp", "addon.messages.type_online": "local_moodlemobileapp", "addon.messages.type_search": "local_moodlemobileapp", "addon.messages.type_strangers": "local_moodlemobileapp", + "addon.messages.unabletomessage": "message", "addon.messages.unblockuser": "message", "addon.messages.unblockuserconfirm": "message", + "addon.messages.userwouldliketocontactyou": "message", "addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp", "addon.messages.warningmessagenotsent": "local_moodlemobileapp", + "addon.messages.wouldliketocontactyou": "message", "addon.messages.you": "message", + "addon.messages.youhaveblockeduser": "message", "addon.mod_assign.acceptsubmissionstatement": "local_moodlemobileapp", "addon.mod_assign.addattempt": "assign", "addon.mod_assign.addnewattempt": "assign", @@ -1540,6 +1562,7 @@ "core.quotausage": "moodle", "core.redirectingtosite": "local_moodlemobileapp", "core.refresh": "moodle", + "core.remove": "moodle", "core.required": "moodle", "core.requireduserdatamissing": "local_moodlemobileapp", "core.resources": "moodle", diff --git a/src/addon/messages/lang/en.json b/src/addon/messages/lang/en.json index 9bfb0f29b..571118cf7 100644 --- a/src/addon/messages/lang/en.json +++ b/src/addon/messages/lang/en.json @@ -1,6 +1,9 @@ { + "acceptandaddcontact": "Accept and add to contacts", "addcontact": "Add contact", + "addcontactconfirm": "Are you sure you want to add {{$a}} to your contacts?", "addtofavourites": "Star", + "addtoyourcontacts": "Add to contacts", "blocknoncontacts": "Prevent non-contacts from messaging me", "blockuser": "Block user", "blockuserconfirm": "Are you sure you want to block {{$a}}?", @@ -11,7 +14,9 @@ "contactblocked": "Contact blocked", "contactlistempty": "The contact list is empty", "contactname": "Contact name", + "contactrequestsent": "Contact request sent", "contacts": "Contacts", + "decline": "Decline", "deleteallconfirm": "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.", "deleteconversation": "Delete conversation", "deletemessage": "Delete message", @@ -20,32 +25,50 @@ "errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", "errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", "errorwhileretrievingmessages": "Error while retrieving messages from the server.", + "errorwhileretrievingusers": "Error while retrieving users from the server.", "groupinfo": "Group info", "groupmessages": "Group messages", "info": "Info", + "isnotinyourcontacts": "{{$a}} is not in your contacts", "messagenotsent": "The message was not sent. Please try again later.", "message": "Message", "messagepreferences": "Message preferences", "messages": "Messages", "newmessage": "New message", "newmessages": "New messages", + "nocontactrequests": "No contact requests", + "noncontacts": "Non-contacts", + "nocontactsgetstarted": "Try searching for someone to add them as a contact", "nofavourites": "No favourites", "nogroupmessages": "No group messages", "nomessages": "No messages", "nousersfound": "No users found", "numparticipants": "{{$a}} participants", "removecontact": "Remove contact", - "removecontactconfirm": "Contact will be removed from your contacts list.", + "removecontactconfirm": "Are you sure you want to remove {{$a}} from your contacts?", + "removefromyourcontacts": "Remove from contacts", "removefromfavourites": "Unstar", + "requests": "Requests", + "requirecontacttomessage": "You need to request {{$a}} to add you as a contact to be able to message them.", + "searchcombined": "Search people and messages", + "searchnocontactsfound": "No contacts found", + "searchnomessagesfound": "No messages found", + "searchnononcontactsfound": "No non contacts found", + "sendcontactrequest": "Send contact request", "showdeletemessages": "Show delete messages", "type_blocked": "Blocked", "type_offline": "Offline", "type_online": "Online", "type_search": "Search results", "type_strangers": "Others", + "unabletomessage": "You are unable to message this user", "unblockuser": "Unblock user", "unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", + "userwouldliketocontactyou": "{{$a}} would like to contact you", "warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", "warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", - "you": "You:" + "wouldliketocontactyou": "Would like to contact you", + "you": "You:", + "youhaveblockeduser": "You have blocked this user in the past", + "yourcontactrequestpending": "Your contact request is pending with {{$a}}" } \ No newline at end of file diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 9e89479cf..fe2a2cea1 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -147,8 +147,11 @@ "addon.files.privatefiles": "Private files", "addon.files.sitefiles": "Site files", "addon.messageoutput_airnotifier.processorsettingsdesc": "Configure devices", + "addon.messages.acceptandaddcontact": "Accept and add to contacts", "addon.messages.addcontact": "Add contact", + "addon.messages.addcontactconfirm": "Are you sure you want to add {{$a}} to your contacts?", "addon.messages.addtofavourites": "Star", + "addon.messages.addtoyourcontacts": "Add to contacts", "addon.messages.blocknoncontacts": "Prevent non-contacts from messaging me", "addon.messages.blockuser": "Block user", "addon.messages.blockuserconfirm": "Are you sure you want to block {{$a}}?", @@ -159,7 +162,9 @@ "addon.messages.contactblocked": "Contact blocked", "addon.messages.contactlistempty": "The contact list is empty", "addon.messages.contactname": "Contact name", + "addon.messages.contactrequestsent": "Contact request sent", "addon.messages.contacts": "Contacts", + "addon.messages.decline": "Decline", "addon.messages.deleteallconfirm": "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.", "addon.messages.deleteconversation": "Delete conversation", "addon.messages.deletemessage": "Delete message", @@ -168,34 +173,52 @@ "addon.messages.errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", "addon.messages.errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", "addon.messages.errorwhileretrievingmessages": "Error while retrieving messages from the server.", + "addon.messages.errorwhileretrievingusers": "Error while retrieving users from the server.", "addon.messages.groupinfo": "Group info", "addon.messages.groupmessages": "Group messages", "addon.messages.info": "Info", + "addon.messages.isnotinyourcontacts": "{{$a}} is not in your contacts", "addon.messages.message": "Message", "addon.messages.messagenotsent": "The message was not sent. Please try again later.", "addon.messages.messagepreferences": "Message preferences", "addon.messages.messages": "Messages", "addon.messages.newmessage": "New message", "addon.messages.newmessages": "New messages", + "addon.messages.nocontactrequests": "No contact requests", + "addon.messages.nocontactsgetstarted": "Try searching for someone to add them as a contact", "addon.messages.nofavourites": "No favourites", "addon.messages.nogroupmessages": "No group messages", "addon.messages.nomessages": "No messages", + "addon.messages.noncontacts": "Non-contacts", "addon.messages.nousersfound": "No users found", "addon.messages.numparticipants": "{{$a}} participants", "addon.messages.removecontact": "Remove contact", - "addon.messages.removecontactconfirm": "Contact will be removed from your contacts list.", + "addon.messages.removecontactconfirm": "Are you sure you want to remove {{$a}} from your contacts?", "addon.messages.removefromfavourites": "Unstar", + "addon.messages.removefromyourcontacts": "Remove from contacts", + "addon.messages.requests": "Requests", + "addon.messages.requirecontacttomessage": "You need to request {{$a}} to add you as a contact to be able to message them.", + "addon.messages.searchcombined": "Search people and messages", + "addon.messages.searchnocontactsfound": "No contacts found", + "addon.messages.searchnomessagesfound": "No messages found", + "addon.messages.searchnononcontactsfound": "No non contacts found", + "addon.messages.sendcontactrequest": "Send contact request", "addon.messages.showdeletemessages": "Show delete messages", "addon.messages.type_blocked": "Blocked", "addon.messages.type_offline": "Offline", "addon.messages.type_online": "Online", "addon.messages.type_search": "Search results", "addon.messages.type_strangers": "Others", + "addon.messages.unabletomessage": "You are unable to message this user", "addon.messages.unblockuser": "Unblock user", "addon.messages.unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", + "addon.messages.userwouldliketocontactyou": "{{$a}} would like to contact you", "addon.messages.warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", "addon.messages.warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", + "addon.messages.wouldliketocontactyou": "Would like to contact you", "addon.messages.you": "You:", + "addon.messages.youhaveblockeduser": "You have blocked this user in the past", + "addon.messages.yourcontactrequestpending": "Your contact request is pending with {{$a}}", "addon.mod_assign.acceptsubmissionstatement": "Please accept the submission statement.", "addon.mod_assign.addattempt": "Allow another attempt", "addon.mod_assign.addnewattempt": "Add a new attempt", @@ -1540,6 +1563,7 @@ "core.quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.", "core.redirectingtosite": "You will be redirected to the site.", "core.refresh": "Refresh", + "core.remove": "Remove", "core.required": "Required", "core.requireduserdatamissing": "This user lacks some required profile data. Please enter the data in your site and try again.
{{$a}}", "core.resources": "Resources", diff --git a/src/lang/en.json b/src/lang/en.json index 70dd8010f..910e6d617 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -188,6 +188,7 @@ "quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.", "redirectingtosite": "You will be redirected to the site.", "refresh": "Refresh", + "remove": "Remove", "required": "Required", "requireduserdatamissing": "This user lacks some required profile data. Please enter the data in your site and try again.
{{$a}}", "resources": "Resources", From f85c06eff54a4318e57b7e1df0cac58fcd5f7662 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 12:57:23 +0100 Subject: [PATCH 03/15] MOBILE-2620 context-menu: Allow menu items without icon --- src/components/context-menu/context-menu-popover.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/context-menu/context-menu-popover.ts b/src/components/context-menu/context-menu-popover.ts index 228df1fa3..7aa987c24 100644 --- a/src/components/context-menu/context-menu-popover.ts +++ b/src/components/context-menu/context-menu-popover.ts @@ -54,11 +54,7 @@ export class CoreContextMenuPopoverComponent { event.preventDefault(); event.stopPropagation(); - if (!item.iconAction) { - this.logger.warn('Items with action must have an icon action to work', item); - - return false; - } else if (item.iconAction == 'spinner') { + if (item.iconAction == 'spinner') { return false; } From e7be2dceb74dd23b3924de2e1219fa8467611763 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 12:58:32 +0100 Subject: [PATCH 04/15] MOBILE-2620 context-menu: Hide menu if no items are added --- src/components/context-menu/context-menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts index 7ccc09e46..5d453384f 100644 --- a/src/components/context-menu/context-menu.ts +++ b/src/components/context-menu/context-menu.ts @@ -32,7 +32,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { @Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon. @Input() title?: string; // Aria label and text to be shown on the top of the popover. - hideMenu: boolean; + hideMenu = true; // It will be unhidden when items are added. ariaLabel: string; protected items: CoreContextMenuItemComponent[] = []; protected itemsMovedToParent: CoreContextMenuItemComponent[] = []; From c5e4c403c6e3f138619bd6de79f944f78f8aeaba Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 13:02:47 +0100 Subject: [PATCH 05/15] MOBILE-2620 messages: New web services for contacts and user search --- .../components/discussions/discussions.ts | 2 +- src/addon/messages/providers/messages.ts | 496 +++++++++++++++++- 2 files changed, 476 insertions(+), 22 deletions(-) diff --git a/src/addon/messages/components/discussions/discussions.ts b/src/addon/messages/components/discussions/discussions.ts index a0241e2a1..8e90279ea 100644 --- a/src/addon/messages/components/discussions/discussions.ts +++ b/src/addon/messages/components/discussions/discussions.ts @@ -209,7 +209,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy { return this.messagesProvider.searchMessages(query).then((searchResults) => { this.search.showResults = true; - this.search.results = searchResults; + this.search.results = searchResults.messages; }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); }).finally(() => { diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index e6f859509..1ae0b3453 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -21,6 +21,8 @@ import { AddonMessagesOfflineProvider } from './messages-offline'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSite } from '@classes/site'; /** * Service to handle messages. @@ -28,7 +30,6 @@ import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper'; @Injectable() export class AddonMessagesProvider { protected ROOT_CACHE_KEY = 'mmaMessages:'; - protected LIMIT_SEARCH_MESSAGES = 50; protected LIMIT_MESSAGES = AddonMessagesProvider.LIMIT_MESSAGES; static NEW_MESSAGE_EVENT = 'addon_messages_new_message_event'; static READ_CHANGED_EVENT = 'addon_messages_read_changed_event'; @@ -36,6 +37,8 @@ export class AddonMessagesProvider { static OPEN_CONVERSATION_EVENT = 'addon_messages_open_conversation_event'; // Notify that a conversation should be opened. static SPLIT_VIEW_LOAD_EVENT = 'addon_messages_split_view_load_event'; static UPDATE_CONVERSATION_LIST_EVENT = 'addon_messages_update_conversation_list_event'; + static MEMBER_INFO_CHANGED_EVENT = 'addon_messages_member_changed_event'; + static CONTACT_REQUESTS_COUNT_EVENT = 'addon_messages_contact_requests_count_event'; static POLL_INTERVAL = 10000; static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation'; @@ -44,7 +47,10 @@ export class AddonMessagesProvider { static MESSAGE_PRIVACY_SITE = 2; // Privacy setting for being messaged by anyone on the site. static MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1; // An individual conversation. static MESSAGE_CONVERSATION_TYPE_GROUP = 2; // A group conversation. + static LIMIT_CONTACTS = 50; static LIMIT_MESSAGES = 50; + static LIMIT_INITIAL_USER_SEARCH = 3; + static LIMIT_SEARCH = 50; static NOTIFICATION_PREFERENCES_KEY = 'message_provider_moodle_instantmessage'; @@ -53,7 +59,7 @@ export class AddonMessagesProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private userProvider: CoreUserProvider, private messagesOffline: AddonMessagesOfflineProvider, private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider, - private emulatorHelper: CoreEmulatorHelperProvider) { + private emulatorHelper: CoreEmulatorHelperProvider, private eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('AddonMessagesProvider'); } @@ -63,6 +69,7 @@ export class AddonMessagesProvider { * @param {number} userId User ID of the person to add. * @param {string} [siteId] Site ID. If not defined, use current site. * @return {Promise} Resolved when done. + * @deprecated since Moodle 3.6 */ addContact(userId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -101,7 +108,90 @@ export class AddonMessagesProvider { } return promise.then(() => { - return this.invalidateAllContactsCache(site.getUserId(), site.getId()); + return this.invalidateAllMemberInfo(userId, site).finally(() => { + const data = { userId, userBlocked: true }; + this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id); + }); + }); + }); + } + + /** + * Confirm a contact request from another user. + * + * @param {number} userId ID of the user who made the contact request. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved when done. + * @since 3.6 + */ + confirmContactRequest(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userid: userId, + requesteduserid: site.getUserId(), + }; + + return site.write('core_message_confirm_contact_request', params).then(() => { + return this.utils.allPromises([ + this.invalidateAllMemberInfo(userId, site), + this.invalidateContactsCache(site.id), + this.invalidateUserContacts(site.id), + this.refreshContactRequestsCount(site.id), + ]).finally(() => { + const data = { userId, contactRequestConfirmed: true }; + this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id); + }); + }); + }); + } + + /** + * Send a contact request to another user. + * + * @param {number} userId ID of the receiver of the contact request. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved when done. + * @since 3.6 + */ + createContactRequest(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userid: site.getUserId(), + requesteduserid: userId, + }; + + return site.write('core_message_create_contact_request', params).then(() => { + return this.invalidateAllMemberInfo(userId, site).finally(() => { + const data = { userId, contactRequestCreated: true }; + this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id); + }); + }); + }); + } + + /** + * Decline a contact request from another user. + * + * @param {number} userId ID of the user who made the contact request. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved when done. + * @since 3.6 + */ + declineContactRequest(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userid: userId, + requesteduserid: site.getUserId(), + }; + + return site.write('core_message_decline_contact_request', params).then(() => { + return this.utils.allPromises([ + this.invalidateAllMemberInfo(userId, site), + this.refreshContactRequestsCount(site.id), + ]).finally(() => { + const data = { userId, contactRequestDeclined: true }; + this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id); + }); }); }); } @@ -247,6 +337,33 @@ export class AddonMessagesProvider { return this.ROOT_CACHE_KEY + 'contacts'; } + /** + * Get the cache key for comfirmed contacts. + * + * @return {string} Cache key. + */ + protected getCacheKeyForUserContacts(): string { + return this.ROOT_CACHE_KEY + 'userContacts'; + } + + /** + * Get the cache key for contact requests. + * + * @return {string} Cache key. + */ + protected getCacheKeyForContactRequests(): string { + return this.ROOT_CACHE_KEY + 'contactRequests'; + } + + /** + * Get the cache key for contact requests count. + * + * @return {string} Cache key. + */ + protected getCacheKeyForContactRequestsCount(): string { + return this.ROOT_CACHE_KEY + 'contactRequestsCount'; + } + /** * Get the cache key for a discussion. * @@ -332,6 +449,17 @@ export class AddonMessagesProvider { return this.getCommonCacheKeyForUserConversations(userId) + ':' + type + ':' + favourites; } + /** + * Get cache key for member info. + * + * @param {number} userId User ID. + * @param {number} otherUserId The other user ID. + * @return {string} Cache key. + */ + protected getCacheKeyForMemberInfo(userId: number, otherUserId: number): string { + return this.ROOT_CACHE_KEY + 'memberInfo:' + userId + ':' + otherUserId; + } + /** * Get common cache key for get user conversations. * @@ -356,6 +484,7 @@ export class AddonMessagesProvider { * * @param {string} [siteId] Site ID. If not defined, use current site. * @return {Promise} Resolved with the WS data. + * @deprecated since Moodle 3.6 */ getAllContacts(siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); @@ -377,7 +506,7 @@ export class AddonMessagesProvider { } /** - * Get all the blocked contacts of the current user. + * Get all the users blocked by the current user. * * @param {string} [siteId] Site ID. If not defined, use current site. * @return {Promise} Resolved with the WS data. @@ -403,6 +532,7 @@ export class AddonMessagesProvider { * * @param {string} [siteId] Site ID. If not defined, use current site. * @return {Promise} Resolved with the WS data. + * @deprecated since Moodle 3.6 */ getContacts(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -430,6 +560,114 @@ export class AddonMessagesProvider { }); } + /** + * Get the list of user contacts. + * + * @param {number} [limitFrom=0] Position of the first contact to fetch. + * @param {number} [limitNum] Number of contacts to fetch. Default is AddonMessagesProvider.LIMIT_CONTACTS. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise<{contacts: any[], canLoadMore: boolean}>} Resolved with the list of user contacts. + * @since 3.6 + */ + getUserContacts(limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_CONTACTS , siteId?: string): + Promise<{contacts: any[], canLoadMore: boolean}> { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userid: site.getUserId(), + limitfrom: limitFrom, + limitnum: limitNum <= 0 ? 0 : limitNum + 1 + }; + const preSets = { + cacheKey: this.getCacheKeyForUserContacts() + }; + + return site.read('core_message_get_user_contacts', params, preSets).then((contacts) => { + if (!contacts || !contacts.length) { + return { contacts: [], canLoadMore: false }; + } + + this.userProvider.storeUsers(contacts, site.id); + + if (limitNum <= 0) { + return { contacts, canLoadMore: false }; + } + + return { + contacts: contacts.slice(0, limitNum), + canLoadMore: contacts.length > limitNum + }; + }); + }); + } + + /** + * Get the contact request sent to the current user. + * + * @param {number} [limitFrom=0] Position of the first contact request to fetch. + * @param {number} [limitNum] Number of contact requests to fetch. Default is AddonMessagesProvider.LIMIT_CONTACTS. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise<{requests: any[], canLoadMore: boolean}>} Resolved with the list of contact requests. + * @since 3.6 + */ + getContactRequests(limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_CONTACTS, siteId?: string): + Promise<{requests: any[], canLoadMore: boolean}> { + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + userid: site.getUserId(), + limitfrom: limitFrom, + limitnum: limitNum <= 0 ? 0 : limitNum + 1 + }; + const preSets = { + cacheKey: this.getCacheKeyForContactRequests() + }; + + return site.read('core_message_get_contact_requests', data, preSets).then((requests) => { + if (!requests || !requests.length) { + return { requests: [], canLoadMore: false }; + } + + this.userProvider.storeUsers(requests, site.id); + + if (limitNum <= 0) { + return { requests, canLoadMore: false }; + } + + return { + requests: requests.slice(0, limitNum), + canLoadMore: requests.length > limitNum + }; + }); + }); + } + + /** + * Get the number of contact requests sent to the current user. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with the number of contact requests. + * @since 3.6 + */ + getContactRequestsCount(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + userid: site.getUserId(), + }; + const preSets = { + cacheKey: this.getCacheKeyForContactRequestsCount(), + typeExpected: 'number' + }; + + return site.read('core_message_get_received_contact_requests_count', data, preSets).then((count) => { + // Notify the new count so all badges are updated. + this.eventsProvider.trigger(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, { count }, site.id); + + return count; + }); + }); + } + /** * Get a conversation by the conversation ID. * @@ -491,18 +729,20 @@ export class AddonMessagesProvider { * @param {boolean} [newestFirst=true] Whether to order messages by newest first. * @param {string} [siteId] Site ID. If not defined, use current site. * @param {number} [userId] User ID. If not defined, current user in the site. + * @param {boolean} [preferCache] True if shouldn't call WS if data is cached, false otherwise. * @return {Promise} Promise resolved with the response. * @since 3.6 */ getConversationBetweenUsers(otherUserId: number, includeContactRequests?: boolean, includePrivacyInfo?: boolean, messageOffset: number = 0, messageLimit: number = 1, memberOffset: number = 0, memberLimit: number = 2, - newestFirst: boolean = true, siteId?: string, userId?: number): Promise { + newestFirst: boolean = true, siteId?: string, userId?: number, preferCache?: boolean): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); const preSets = { - cacheKey: this.getCacheKeyForConversationBetweenUsers(userId, otherUserId) + cacheKey: this.getCacheKeyForConversationBetweenUsers(userId, otherUserId), + omitExpires: !!preferCache, }, params: any = { userid: userId, @@ -900,6 +1140,40 @@ export class AddonMessagesProvider { }); } + /** + * Get conversation member info by user id, works even if no conversation betwen the users exists. + * + * @param {number} otherUserId The other user ID. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Promise resolved with the member info. + * @since 3.6 + */ + getMemberInfo(otherUserId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const preSets = { + cacheKey: this.getCacheKeyForMemberInfo(userId, otherUserId) + }, + params: any = { + referenceuserid: userId, + userids: [otherUserId], + includecontactrequests: 1, + includeprivacyinfo: 1, + }; + + return site.read('core_message_get_member_info', params, preSets).then((members) => { + if (!members || members.length < 1) { + // Should never happen. + return Promise.reject(null); + } + + return members[0]; + }); + }); + } + /** * Get the cache key for the get message preferences call. * @@ -1146,6 +1420,42 @@ export class AddonMessagesProvider { }); } + /** + * Invalidate user contacts cache. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateUserContacts(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCacheKeyForUserContacts()); + }); + } + + /** + * Invalidate contact requests cache. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateContactRequestsCache(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCacheKeyForContactRequests()); + }); + } + + /** + * Invalidate contact requests count cache. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateContactRequestsCountCache(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCacheKeyForContactRequestsCount()); + }); + } + /** * Invalidate conversation. * @@ -1256,6 +1566,22 @@ export class AddonMessagesProvider { }); } + /** + * Invalidate member info cache. + * + * @param {number} otherUserId The other user ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Resolved when done. + */ + invalidateMemberInfo(otherUserId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getCacheKeyForMemberInfo(userId, otherUserId)); + }); + } + /** * Invalidate get message preferences. * @@ -1268,6 +1594,31 @@ export class AddonMessagesProvider { }); } + /** + * Invalidate all cache entries with member info. + * + * @param {number} userId Id of the user to invalidate. + * @param {CoreSite} site Site object. + * @return {Promie} Promise resolved when done. + */ + protected invalidateAllMemberInfo(userId: number, site: CoreSite): Promise { + return this.utils.allPromises([ + this.invalidateMemberInfo(userId, site.id), + this.invalidateUserContacts(site.id), + this.invalidateContactRequestsCache(site.id), + this.invalidateConversations(site.id), + this.getConversationBetweenUsers(userId, undefined, undefined, undefined, undefined, undefined, undefined, undefined, + site.id, undefined, true).then((conversation) => { + return this.utils.allPromises([ + this.invalidateConversation(conversation.id), + this.invalidateConversationMembers(conversation.id, site.id), + ]); + }).catch(() => { + // The conversation does not exist or we can't fetch it now, ignore it. + }) + ]); + } + /** * Checks if the a user is blocked by the current user. * @@ -1276,6 +1627,12 @@ export class AddonMessagesProvider { * @return {Promise} Resolved with boolean, rejected when we do not know. */ isBlocked(userId: number, siteId?: string): Promise { + if (this.isGroupMessagingEnabled()) { + return this.getMemberInfo(userId, siteId).then((member) => { + return member.isblocked; + }); + } + return this.getBlockedContacts(siteId).then((blockedContacts) => { if (!blockedContacts.users || blockedContacts.users.length < 1) { return false; @@ -1295,6 +1652,12 @@ export class AddonMessagesProvider { * @return {Promise} Resolved with boolean, rejected when we do not know. */ isContact(userId: number, siteId?: string): Promise { + if (this.isGroupMessagingEnabled()) { + return this.getMemberInfo(userId, siteId).then((member) => { + return member.iscontact; + }); + } + return this.getContacts(siteId).then((contacts) => { return ['online', 'offline'].some((type) => { if (contacts[type] && contacts[type].length > 0) { @@ -1438,6 +1801,21 @@ export class AddonMessagesProvider { return this.sitesProvider.getCurrentSite().write('core_message_mark_all_messages_as_read', params, preSets); } + /** + * Refresh the number of contact requests sent to the current user. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with the number of contact requests. + * @since 3.6 + */ + refreshContactRequestsCount(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.invalidateContactRequestsCountCache(siteId).then(() => { + return this.getContactRequestsCount(siteId); + }); + } + /** * Remove a contact. * @@ -1455,7 +1833,17 @@ export class AddonMessagesProvider { }; return site.write('core_message_delete_contacts', params, preSets).then(() => { - return this.invalidateContactsCache(site.getId()); + if (this.isGroupMessagingEnabled()) { + return this.utils.allPromises([ + this.invalidateUserContacts(site.id), + this.invalidateAllMemberInfo(userId, site), + ]).then(() => { + const data = { userId, contactRemoved: true }; + this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id); + }); + } else { + return this.invalidateContactsCache(site.id); + } }); }); } @@ -1496,28 +1884,91 @@ export class AddonMessagesProvider { /** * Search for all the messges with a specific text. * - * @param {string} query The query string - * @param {number} [userId] The user ID. If not defined, current user. - * @param {number} [from=0] Position of the first result to get. Defaults to 0. - * @param {number} [limit] Number of results to get. Defaults to LIMIT_SEARCH_MESSAGES. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with the results. + * @param {string} query The query string. + * @param {number} [userId] The user ID. If not defined, current user. + * @param {number} [limitFrom=0] Position of the first result to get. Defaults to 0. + * @param {number} [limitNum] Number of results to get. Defaults to AddonMessagesProvider.LIMIT_SEARCH. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the results. */ - searchMessages(query: string, userId?: number, from: number = 0, limit: number = this.LIMIT_SEARCH_MESSAGES, siteId?: string): - Promise { + searchMessages(query: string, userId?: number, limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_SEARCH, + siteId?: string): Promise<{messages: any[], canLoadMore: boolean}> { + return this.sitesProvider.getSite(siteId).then((site) => { - const param = { + const params = { userid: userId || site.getUserId(), search: query, - limitfrom: from, - limitnum: limit + limitfrom: limitFrom, + limitnum: limitNum <= 0 ? 0 : limitNum + 1 }, preSets = { getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. }; - return site.read('core_message_data_for_messagearea_search_messages', param, preSets).then((searchResults) => { - return searchResults.contacts; + return site.read('core_message_data_for_messagearea_search_messages', params, preSets).then((result) => { + if (!result.contacts || !result.contacts.length) { + return { messages: [], canLoadMore: false }; + } + + result.contacts.forEach((result) => { + result.id = result.userid; + }); + + this.userProvider.storeUsers(result.contacts, site.id); + + if (limitNum <= 0) { + return { messages: result.contacts, canLoadMore: false }; + } + + return { + messages: result.contacts.slice(0, limitNum), + canLoadMore: result.contacts.length > limitNum + }; + }); + }); + } + + /** + * Search for users. + * + * @param {string} query Text to search for. + * @param {number} [limitFrom=0] Position of the first found user to fetch. + * @param {number} [limitNum] Number of found users to fetch. Defaults to AddonMessagesProvider.LIMIT_SEARCH. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with two lists of found users: contacts and non-contacts. + * @since 3.6 + */ + searchUsers(query: string, limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_SEARCH, siteId?: string): + Promise<{contacts: any[], nonContacts: any[], canLoadMoreContacts: boolean, canLoadMoreNonContacts: boolean}> { + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + userid: site.getUserId(), + search: query, + limitfrom: limitFrom, + limitnum: limitNum <= 0 ? 0 : limitNum + 1 + }, + preSets = { + getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. + }; + + return site.read('core_message_message_search_users', data, preSets).then((result) => { + const contacts = result.contacts || []; + const nonContacts = result.noncontacts || []; + + this.userProvider.storeUsers(contacts, site.id); + this.userProvider.storeUsers(nonContacts, site.id); + + if (limitNum <= 0) { + return { contacts, nonContacts, canLoadMoreContacts: false, canLoadMoreNonContacts: false }; + } + + return { + contacts: contacts.slice(0, limitNum), + nonContacts: nonContacts.slice(0, limitNum), + canLoadMoreContacts: contacts.length > limitNum, + canLoadMoreNonContacts: nonContacts.length > limitNum + }; }); }); } @@ -1918,7 +2369,10 @@ export class AddonMessagesProvider { } return promise.then(() => { - return this.invalidateAllContactsCache(site.getUserId(), site.getId()); + return this.invalidateAllMemberInfo(userId, site).finally(() => { + const data = { userId, userUnblocked: true }; + this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id); + }); }); }); } From 481351c6821628e2b7570271490cdd33d5cfe96c Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 13:06:00 +0100 Subject: [PATCH 06/15] MOBILE-2620 messages: Combined search page --- src/addon/messages/pages/search/search.html | 56 ++++ .../messages/pages/search/search.module.ts | 37 +++ src/addon/messages/pages/search/search.ts | 244 ++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 src/addon/messages/pages/search/search.html create mode 100644 src/addon/messages/pages/search/search.module.ts create mode 100644 src/addon/messages/pages/search/search.ts diff --git a/src/addon/messages/pages/search/search.html b/src/addon/messages/pages/search/search.html new file mode 100644 index 000000000..1acad9cb8 --- /dev/null +++ b/src/addon/messages/pages/search/search.html @@ -0,0 +1,56 @@ + + + {{ 'addon.messages.searchcombined' | translate }} + + + + + + + + + + + + + + + + + + + + + + + + {{ item.titleString | translate }} + + {{ item.emptyString | translate }} + + + + + +

+ + +

+ + {{result.lastmessagedate | coreDateDayOrTime}} + + +
+ + + +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/addon/messages/pages/search/search.module.ts b/src/addon/messages/pages/search/search.module.ts new file mode 100644 index 000000000..c7513f688 --- /dev/null +++ b/src/addon/messages/pages/search/search.module.ts @@ -0,0 +1,37 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonMessagesSearchPage } from './search'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonMessagesComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + AddonMessagesSearchPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + AddonMessagesComponentsModule, + IonicPageModule.forChild(AddonMessagesSearchPage), + TranslateModule.forChild() + ], +}) +export class AddonMessagesSearchPageModule {} diff --git a/src/addon/messages/pages/search/search.ts b/src/addon/messages/pages/search/search.ts new file mode 100644 index 000000000..205d10427 --- /dev/null +++ b/src/addon/messages/pages/search/search.ts @@ -0,0 +1,244 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnDestroy, ViewChild } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreAppProvider } from '@providers/app'; + +/** + * Page for searching users. + */ +@IonicPage({ segment: 'addon-messages-search' }) +@Component({ + selector: 'page-addon-messages-search', + templateUrl: 'search.html', +}) +export class AddonMessagesSearchPage implements OnDestroy { + + disableSearch = false; + displaySearching = false; + displayResults = false; + query = ''; + contacts = { + type: 'contacts', + titleString: 'addon.messages.contacts', + emptyString: 'addon.messages.searchnocontactsfound', + results: [], + canLoadMore: false, + loadingMore: false + }; + nonContacts = { + type: 'noncontacts', + titleString: 'addon.messages.noncontacts', + emptyString: 'addon.messages.searchnononcontactsfound', + results: [], + canLoadMore: false, + loadingMore: false + }; + messages = { + type: 'messages', + titleString: 'addon.messages.messages', + emptyString: 'addon.messages.searchnomessagesfound', + results: [], + canLoadMore: false, + loadingMore: false, + loadMoreError: false + }; + selectedUserId = null; + + protected memberInfoObserver; + + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + constructor(private appProvider: CoreAppProvider, private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, + sitesProvider: CoreSitesProvider, private messagesProvider: AddonMessagesProvider) { + + // Update block status of a user. + this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => { + if (!data.userBlocked && !data.userUnblocked) { + // The block status has not changed, ignore. + return; + } + + const contact = this.contacts.results.find((user) => user.id == data.userId); + if (contact) { + contact.isblocked = data.userBlocked; + } else { + const nonContact = this.nonContacts.results.find((user) => user.id == data.userId); + if (nonContact) { + nonContact.isblocked = data.userBlocked; + } + } + + this.messages.results.forEach((message: any): void => { + if (message.userid == data.userId) { + message.isblocked = data.userBlocked; + } + }); + }, sitesProvider.getCurrentSiteId()); + } + + /** + * Clear search. + */ + clearSearch(): void { + this.query = ''; + this.displayResults = false; + this.splitviewCtrl.emptyDetails(); + } + + /** + * Start a new search or load more results. + * + * @param {string} query Text to search for. + * @param {strings} loadMore Load more contacts, noncontacts or messages. If undefined, start a new search. + * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. + * @return {Promise} Resolved when done. + */ + search(query: string, loadMore?: 'contacts' | 'noncontacts' | 'messages', infiniteComplete?: any): Promise { + this.appProvider.closeKeyboard(); + + this.query = query; + this.disableSearch = true; + this.displaySearching = !loadMore; + + const promises = []; + let newContacts = []; + let newNonContacts = []; + let newMessages = []; + let canLoadMoreContacts = false; + let canLoadMoreNonContacts = false; + let canLoadMoreMessages = false; + + if (!loadMore || loadMore == 'contacts' || loadMore == 'noncontacts') { + const limitNum = loadMore ? AddonMessagesProvider.LIMIT_SEARCH : AddonMessagesProvider.LIMIT_INITIAL_USER_SEARCH; + let limitFrom = 0; + if (loadMore == 'contacts') { + limitFrom = this.contacts.results.length; + this.contacts.loadingMore = true; + } else if (loadMore == 'noncontacts') { + limitFrom = this.nonContacts.results.length; + this.nonContacts.loadingMore = true; + } + + promises.push( + this.messagesProvider.searchUsers(query, limitFrom, limitNum).then((result) => { + if (!loadMore || loadMore == 'contacts') { + newContacts = result.contacts; + canLoadMoreContacts = result.canLoadMoreContacts; + } + if (!loadMore || loadMore == 'noncontacts') { + newNonContacts = result.nonContacts; + canLoadMoreNonContacts = result.canLoadMoreNonContacts; + } + }) + ); + } + + if (!loadMore || loadMore == 'messages') { + let limitFrom = 0; + if (loadMore == 'messages') { + limitFrom = this.messages.results.length; + this.messages.loadingMore = true; + } + + promises.push( + this.messagesProvider.searchMessages(query, undefined, limitFrom).then((result) => { + newMessages = result.messages; + canLoadMoreMessages = result.canLoadMore; + }) + ); + } + + return Promise.all(promises).then(() => { + if (!loadMore) { + this.contacts.results = []; + this.nonContacts.results = []; + this.messages.results = []; + } + + this.displayResults = true; + + if (!loadMore || loadMore == 'contacts') { + this.contacts.results.push(...newContacts); + this.contacts.canLoadMore = canLoadMoreContacts; + } + + if (!loadMore || loadMore == 'noncontacts') { + this.nonContacts.results.push(...newNonContacts); + this.nonContacts.canLoadMore = canLoadMoreNonContacts; + } + + if (!loadMore || loadMore == 'messages') { + this.messages.results.push(...newMessages); + this.messages.canLoadMore = canLoadMoreMessages; + this.messages.loadMoreError = false; + } + + if (!loadMore) { + if (this.contacts.results.length > 0) { + this.openDiscussion(this.contacts.results[0].id, true); + } else if (this.nonContacts.results.length > 0) { + this.openDiscussion(this.nonContacts.results[0].id, true); + } else if (this.messages.results.length > 0) { + this.openDiscussion(this.messages.results[0].userid, true); + } + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingusers', true); + + if (loadMore == 'messages') { + this.messages.loadMoreError = true; + } + }).finally(() => { + this.disableSearch = false; + this.displaySearching = false; + + if (loadMore == 'contacts') { + this.contacts.loadingMore = false; + } else if (loadMore == 'noncontacts') { + this.nonContacts.loadingMore = false; + } else if (loadMore == 'messages') { + this.messages.loadingMore = false; + } + + infiniteComplete && infiniteComplete(); + }); + } + + /** + * Open a discussion in the split view. + * + * @param {number} userId User id. + * @param {boolean} [onInit=false] Whether the tser was selected on initial load. + */ + openDiscussion(userId: number, onInit: boolean = false): void { + if (!onInit || this.splitviewCtrl.isOn()) { + this.selectedUserId = userId; + this.splitviewCtrl.push('AddonMessagesDiscussionPage', { userId }); + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.memberInfoObserver && this.memberInfoObserver.off(); + } +} From 17ba42c9aa2b001153a73487af2f0a2f5289568f Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 13:08:24 +0100 Subject: [PATCH 07/15] MOBILE-2620 messages: Contacts & contact requests page --- .../messages/components/components.module.ts | 6 + .../addon-messages-confirmed-contacts.html | 16 ++ .../confirmed-contacts/confirmed-contacts.ts | 141 ++++++++++++++++++ .../addon-messages-contact-requests.html | 16 ++ .../contact-requests/contact-requests.ts | 137 +++++++++++++++++ .../messages/pages/contacts/contacts.html | 28 ++++ .../pages/contacts/contacts.module.ts | 37 +++++ src/addon/messages/pages/contacts/contacts.ts | 117 +++++++++++++++ 8 files changed, 498 insertions(+) create mode 100644 src/addon/messages/components/confirmed-contacts/addon-messages-confirmed-contacts.html create mode 100644 src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts create mode 100644 src/addon/messages/components/contact-requests/addon-messages-contact-requests.html create mode 100644 src/addon/messages/components/contact-requests/contact-requests.ts create mode 100644 src/addon/messages/pages/contacts/contacts.html create mode 100644 src/addon/messages/pages/contacts/contacts.module.ts create mode 100644 src/addon/messages/pages/contacts/contacts.ts diff --git a/src/addon/messages/components/components.module.ts b/src/addon/messages/components/components.module.ts index 929d642f4..1c8091937 100644 --- a/src/addon/messages/components/components.module.ts +++ b/src/addon/messages/components/components.module.ts @@ -20,11 +20,15 @@ import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions'; +import { AddonMessagesConfirmedContactsComponent } from '../components/confirmed-contacts/confirmed-contacts'; +import { AddonMessagesContactRequestsComponent } from '../components/contact-requests/contact-requests'; import { AddonMessagesContactsComponent } from '../components/contacts/contacts'; @NgModule({ declarations: [ AddonMessagesDiscussionsComponent, + AddonMessagesConfirmedContactsComponent, + AddonMessagesContactRequestsComponent, AddonMessagesContactsComponent ], imports: [ @@ -39,6 +43,8 @@ import { AddonMessagesContactsComponent } from '../components/contacts/contacts' ], exports: [ AddonMessagesDiscussionsComponent, + AddonMessagesConfirmedContactsComponent, + AddonMessagesContactRequestsComponent, AddonMessagesContactsComponent ] }) diff --git a/src/addon/messages/components/confirmed-contacts/addon-messages-confirmed-contacts.html b/src/addon/messages/components/confirmed-contacts/addon-messages-confirmed-contacts.html new file mode 100644 index 000000000..cea86c838 --- /dev/null +++ b/src/addon/messages/components/confirmed-contacts/addon-messages-confirmed-contacts.html @@ -0,0 +1,16 @@ + + + + + + + + +

+ +
+
+ + +
+
diff --git a/src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts b/src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts new file mode 100644 index 000000000..7a4a3d1da --- /dev/null +++ b/src/addon/messages/components/confirmed-contacts/confirmed-contacts.ts @@ -0,0 +1,141 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; +import { Content } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Component that displays the list of confirmed contacts. + */ +@Component({ + selector: 'addon-messages-confirmed-contacts', + templateUrl: 'addon-messages-confirmed-contacts.html', +}) +export class AddonMessagesConfirmedContactsComponent implements OnInit, OnDestroy { + @Output() onUserSelected = new EventEmitter<{userId: number, onInit?: boolean}>(); + @ViewChild(Content) content: Content; + + loaded = false; + canLoadMore = false; + loadMoreError = false; + contacts = []; + selectedUserId: number; + + protected memberInfoObserver; + + constructor(private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + private messagesProvider: AddonMessagesProvider) { + + this.onUserSelected = new EventEmitter(); + + // Update block status of a user. + this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => { + if (data.userBlocked || data.userUnblocked) { + const user = this.contacts.find((user) => user.id == data.userId); + if (user) { + user.isblocked = data.userBlocked; + } + } else if (data.contactRemoved) { + const index = this.contacts.findIndex((contact) => contact.id == data.userId); + if (index >= 0) { + this.contacts.splice(index, 1); + } + } + }, sitesProvider.getCurrentSiteId()); + } + + /** + * Component loaded. + */ + ngOnInit(): void { + this.fetchData().then(() => { + if (this.contacts.length) { + this.selectUser(this.contacts[0].id, true); + } + }).finally(() => { + this.loaded = true; + }); + + // Workaround for infinite scrolling. + this.content.resize(); + } + + /** + * Fetch contacts. + * + * @param {boolean} [refresh=false] True if we are refreshing contacts, false if we are loading more. + * @return {Promise} Promise resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + this.loadMoreError = false; + + const limitFrom = refresh ? 0 : this.contacts.length; + + return this.messagesProvider.getUserContacts(limitFrom).then((result) => { + this.contacts = refresh ? result.contacts : this.contacts.concat(result.contacts); + this.canLoadMore = result.canLoadMore; + }).catch((error) => { + this.loadMoreError = true; + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); + }); + } + + /** + * Refresh contacts. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + refreshData(refresher?: any): Promise { + return this.messagesProvider.invalidateUserContacts().then(() => { + return this.fetchData(true); + }).finally(() => { + refresher && refresher.complete(); + }); + } + + /** + * Load more contacts. + * + * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. + * @return {Promise} Resolved when done. + */ + loadMore(infiniteComplete?: any): Promise { + return this.fetchData().finally(() => { + infiniteComplete && infiniteComplete(); + }); + } + + /** + * Notify that a contact has been selected. + * + * @param {number} userId User id. + * @param {boolean} [onInit=false] Whether the contact is selected on initial load. + */ + selectUser(userId: number, onInit: boolean = false): void { + this.selectedUserId = userId; + this.onUserSelected.emit({userId, onInit}); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.memberInfoObserver && this.memberInfoObserver.off(); + } +} diff --git a/src/addon/messages/components/contact-requests/addon-messages-contact-requests.html b/src/addon/messages/components/contact-requests/addon-messages-contact-requests.html new file mode 100644 index 000000000..d55408208 --- /dev/null +++ b/src/addon/messages/components/contact-requests/addon-messages-contact-requests.html @@ -0,0 +1,16 @@ + + + + + + + + +

+

{{ 'addon.messages.wouldliketocontactyou' | translate }}

+
+
+ + +
+
diff --git a/src/addon/messages/components/contact-requests/contact-requests.ts b/src/addon/messages/components/contact-requests/contact-requests.ts new file mode 100644 index 000000000..854bd90bb --- /dev/null +++ b/src/addon/messages/components/contact-requests/contact-requests.ts @@ -0,0 +1,137 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; +import { Content } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Component that displays the list of contact requests. + */ +@Component({ + selector: 'addon-messages-contact-requests', + templateUrl: 'addon-messages-contact-requests.html', +}) +export class AddonMessagesContactRequestsComponent implements OnInit, OnDestroy { + @Output() onUserSelected = new EventEmitter<{userId: number, onInit?: boolean}>(); + @ViewChild(Content) content: Content; + + loaded = false; + canLoadMore = false; + loadMoreError = false; + requests = []; + selectedUserId: number; + + protected memberInfoObserver; + + constructor(private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + private messagesProvider: AddonMessagesProvider) { + + // Hide the "Would like to contact you" message when a contact request is confirmed. + this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => { + if (data.contactRequestConfirmed || data.contactRequestDeclined) { + const index = this.requests.findIndex((request) => request.id == data.userId); + if (index >= 0) { + this.requests.splice(index, 1); + } + } + }, sitesProvider.getCurrentSiteId()); + } + + /** + * Component loaded. + */ + ngOnInit(): void { + this.fetchData().then(() => { + if (this.requests.length) { + this.selectUser(this.requests[0].id, true); + } + }).finally(() => { + this.loaded = true; + }); + + // Workaround for infinite scrolling. + this.content.resize(); + } + + /** + * Fetch contact requests. + * + * @param {boolean} [refresh=false] True if we are refreshing contact requests, false if we are loading more. + * @return {Promise} Promise resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + this.loadMoreError = false; + + const limitFrom = refresh ? 0 : this.requests.length; + + return this.messagesProvider.getContactRequests(limitFrom).then((result) => { + this.requests = refresh ? result.requests : this.requests.concat(result.requests); + this.canLoadMore = result.canLoadMore; + }).catch((error) => { + this.loadMoreError = true; + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); + }); + } + + /** + * Refresh contact requests. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + refreshData(refresher?: any): Promise { + // Refresh the number of contacts requests to update badges. + this.messagesProvider.refreshContactRequestsCount(); + + return this.messagesProvider.invalidateContactRequestsCache().then(() => { + return this.fetchData(true); + }).finally(() => { + refresher && refresher.complete(); + }); + } + + /** + * Load more contact requests. + * + * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. + * @return {Promise} Resolved when done. + */ + loadMore(infiniteComplete?: any): Promise { + return this.fetchData().finally(() => { + infiniteComplete && infiniteComplete(); + }); + } + + /** + * Notify that a contact has been selected. + * + * @param {number} userId User id. + * @param {boolean} [onInit=false] Whether the contact is selected on initial load. + */ + selectUser(userId: number, onInit: boolean = false): void { + this.selectedUserId = userId; + this.onUserSelected.emit({userId, onInit}); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.memberInfoObserver && this.memberInfoObserver.off(); + } +} diff --git a/src/addon/messages/pages/contacts/contacts.html b/src/addon/messages/pages/contacts/contacts.html new file mode 100644 index 000000000..1316bd027 --- /dev/null +++ b/src/addon/messages/pages/contacts/contacts.html @@ -0,0 +1,28 @@ + + + {{ 'addon.messages.contacts' | translate }} + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/addon/messages/pages/contacts/contacts.module.ts b/src/addon/messages/pages/contacts/contacts.module.ts new file mode 100644 index 000000000..a69ec60b7 --- /dev/null +++ b/src/addon/messages/pages/contacts/contacts.module.ts @@ -0,0 +1,37 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonMessagesContactsPage } from './contacts'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonMessagesComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + AddonMessagesContactsPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + AddonMessagesComponentsModule, + IonicPageModule.forChild(AddonMessagesContactsPage), + TranslateModule.forChild() + ], +}) +export class AddonMessagesContactsPageModule {} diff --git a/src/addon/messages/pages/contacts/contacts.ts b/src/addon/messages/pages/contacts/contacts.ts new file mode 100644 index 000000000..5e88a8593 --- /dev/null +++ b/src/addon/messages/pages/contacts/contacts.ts @@ -0,0 +1,117 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnDestroy, ViewChild } from '@angular/core'; +import { IonicPage, NavController } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreTabsComponent } from '@components/tabs/tabs'; + +/** + * Page that displays contacts and contact requests. + */ +@IonicPage({ segment: 'addon-messages-contacts' }) +@Component({ + selector: 'page-addon-messages-contacts', + templateUrl: 'contacts.html', +}) +export class AddonMessagesContactsPage implements OnDestroy { + + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + @ViewChild(CoreTabsComponent) tabsComponent: CoreTabsComponent; + + contactRequestsCount = 0; + + protected loadSplitViewObserver: any; + protected siteId: string; + protected contactRequestsCountObserver: any; + protected conversationUserId: number; // User id of the conversation opened in the split view. + protected selectedUserId = { + contacts: null, // User id of the selected user in the confirmed contacts tab. + requests: null, // User id of the selected user in the contact requests tab. + }; + + constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + private navCtrl: NavController, private messagesProvider: AddonMessagesProvider) { + + this.siteId = sitesProvider.getCurrentSiteId(); + + // Update the contact requests badge. + this.contactRequestsCountObserver = eventsProvider.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => { + this.contactRequestsCount = data.count; + }, this.siteId); + } + + /** + * Page being initialized. + */ + ngOnInit(): void { + this.messagesProvider.getContactRequestsCount(this.siteId); // Badge already updated by the observer. + } + + /** + * Navigate to the search page. + */ + gotoSearch(): void { + this.navCtrl.push('AddonMessagesSearchPage'); + } + + /** + * User entered the page. + */ + ionViewDidEnter(): void { + this.tabsComponent && this.tabsComponent.ionViewDidEnter(); + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + this.tabsComponent && this.tabsComponent.ionViewDidLeave(); + } + + /** + * Set the selected user and open the conversation in the split view if needed. + * + * @param {string} tab Active tab: "contacts" or "requests". + * @param {number} [userId] Id of the selected user, undefined to use the last selected user in the tab. + * @param {boolean} [onInit=false] Whether the contact was selected on initial load. + */ + selectUser(tab: string, userId?: number, onInit: boolean = false): void { + userId = userId || this.selectedUserId[tab]; + + if (!userId || userId == this.conversationUserId) { + // No user conversation to open or it is already opened. + return; + } + + if (onInit && !this.splitviewCtrl.isOn()) { + // Do not open a conversation by default when split view is not visible. + return; + } + + this.conversationUserId = userId; + this.selectedUserId[tab] = userId; + this.splitviewCtrl.push('AddonMessagesDiscussionPage', { userId }); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.contactRequestsCountObserver && this.contactRequestsCountObserver.off(); + } +} From dca25d2e010f0c83a2e4d10ffddd2b7d12ec3e74 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 13:24:39 +0100 Subject: [PATCH 08/15] MOBILE-2620 messages: Link to contacts and search from conversations --- .../group-conversations.html | 40 +++---------- .../group-conversations.ts | 57 ++++++------------- 2 files changed, 24 insertions(+), 73 deletions(-) diff --git a/src/addon/messages/pages/group-conversations/group-conversations.html b/src/addon/messages/pages/group-conversations/group-conversations.html index 1e4757d0f..bbead5710 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.html +++ b/src/addon/messages/pages/group-conversations/group-conversations.html @@ -2,8 +2,8 @@ {{ 'addon.messages.messages' | translate }} - + +
+

{{ 'addon.messages.isnotinyourcontacts' | translate: {$a: otherMember.fullname} }}

+

{{ 'addon.messages.requirecontacttomessage' | translate: {$a: otherMember.fullname} }}

+ +
+
+

{{ 'addon.messages.userwouldliketocontactyou' | translate: {$a: otherMember.fullname} }}

+ + +
+
+

{{ 'addon.messages.contactrequestsent' | translate }}

+

{{ 'addon.messages.yourcontactrequestpending' | translate: {$a: otherMember.fullname} }}

+
+ diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index cd1d1d967..d4f6508da 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -57,6 +57,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { protected keyboardObserver: any; protected scrollBottom = true; protected viewDestroyed = false; + protected memberInfoObserver: any; + protected showLoadingModal = false; // Whether to show a loading modal while fetching data. conversationId: number; // Conversation ID. Undefined if it's a new individual conversation. conversation: any; // The conversation object (if it exists). @@ -77,6 +79,10 @@ export class AddonMessagesDiscussionPage implements OnDestroy { members: any = {}; // Members that wrote a message, indexed by ID. favouriteIcon = 'fa-star'; deleteIcon = 'trash'; + otherMember: any; // Other member information (individual conversations only). + footerType: 'message' | 'blocked' | 'requiresContact' | 'requestSent' | 'requestReceived' | 'unable'; + requestContactSent = false; + requestContactReceived = false; constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, @@ -108,6 +114,13 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } } }, this.siteId); + + // Refresh data if info of a mamber of the conversation have changed. + this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => { + if (data.userId && (this.members[data.userId] || this.otherMember && data.userId == this.otherMember.id)) { + this.fetchData(); + } + }, this.siteId); } /** @@ -160,6 +173,25 @@ export class AddonMessagesDiscussionPage implements OnDestroy { const backViewPage = this.navCtrl.getPrevious() && this.navCtrl.getPrevious().component.name; this.showInfo = !backViewPage || backViewPage !== 'CoreUserProfilePage'; + // Recalculate footer position when keyboard is shown or hidden. + this.keyboardObserver = this.eventsProvider.on(CoreEventsProvider.KEYBOARD_CHANGE, (kbHeight) => { + this.content.resize(); + }); + + this.fetchData(); + } + + /** + * Convenience function to fetch the conversation data. + * + * @return {Promise} Resolved when done. + */ + protected fetchData(): Promise { + let loader; + if (this.showLoadingModal) { + loader = this.domUtils.showModalLoading(); + } + if (!this.groupMessagingEnabled && this.userId) { // Get the user profile to retrieve the user fullname and image. this.userProvider.getProfile(this.userId, undefined, true).then((user) => { @@ -171,7 +203,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } // Synchronize messages if needed. - this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => { + return this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => { // Ignore errors. }).then((warnings) => { if (warnings && warnings[0]) { @@ -185,8 +217,22 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // Fetch the messages for the first time. return this.fetchMessages(); } + }).then(() => { + let promise; + if (this.userId) { + promise = this.messagesProvider.getMemberInfo(this.userId); + } else { + // Group conversation. + promise = Promise.resolve(null); + } + + return promise.then((member) => { + this.otherMember = member; + }); }); } else { + this.otherMember = null; + // Fetch the messages for the first time. return this.fetchMessages().then(() => { if (!this.title && this.messages.length) { @@ -207,11 +253,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.resizeContent(); this.loaded = true; this.setPolling(); // Make sure we're polling messages. - }); - - // Recalculate footer position when keyboard is shown or hidden. - this.keyboardObserver = this.eventsProvider.on(CoreEventsProvider.KEYBOARD_CHANGE, (kbHeight) => { - this.content.resize(); + this.setContactRequestInfo(); + this.setFooterType(); + loader && loader.dismiss(); }); } @@ -985,6 +1029,71 @@ export class AddonMessagesDiscussionPage implements OnDestroy { }); } + /** + * Calculate whether there are pending contact requests. + */ + protected setContactRequestInfo(): void { + this.requestContactSent = false; + this.requestContactReceived = false; + if (this.otherMember && !this.otherMember.iscontact) { + this.requestContactSent = this.otherMember.contactrequests.some((request) => { + return request.userid == this.currentUserId && request.requesteduserid == this.otherMember.id; + }); + this.requestContactReceived = this.otherMember.contactrequests.some((request) => { + return request.userid == this.otherMember.id && request.requesteduserid == this.currentUserId; + }); + } + } + + /** + * Calculate what to display in the footer. + */ + protected setFooterType(): void { + if (!this.otherMember) { + // Group conversation or group messaging not available. + this.footerType = 'message'; + } else if (this.otherMember.isblocked) { + this.footerType = 'blocked'; + } else if (this.requestContactReceived) { + this.footerType = 'requestReceived'; + } else if (this.otherMember.canmessage) { + this.footerType = 'message'; + } else if (this.requestContactSent) { + this.footerType = 'requestSent'; + } else if (this.otherMember.requirescontact) { + this.footerType = 'requiresContact'; + } else { + this.footerType = 'unable'; + } + } + + /** + * Displays a confirmation modal to block the user of the individual conversation. + * + * @return {Promise} Promise resolved when user is blocked or dialog is cancelled. + */ + blockUser(): Promise { + if (!this.otherMember) { + // Should never happen. + return Promise.reject(null); + } + + const template = this.translate.instant('addon.messages.blockuserconfirm', {$a: this.otherMember.fullname}); + const okText = this.translate.instant('addon.messages.blockuser'); + + return this.domUtils.showConfirm(template, undefined, okText).then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true); + this.showLoadingModal = true; + + return this.messagesProvider.blockContact(this.otherMember.id).finally(() => { + modal.dismiss(); + this.showLoadingModal = false; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }); + } + /** * Delete the conversation. * @@ -1012,6 +1121,131 @@ export class AddonMessagesDiscussionPage implements OnDestroy { }); } + /** + * Displays a confirmation modal to unblock the user of the individual conversation. + * + * @return {Promise} Promise resolved when user is unblocked or dialog is cancelled. + */ + unblockUser(): Promise { + if (!this.otherMember) { + // Should never happen. + return Promise.reject(null); + } + + const template = this.translate.instant('addon.messages.unblockuserconfirm', {$a: this.otherMember.fullname}); + const okText = this.translate.instant('addon.messages.unblockuser'); + + return this.domUtils.showConfirm(template, undefined, okText).then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true); + this.showLoadingModal = true; + + return this.messagesProvider.unblockContact(this.otherMember.id).finally(() => { + modal.dismiss(); + this.showLoadingModal = false; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }); + } + + /** + * Displays a confirmation modal to send a contact request to the other user of the individual conversation. + * + * @return {Promise} Promise resolved when the request is sent or the dialog is cancelled. + */ + createContactRequest(): Promise { + if (!this.otherMember) { + // Should never happen. + return Promise.reject(null); + } + + const template = this.translate.instant('addon.messages.addcontactconfirm', { $a: this.otherMember.fullname }); + const okText = this.translate.instant('core.add'); + + return this.domUtils.showConfirm(template, undefined, okText).then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true); + this.showLoadingModal = true; + + return this.messagesProvider.createContactRequest(this.otherMember.id).finally(() => { + modal.dismiss(); + this.showLoadingModal = false; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }); + } + + /** + * Confirms the contact request of the other user of the individual conversation. + * + * @return {Promise} Promise resolved when the request is confirmed. + */ + confirmContactRequest(): Promise { + if (!this.otherMember) { + // Should never happen. + return Promise.reject(null); + } + + const modal = this.domUtils.showModalLoading('core.sending', true); + this.showLoadingModal = true; + + return this.messagesProvider.confirmContactRequest(this.otherMember.id).finally(() => { + modal.dismiss(); + this.showLoadingModal = false; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }); + } + + /** + * Declines the contact request of the other user of the individual conversation. + * + * @return {Promise} Promise resolved when the request is confirmed. + */ + declineContactRequest(): Promise { + if (!this.otherMember) { + // Should never happen. + return Promise.reject(null); + } + + const modal = this.domUtils.showModalLoading('core.sending', true); + this.showLoadingModal = true; + + return this.messagesProvider.declineContactRequest(this.otherMember.id).finally(() => { + modal.dismiss(); + this.showLoadingModal = false; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }); + } + + /** + * Displays a confirmation modal to remove the other user of the conversation from contacts. + * + * @return {Promise} Promise resolved when the request is sent or the dialog is cancelled. + */ + removeContact(): Promise { + if (!this.otherMember) { + // Should never happen. + return Promise.reject(null); + } + + const template = this.translate.instant('addon.messages.removecontactconfirm', { $a: this.otherMember.fullname }); + const okText = this.translate.instant('core.remove'); + + return this.domUtils.showConfirm(template, undefined, okText).then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true); + this.showLoadingModal = true; + + return this.messagesProvider.removeContact(this.otherMember.id).finally(() => { + modal.dismiss(); + this.showLoadingModal = false; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }); + } + /** * Page destroyed. */ @@ -1020,6 +1254,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.unsetPolling(); this.syncObserver && this.syncObserver.off(); this.keyboardObserver && this.keyboardObserver.off(); + this.memberInfoObserver && this.memberInfoObserver.off(); this.viewDestroyed = true; } } From 1cefb4ae043391c8409eb7f7bce611e3fb6d575c Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 13:30:05 +0100 Subject: [PATCH 11/15] MOBILE-2620 messages: Contact requests in user profile handler --- .../providers/user-add-contact-handler.ts | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/addon/messages/providers/user-add-contact-handler.ts b/src/addon/messages/providers/user-add-contact-handler.ts index 4fdc8250b..903d2b2e2 100644 --- a/src/addon/messages/providers/user-add-contact-handler.ts +++ b/src/addon/messages/providers/user-add-contact-handler.ts @@ -95,14 +95,14 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle this.messagesProvider.isContact(user.id).then((isContact) => { if (isContact) { - const template = this.translate.instant('addon.messages.removecontactconfirm'), - title = this.translate.instant('addon.messages.removecontact'); + const message = this.translate.instant('addon.messages.removecontactconfirm', {$a: user.fullname}); + const okText = this.translate.instant('core.remove'); - return this.domUtils.showConfirm(template, title, title).then(() => { + return this.domUtils.showConfirm(message, undefined, okText).then(() => { return this.messagesProvider.removeContact(user.id); }); } else { - return this.messagesProvider.addContact(user.id); + return this.addContact(user); } }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.error', true); @@ -125,10 +125,12 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle protected checkButton(userId: number): Promise { this.updateButton(userId, {spinner: true}); + const groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); + return this.messagesProvider.isContact(userId).then((isContact) => { if (isContact) { this.updateButton(userId, { - title: 'addon.messages.removecontact', + title: groupMessagingEnabled ? 'addon.messages.removefromyourcontacts' : 'addon.messages.removecontact', class: 'addon-messages-removecontact-handler', icon: 'remove', hidden: false, @@ -136,7 +138,7 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle }); } else { this.updateButton(userId, { - title: 'addon.messages.addcontact', + title: groupMessagingEnabled ? 'addon.messages.addtoyourcontacts' : 'addon.messages.addcontact', class: 'addon-messages-addcontact-handler', icon: 'add', hidden: false, @@ -160,6 +162,42 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle this.eventsProvider.trigger(CoreUserDelegate.UPDATE_HANDLER_EVENT, { handler: this.name, data: data, userId: userId }); } + /** + * Add a contact or send a contact request if group messaging is enabled. + * + * @param {any} user User to add as contact. + * @return {Promise} Promise resolved when done. + */ + protected addContact(user: any): Promise { + if (!this.messagesProvider.isGroupMessagingEnabled()) { + return this.messagesProvider.addContact(user.id); + } + + return this.messagesProvider.getMemberInfo(user.id).then((member) => { + const currentUserId = this.sitesProvider.getCurrentSiteUserId(); + const requestSent = member.contactrequests.some((request) => { + return request.userid == currentUserId && request.requesteduserid == user.id; + }); + + if (requestSent) { + const message = this.translate.instant('addon.messages.yourcontactrequestpending', {$a: user.fullname}); + + return this.domUtils.showAlert(null, message); + } + + const message = this.translate.instant('addon.messages.addcontactconfirm', {$a: user.fullname}); + const okText = this.translate.instant('core.add'); + + return this.domUtils.showConfirm(message, undefined, okText).then(() => { + return this.messagesProvider.createContactRequest(user.id); + }).then(() => { + const message = this.translate.instant('addon.messages.contactrequestsent'); + + return this.domUtils.showAlert(null, message); + }); + }); + } + /** * Destroyed method. */ From d3459dc078b31ddce4a572600be655b765fd1675 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 13:30:45 +0100 Subject: [PATCH 12/15] MOBILE-2620 messages: Contact requests count in main menu handler --- .../messages/providers/mainmenu-handler.ts | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index 8ba2fedbd..588177186 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -43,6 +43,8 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr loading: true }; + protected updating = false; + constructor(private messagesProvider: AddonMessagesProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider, @@ -57,10 +59,15 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr this.updateBadge(data.siteId); }); + eventsProvider.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => { + this.updateBadge(data.siteId, data.count); + }); + // Reset info on logout. eventsProvider.on(CoreEventsProvider.LOGOUT, (data) => { this.handler.badge = ''; this.handler.loading = true; + this.updating = false; }); // If a message push notification is received, refresh the count. @@ -103,23 +110,55 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr /** * Triggers an update for the badge number and loading status. Mandatory if showBadge is enabled. * - * @param {string} siteId Site ID or current Site if undefined. + * @param {string} [siteId] Site ID or current Site if undefined. + * @param {number} [contactRequestsCount] Number of contact requests, if known. */ - updateBadge(siteId?: string): void { + updateBadge(siteId?: string, contactRequestsCount?: number): void { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (!siteId) { return; } - this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => { - // Leave badge enter if there is a 0+ or a 0. - this.handler.badge = parseInt(unread, 10) > 0 ? unread : ''; - // Update badge. - this.pushNotificationsProvider.updateAddonCounter('AddonMessages', unread, siteId); + if (this.updating) { + // An update is already in prgoress. + return; + } + + this.updating = true; + + const promises = []; + let unreadCount = 0; + let unreadPlus = false; + + promises.push(this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => { + unreadCount = parseInt(unread, 10); + unreadPlus = (typeof unread === 'string' && unread.slice(-1) === '+'); }).catch(() => { - this.handler.badge = ''; + // Ignore error. + })); + + // Get the number of contact requests in 3.6+ sites if needed. + if (contactRequestsCount == null && this.messagesProvider.isGroupMessagingEnabled()) { + promises.push(this.messagesProvider.getContactRequestsCount(siteId).then((count) => { + contactRequestsCount = count; + }).catch(() => { + // Ignore errors + })); + } + + Promise.all(promises).then(() => { + const totalCount = unreadCount + (contactRequestsCount || 0); + if (totalCount > 0) { + this.handler.badge = totalCount + (unreadPlus ? '+' : ''); + } else { + this.handler.badge = ''; + } + + // Update badge. + this.pushNotificationsProvider.updateAddonCounter('AddonMessages', totalCount, siteId); }).finally(() => { this.handler.loading = false; + this.updating = false; }); } From d74b09f8bce34e07166468f6bfcb6698ac50bc97 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 7 Dec 2018 13:31:13 +0100 Subject: [PATCH 13/15] MOBILE-2620 messages: Contact requests link handler --- src/addon/messages/messages.module.ts | 6 +- .../providers/contact-request-link-handler.ts | 71 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/addon/messages/providers/contact-request-link-handler.ts diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts index 5413893ae..58da7f789 100644 --- a/src/addon/messages/messages.module.ts +++ b/src/addon/messages/messages.module.ts @@ -25,6 +25,7 @@ import { CoreCronDelegate } from '@providers/cron'; import { AddonMessagesSendMessageUserHandler } from './providers/user-send-message-handler'; import { AddonMessagesAddContactUserHandler } from './providers/user-add-contact-handler'; import { AddonMessagesBlockContactUserHandler } from './providers/user-block-contact-handler'; +import { AddonMessagesContactRequestLinkHandler } from './providers/contact-request-link-handler'; import { AddonMessagesDiscussionLinkHandler } from './providers/discussion-link-handler'; import { AddonMessagesIndexLinkHandler } from './providers/index-link-handler'; import { AddonMessagesSyncCronHandler } from './providers/sync-cron-handler'; @@ -58,6 +59,7 @@ export const ADDON_MESSAGES_PROVIDERS: any[] = [ AddonMessagesSendMessageUserHandler, AddonMessagesAddContactUserHandler, AddonMessagesBlockContactUserHandler, + AddonMessagesContactRequestLinkHandler, AddonMessagesDiscussionLinkHandler, AddonMessagesIndexLinkHandler, AddonMessagesSyncCronHandler, @@ -74,11 +76,13 @@ export class AddonMessagesModule { sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, updateManager: CoreUpdateManagerProvider, settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate, pushNotificationsDelegate: AddonPushNotificationsDelegate, utils: CoreUtilsProvider, - addContactHandler: AddonMessagesAddContactUserHandler, blockContactHandler: AddonMessagesBlockContactUserHandler) { + addContactHandler: AddonMessagesAddContactUserHandler, blockContactHandler: AddonMessagesBlockContactUserHandler, + contactRequestLinkHandler: AddonMessagesContactRequestLinkHandler) { // Register handlers. mainMenuDelegate.registerHandler(mainmenuHandler); contentLinksDelegate.registerHandler(indexLinkHandler); contentLinksDelegate.registerHandler(discussionLinkHandler); + contentLinksDelegate.registerHandler(contactRequestLinkHandler); userDelegate.registerHandler(sendMessageHandler); userDelegate.registerHandler(addContactHandler); userDelegate.registerHandler(blockContactHandler); diff --git a/src/addon/messages/providers/contact-request-link-handler.ts b/src/addon/messages/providers/contact-request-link-handler.ts new file mode 100644 index 000000000..1c262f601 --- /dev/null +++ b/src/addon/messages/providers/contact-request-link-handler.ts @@ -0,0 +1,71 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonMessagesProvider } from './messages'; + +/** + * Content links handler for a contact requests. + */ +@Injectable() +export class AddonMessagesContactRequestLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonMessagesContactRequestLinkHandler'; + pattern = /\/message\/pendingcontactrequests\.php/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private messagesProvider: AddonMessagesProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'AddonMessagesContactsPage', {}, siteId); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + return this.messagesProvider.isPluginEnabled(siteId).then((enabled) => { + if (!enabled) { + return false; + } + + return this.messagesProvider.isGroupMessagingEnabled(); + }); + } +} From 371c66407a9937eb991f708019947560e0d42a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 11 Dec 2018 15:40:45 +0100 Subject: [PATCH 14/15] MOBILE-2774 messages: Style conversation page --- .../messages/pages/discussion/discussion.html | 26 ++--- .../messages/pages/discussion/discussion.scss | 101 +++++++++++++----- src/theme/format-text.scss | 5 - 3 files changed, 90 insertions(+), 42 deletions(-) diff --git a/src/addon/messages/pages/discussion/discussion.html b/src/addon/messages/pages/discussion/discussion.html index ac8de345d..d307ec3c5 100644 --- a/src/addon/messages/pages/discussion/discussion.html +++ b/src/addon/messages/pages/discussion/discussion.html @@ -25,32 +25,32 @@ - + - - {{ message.timecreated | coreFormatDate: "LL" }} - +
+ {{ message.timecreated | coreFormatDate: "LL" }} +
{{ 'addon.messages.newmessages' | translate:{$a: title} }} - + - - - -

{{ members[message.useridfrom].fullname }}

+

+ + +
{{ members[message.useridfrom].fullname }}
+ + {{ message.timecreated | coreFormatDate: "dftimedate" }} + +

- - {{ message.timecreated | coreFormatDate: "dftimedate" }} - -