Merge pull request #1935 from albertgasset/MOBILE-3018
MOBILE-3018 forum: Post a copy to all groupsmain
commit
4d6e019c75
|
@ -503,6 +503,7 @@
|
||||||
"addon.mod_forum.erroremptysubject": "forum",
|
"addon.mod_forum.erroremptysubject": "forum",
|
||||||
"addon.mod_forum.errorgetforum": "local_moodlemobileapp",
|
"addon.mod_forum.errorgetforum": "local_moodlemobileapp",
|
||||||
"addon.mod_forum.errorgetgroups": "local_moodlemobileapp",
|
"addon.mod_forum.errorgetgroups": "local_moodlemobileapp",
|
||||||
|
"addon.mod_forum.errorposttoallgroups": "local_moodlemobileapp",
|
||||||
"addon.mod_forum.favouriteupdated": "forum",
|
"addon.mod_forum.favouriteupdated": "forum",
|
||||||
"addon.mod_forum.forumnodiscussionsyet": "local_moodlemobileapp",
|
"addon.mod_forum.forumnodiscussionsyet": "local_moodlemobileapp",
|
||||||
"addon.mod_forum.group": "local_moodlemobileapp",
|
"addon.mod_forum.group": "local_moodlemobileapp",
|
||||||
|
@ -519,6 +520,7 @@
|
||||||
"addon.mod_forum.pinupdated": "forum",
|
"addon.mod_forum.pinupdated": "forum",
|
||||||
"addon.mod_forum.postisprivatereply": "forum",
|
"addon.mod_forum.postisprivatereply": "forum",
|
||||||
"addon.mod_forum.posttoforum": "forum",
|
"addon.mod_forum.posttoforum": "forum",
|
||||||
|
"addon.mod_forum.posttomygroups": "forum",
|
||||||
"addon.mod_forum.privatereply": "forum",
|
"addon.mod_forum.privatereply": "forum",
|
||||||
"addon.mod_forum.re": "forum",
|
"addon.mod_forum.re": "forum",
|
||||||
"addon.mod_forum.refreshdiscussions": "local_moodlemobileapp",
|
"addon.mod_forum.refreshdiscussions": "local_moodlemobileapp",
|
||||||
|
@ -1219,6 +1221,7 @@
|
||||||
"core.agelocationverification": "moodle",
|
"core.agelocationverification": "moodle",
|
||||||
"core.ago": "message",
|
"core.ago": "message",
|
||||||
"core.all": "moodle",
|
"core.all": "moodle",
|
||||||
|
"core.allgroups": "moodle",
|
||||||
"core.allparticipants": "moodle",
|
"core.allparticipants": "moodle",
|
||||||
"core.android": "local_moodlemobileapp",
|
"core.android": "local_moodlemobileapp",
|
||||||
"core.answer": "moodle",
|
"core.answer": "moodle",
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<ion-icon name="information-circle"></ion-icon> {{ availabilityMessage }}
|
<ion-icon name="information-circle"></ion-icon> {{ availabilityMessage }}
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<core-empty-box *ngIf="forum && discussions.length == 0" icon="chatbubbles" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
<core-empty-box *ngIf="forum && discussions.length == 0 && offlineDiscussions.length == 0" icon="chatbubbles" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<div text-wrap *ngIf="sortingAvailable && selectedSortOrder" ion-row padding-horizontal padding-top margin-bottom>
|
<div text-wrap *ngIf="sortingAvailable && selectedSortOrder" ion-row padding-horizontal padding-top margin-bottom>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="forum && discussions.length > 0">
|
<ng-container *ngIf="forum">
|
||||||
<ion-card *ngFor="let discussion of offlineDiscussions" (click)="openNewDiscussion(discussion.timecreated)" [class.addon-forum-discussion-selected]="discussion.timecreated == -selectedDiscussion">
|
<ion-card *ngFor="let discussion of offlineDiscussions" (click)="openNewDiscussion(discussion.timecreated)" [class.addon-forum-discussion-selected]="discussion.timecreated == -selectedDiscussion">
|
||||||
<ion-item text-wrap>
|
<ion-item text-wrap>
|
||||||
<ion-avatar core-user-avatar [user]="discussion" item-start [courseId]="courseId"></ion-avatar>
|
<ion-avatar core-user-avatar [user]="discussion" item-start [courseId]="courseId"></ion-avatar>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"erroremptysubject": "Post subject cannot be empty.",
|
"erroremptysubject": "Post subject cannot be empty.",
|
||||||
"errorgetforum": "Error getting forum data.",
|
"errorgetforum": "Error getting forum data.",
|
||||||
"errorgetgroups": "Error getting group settings.",
|
"errorgetgroups": "Error getting group settings.",
|
||||||
|
"errorposttoallgroups": "Could not create new discussion in all groups.",
|
||||||
"favouriteupdated": "Your star option has been updated.",
|
"favouriteupdated": "Your star option has been updated.",
|
||||||
"forumnodiscussionsyet": "There are no discussions yet in this forum.",
|
"forumnodiscussionsyet": "There are no discussions yet in this forum.",
|
||||||
"group": "Group",
|
"group": "Group",
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
"pinupdated": "The pin option has been updated.",
|
"pinupdated": "The pin option has been updated.",
|
||||||
"postisprivatereply": "This post was made privately and is not visible to all users.",
|
"postisprivatereply": "This post was made privately and is not visible to all users.",
|
||||||
"posttoforum": "Post to forum",
|
"posttoforum": "Post to forum",
|
||||||
|
"posttomygroups": "Post a copy to all groups",
|
||||||
"privatereply": "Reply privately",
|
"privatereply": "Reply privately",
|
||||||
"re": "Re:",
|
"re": "Re:",
|
||||||
"refreshdiscussions": "Refresh discussions",
|
"refreshdiscussions": "Refresh discussions",
|
||||||
|
|
|
@ -27,9 +27,13 @@
|
||||||
{{ 'addon.mod_forum.advanced' | translate }}
|
{{ 'addon.mod_forum.advanced' | translate }}
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<ng-container *ngIf="advanced">
|
<ng-container *ngIf="advanced">
|
||||||
|
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
|
||||||
|
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
|
||||||
|
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
<ion-item *ngIf="showGroups">
|
<ion-item *ngIf="showGroups">
|
||||||
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
|
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
|
||||||
<ion-select [(ngModel)]="newDiscussion.groupId" aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet">
|
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet">
|
||||||
<ion-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-option>
|
<ion-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
|
@ -53,15 +53,18 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
forum: any;
|
forum: any;
|
||||||
showForm = false;
|
showForm = false;
|
||||||
groups = [];
|
groups = [];
|
||||||
|
groupIds = [];
|
||||||
newDiscussion = {
|
newDiscussion = {
|
||||||
subject: '',
|
subject: '',
|
||||||
message: null, // Null means empty or just white space.
|
message: null, // Null means empty or just white space.
|
||||||
|
postToAllGroups: false,
|
||||||
groupId: 0,
|
groupId: 0,
|
||||||
subscribe: true,
|
subscribe: true,
|
||||||
pin: false,
|
pin: false,
|
||||||
files: []
|
files: []
|
||||||
};
|
};
|
||||||
advanced = false; // Display all form fields.
|
advanced = false; // Display all form fields.
|
||||||
|
accessInfo: any = {};
|
||||||
|
|
||||||
protected courseId: number;
|
protected courseId: number;
|
||||||
protected cmId: number;
|
protected cmId: number;
|
||||||
|
@ -146,9 +149,13 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
return promise.then((forumGroups) => {
|
return promise.then((forumGroups) => {
|
||||||
if (forumGroups.length > 0) {
|
if (forumGroups.length > 0) {
|
||||||
this.groups = forumGroups;
|
this.groups = forumGroups;
|
||||||
|
this.groupIds = forumGroups.map((group) => group.id).filter((id) => id > 0);
|
||||||
// Do not override group id.
|
// Do not override group id.
|
||||||
this.newDiscussion.groupId = this.newDiscussion.groupId || forumGroups[0].id;
|
this.newDiscussion.groupId = this.newDiscussion.groupId || forumGroups[0].id;
|
||||||
this.showGroups = true;
|
this.showGroups = true;
|
||||||
|
if (this.groupIds.length <= 1) {
|
||||||
|
this.newDiscussion.postToAllGroups = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const message = mode === CoreGroupsProvider.SEPARATEGROUPS ?
|
const message = mode === CoreGroupsProvider.SEPARATEGROUPS ?
|
||||||
'addon.mod_forum.cannotadddiscussionall' : 'addon.mod_forum.cannotadddiscussion';
|
'addon.mod_forum.cannotadddiscussionall' : 'addon.mod_forum.cannotadddiscussion';
|
||||||
|
@ -159,6 +166,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
this.showGroups = false;
|
this.showGroups = false;
|
||||||
|
this.newDiscussion.postToAllGroups = false;
|
||||||
|
|
||||||
// Use the canAddDiscussion WS to check if the user can add attachments and pin discussions.
|
// Use the canAddDiscussion WS to check if the user can add attachments and pin discussions.
|
||||||
promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
|
promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => {
|
||||||
|
@ -174,10 +182,18 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
this.forum = forum;
|
this.forum = forum;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Get access information.
|
||||||
|
promises.push(this.forumProvider.getAccessInformation(this.forumId).then((accessInfo) => {
|
||||||
|
this.accessInfo = accessInfo;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).then(() => {
|
||||||
// If editing a discussion, get offline data.
|
// If editing a discussion, get offline data.
|
||||||
if (this.timeCreated && !refresh) {
|
if (this.timeCreated && !refresh) {
|
||||||
this.syncId = this.forumSync.getForumSyncId(this.forumId);
|
this.syncId = this.forumSync.getForumSyncId(this.forumId);
|
||||||
promises.push(this.forumSync.waitForSync(this.syncId).then(() => {
|
|
||||||
|
return this.forumSync.waitForSync(this.syncId).then(() => {
|
||||||
// Do not block if the scope is already destroyed.
|
// Do not block if the scope is already destroyed.
|
||||||
if (!this.isDestroyed) {
|
if (!this.isDestroyed) {
|
||||||
this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
|
||||||
|
@ -186,7 +202,13 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
return this.forumOffline.getNewDiscussion(this.forumId, this.timeCreated).then((discussion) => {
|
return this.forumOffline.getNewDiscussion(this.forumId, this.timeCreated).then((discussion) => {
|
||||||
this.hasOffline = true;
|
this.hasOffline = true;
|
||||||
discussion.options = discussion.options || {};
|
discussion.options = discussion.options || {};
|
||||||
this.newDiscussion.groupId = discussion.groupid ? discussion.groupid : this.newDiscussion.groupId;
|
if (discussion.groupid == AddonModForumProvider.ALL_GROUPS) {
|
||||||
|
this.newDiscussion.groupId = this.groups[0].id;
|
||||||
|
this.newDiscussion.postToAllGroups = true;
|
||||||
|
} else {
|
||||||
|
this.newDiscussion.groupId = discussion.groupid;
|
||||||
|
this.newDiscussion.postToAllGroups = false;
|
||||||
|
}
|
||||||
this.newDiscussion.subject = discussion.subject;
|
this.newDiscussion.subject = discussion.subject;
|
||||||
this.newDiscussion.message = discussion.message;
|
this.newDiscussion.message = discussion.message;
|
||||||
this.newDiscussion.subscribe = discussion.options.discussionsubscribe;
|
this.newDiscussion.subscribe = discussion.options.discussionsubscribe;
|
||||||
|
@ -203,15 +225,15 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
|
|
||||||
return Promise.resolve(promise).then(() => {
|
return Promise.resolve(promise).then(() => {
|
||||||
// Show advanced fields by default if any of them has not the default value.
|
// Show advanced fields by default if any of them has not the default value.
|
||||||
if (!this.newDiscussion.subscribe || this.newDiscussion.pin || this.newDiscussion.files.length) {
|
if (!this.newDiscussion.subscribe || this.newDiscussion.pin || this.newDiscussion.files.length ||
|
||||||
|
this.groups.length > 0 && this.newDiscussion.groupId != this.groups[0].id ||
|
||||||
|
this.newDiscussion.postToAllGroups) {
|
||||||
this.advanced = true;
|
this.advanced = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises);
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (!this.originalData) {
|
if (!this.originalData) {
|
||||||
// Initialize original data.
|
// Initialize original data.
|
||||||
|
@ -232,9 +254,9 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
* Validate which of the groups returned by getActivityAllowedGroups in visible groups should be shown to post to.
|
* Validate which of the groups returned by getActivityAllowedGroups in visible groups should be shown to post to.
|
||||||
*
|
*
|
||||||
* @param {any[]} forumGroups Forum groups.
|
* @param {any[]} forumGroups Forum groups.
|
||||||
* @return {Promise<any>} Promise resolved when done.
|
* @return {Promise<any[]>} Promise resolved with the list of groups.
|
||||||
*/
|
*/
|
||||||
protected validateVisibleGroups(forumGroups: any[]): Promise<any> {
|
protected validateVisibleGroups(forumGroups: any[]): Promise<any[]> {
|
||||||
// We first check if the user can post to all the groups.
|
// We first check if the user can post to all the groups.
|
||||||
return this.forumProvider.canAddDiscussionToAll(this.forumId).catch(() => {
|
return this.forumProvider.canAddDiscussionToAll(this.forumId).catch(() => {
|
||||||
// The call failed, let's assume he can't.
|
// The call failed, let's assume he can't.
|
||||||
|
@ -331,7 +353,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
if (canAdd) {
|
if (canAdd) {
|
||||||
groups.unshift({
|
groups.unshift({
|
||||||
courseid: this.courseId,
|
courseid: this.courseId,
|
||||||
id: -1,
|
id: AddonModForumProvider.ALL_PARTICIPANTS,
|
||||||
name: this.translate.instant('core.allparticipants')
|
name: this.translate.instant('core.allparticipants')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -362,14 +384,14 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Convenience function to update or return to discussions depending on device.
|
* Convenience function to update or return to discussions depending on device.
|
||||||
*
|
*
|
||||||
* @param {number} [discussionId] Id of the new discussion.
|
* @param {number} [discussionIds] Ids of the new discussions.
|
||||||
* @param {number} [discTimecreated] The time created of the discussion (if offline).
|
* @param {number} [discTimecreated] The time created of the discussion (if offline).
|
||||||
*/
|
*/
|
||||||
protected returnToDiscussions(discussionId?: number, discTimecreated?: number): void {
|
protected returnToDiscussions(discussionIds?: number[], discTimecreated?: number): void {
|
||||||
const data: any = {
|
const data: any = {
|
||||||
forumId: this.forumId,
|
forumId: this.forumId,
|
||||||
cmId: this.cmId,
|
cmId: this.cmId,
|
||||||
discussionId: discussionId,
|
discussionIds: discussionIds,
|
||||||
discTimecreated: discTimecreated
|
discTimecreated: discTimecreated
|
||||||
};
|
};
|
||||||
this.eventsProvider.trigger(AddonModForumProvider.NEW_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
|
this.eventsProvider.trigger(AddonModForumProvider.NEW_DISCUSSION_EVENT, data, this.sitesProvider.getCurrentSiteId());
|
||||||
|
@ -383,6 +405,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
this.newDiscussion.subject = '';
|
this.newDiscussion.subject = '';
|
||||||
this.newDiscussion.message = null;
|
this.newDiscussion.message = null;
|
||||||
this.newDiscussion.files = [];
|
this.newDiscussion.files = [];
|
||||||
|
this.newDiscussion.postToAllGroups = false;
|
||||||
this.messageEditor.clearText();
|
this.messageEditor.clearText();
|
||||||
this.originalData = this.utils.clone(this.newDiscussion);
|
this.originalData = this.utils.clone(this.newDiscussion);
|
||||||
|
|
||||||
|
@ -414,13 +437,11 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
const subject = this.newDiscussion.subject;
|
const subject = this.newDiscussion.subject;
|
||||||
let message = this.newDiscussion.message;
|
let message = this.newDiscussion.message;
|
||||||
const pin = this.newDiscussion.pin;
|
const pin = this.newDiscussion.pin;
|
||||||
const groupId = this.newDiscussion.groupId;
|
|
||||||
const attachments = this.newDiscussion.files;
|
const attachments = this.newDiscussion.files;
|
||||||
const discTimecreated = this.timeCreated || Date.now();
|
const discTimecreated = this.timeCreated || Date.now();
|
||||||
const options: any = {
|
const options: any = {
|
||||||
discussionsubscribe: !!this.newDiscussion.subscribe
|
discussionsubscribe: !!this.newDiscussion.subscribe
|
||||||
};
|
};
|
||||||
let saveOffline = false;
|
|
||||||
|
|
||||||
if (!subject) {
|
if (!subject) {
|
||||||
this.domUtils.showErrorModal('addon.mod_forum.erroremptysubject', true);
|
this.domUtils.showErrorModal('addon.mod_forum.erroremptysubject', true);
|
||||||
|
@ -434,51 +455,29 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = this.domUtils.showModalLoading('core.sending', true);
|
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||||
let promise;
|
|
||||||
|
|
||||||
// Add some HTML to the message if needed.
|
// Add some HTML to the message if needed.
|
||||||
message = this.textUtils.formatHtmlLines(message);
|
message = this.textUtils.formatHtmlLines(message);
|
||||||
|
|
||||||
// Upload attachments first if any.
|
if (pin) {
|
||||||
if (attachments.length) {
|
options.discussionpinned = true;
|
||||||
promise = this.forumHelper.uploadOrStoreNewDiscussionFiles(this.forumId, discTimecreated, attachments, false)
|
|
||||||
.catch(() => {
|
|
||||||
// Cannot upload them in online, save them in offline.
|
|
||||||
saveOffline = true;
|
|
||||||
|
|
||||||
return this.forumHelper.uploadOrStoreNewDiscussionFiles(this.forumId, discTimecreated, attachments, true);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
promise = Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.then((attach) => {
|
const groupIds = this.newDiscussion.postToAllGroups ? this.groupIds : [this.newDiscussion.groupId];
|
||||||
if (attach) {
|
|
||||||
options.attachmentsid = attach;
|
|
||||||
}
|
|
||||||
if (pin) {
|
|
||||||
options.discussionpinned = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveOffline) {
|
this.forumHelper.addNewDiscussion(this.forumId, forumName, this.courseId, subject, message, attachments, options, groupIds,
|
||||||
// Save discussion in offline.
|
discTimecreated).then((discussionIds) => {
|
||||||
return this.forumOffline.addNewDiscussion(this.forumId, forumName, this.courseId, subject,
|
if (discussionIds) {
|
||||||
message, options, groupId, discTimecreated).then(() => {
|
|
||||||
// Don't return anything.
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Try to send it to server.
|
|
||||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
|
||||||
return this.forumProvider.addNewDiscussion(this.forumId, forumName, this.courseId, subject, message, options,
|
|
||||||
groupId, undefined, discTimecreated, !attachments.length);
|
|
||||||
}
|
|
||||||
}).then((discussionId) => {
|
|
||||||
if (discussionId) {
|
|
||||||
// Data sent to server, delete stored files (if any).
|
// Data sent to server, delete stored files (if any).
|
||||||
this.forumHelper.deleteNewDiscussionStoredFiles(this.forumId, discTimecreated);
|
this.forumHelper.deleteNewDiscussionStoredFiles(this.forumId, discTimecreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.returnToDiscussions(discussionId, discTimecreated);
|
if (discussionIds && discussionIds.length < groupIds.length) {
|
||||||
|
// Some discussions could not be created.
|
||||||
|
this.domUtils.showErrorModalDefault(null, 'addon.mod_forum.errorposttoallgroups', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.returnToDiscussions(discussionIds, discTimecreated);
|
||||||
}).catch((message) => {
|
}).catch((message) => {
|
||||||
this.domUtils.showErrorModalDefault(message, 'addon.mod_forum.cannotcreatediscussion', true);
|
this.domUtils.showErrorModalDefault(message, 'addon.mod_forum.cannotcreatediscussion', true);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
|
@ -46,6 +46,9 @@ export class AddonModForumProvider {
|
||||||
static SORTORDER_REPLIES_DESC = 5;
|
static SORTORDER_REPLIES_DESC = 5;
|
||||||
static SORTORDER_REPLIES_ASC = 6;
|
static SORTORDER_REPLIES_ASC = 6;
|
||||||
|
|
||||||
|
static ALL_PARTICIPANTS = -1;
|
||||||
|
static ALL_GROUPS = -2;
|
||||||
|
|
||||||
protected ROOT_CACHE_KEY = 'mmaModForum:';
|
protected ROOT_CACHE_KEY = 'mmaModForum:';
|
||||||
|
|
||||||
constructor(private appProvider: CoreAppProvider,
|
constructor(private appProvider: CoreAppProvider,
|
||||||
|
@ -126,62 +129,6 @@ export class AddonModForumProvider {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new discussion.
|
|
||||||
*
|
|
||||||
* @param {number} forumId Forum ID.
|
|
||||||
* @param {string} name Forum name.
|
|
||||||
* @param {number} courseId Course ID the forum belongs to.
|
|
||||||
* @param {string} subject New discussion's subject.
|
|
||||||
* @param {string} message New discussion's message.
|
|
||||||
* @param {any} [options] Options (subscribe, pin, ...).
|
|
||||||
* @param {string} [groupId] Group this discussion belongs to.
|
|
||||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
||||||
* @param {number} [timeCreated] The time the discussion was created. Only used when editing discussion.
|
|
||||||
* @param {boolean} allowOffline True if it can be stored in offline, false otherwise.
|
|
||||||
* @return {Promise<any>} Promise resolved with discussion ID if sent online, resolved with false if stored offline.
|
|
||||||
*/
|
|
||||||
addNewDiscussion(forumId: number, name: string, courseId: number, subject: string, message: string, options?: any,
|
|
||||||
groupId?: number, siteId?: string, timeCreated?: number, allowOffline?: boolean): Promise<any> {
|
|
||||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
|
||||||
|
|
||||||
// Convenience function to store a message to be synchronized later.
|
|
||||||
const storeOffline = (): Promise<any> => {
|
|
||||||
return this.forumOffline.addNewDiscussion(forumId, name, courseId, subject, message, options,
|
|
||||||
groupId, timeCreated, siteId).then(() => {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// If we are editing an offline discussion, discard previous first.
|
|
||||||
let discardPromise;
|
|
||||||
if (timeCreated) {
|
|
||||||
discardPromise = this.forumOffline.deleteNewDiscussion(forumId, timeCreated, siteId);
|
|
||||||
} else {
|
|
||||||
discardPromise = Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return discardPromise.then(() => {
|
|
||||||
if (!this.appProvider.isOnline() && allowOffline) {
|
|
||||||
// App is offline, store the action.
|
|
||||||
return storeOffline();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.addNewDiscussionOnline(forumId, subject, message, options, groupId, siteId).then((id) => {
|
|
||||||
// Success, return the discussion ID.
|
|
||||||
return id;
|
|
||||||
}).catch((error) => {
|
|
||||||
if (!allowOffline || this.utils.isWebServiceError(error)) {
|
|
||||||
// The WebService has thrown an error or offline not supported, reject.
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't connect to server, store in offline.
|
|
||||||
return storeOffline();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new discussion. It will fail if offline or cannot connect.
|
* Add a new discussion. It will fail if offline or cannot connect.
|
||||||
*
|
*
|
||||||
|
@ -268,7 +215,7 @@ export class AddonModForumProvider {
|
||||||
* - cancreateattachment (boolean)
|
* - cancreateattachment (boolean)
|
||||||
*/
|
*/
|
||||||
canAddDiscussionToAll(forumId: number): Promise<any> {
|
canAddDiscussionToAll(forumId: number): Promise<any> {
|
||||||
return this.canAddDiscussion(forumId, -1);
|
return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -309,6 +256,7 @@ export class AddonModForumProvider {
|
||||||
|
|
||||||
return this.groupsProvider.getActivityAllowedGroups(cmId).then((forumGroups) => {
|
return this.groupsProvider.getActivityAllowedGroups(cmId).then((forumGroups) => {
|
||||||
const strAllParts = this.translate.instant('core.allparticipants');
|
const strAllParts = this.translate.instant('core.allparticipants');
|
||||||
|
const strAllGroups = this.translate.instant('core.allgroups');
|
||||||
|
|
||||||
// Turn groups into an object where each group is identified by id.
|
// Turn groups into an object where each group is identified by id.
|
||||||
const groups = {};
|
const groups = {};
|
||||||
|
@ -318,8 +266,11 @@ export class AddonModForumProvider {
|
||||||
|
|
||||||
// Format discussions.
|
// Format discussions.
|
||||||
discussions.forEach((disc) => {
|
discussions.forEach((disc) => {
|
||||||
if (disc.groupid === -1) {
|
if (disc.groupid == AddonModForumProvider.ALL_PARTICIPANTS) {
|
||||||
disc.groupname = strAllParts;
|
disc.groupname = strAllParts;
|
||||||
|
} else if (disc.groupid == AddonModForumProvider.ALL_GROUPS) {
|
||||||
|
// Offline discussions only.
|
||||||
|
disc.groupname = strAllGroups;
|
||||||
} else {
|
} else {
|
||||||
const group = groups[disc.groupid];
|
const group = groups[disc.groupid];
|
||||||
if (group) {
|
if (group) {
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
import { CoreFileProvider } from '@providers/file';
|
import { CoreFileProvider } from '@providers/file';
|
||||||
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
import { CoreUserProvider } from '@core/user/providers/user';
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
import { AddonModForumProvider } from './forum';
|
import { AddonModForumProvider } from './forum';
|
||||||
import { AddonModForumOfflineProvider } from './offline';
|
import { AddonModForumOfflineProvider } from './offline';
|
||||||
|
@ -33,9 +35,123 @@ export class AddonModForumHelperProvider {
|
||||||
private uploaderProvider: CoreFileUploaderProvider,
|
private uploaderProvider: CoreFileUploaderProvider,
|
||||||
private timeUtils: CoreTimeUtilsProvider,
|
private timeUtils: CoreTimeUtilsProvider,
|
||||||
private userProvider: CoreUserProvider,
|
private userProvider: CoreUserProvider,
|
||||||
|
private appProvider: CoreAppProvider,
|
||||||
|
private utils: CoreUtilsProvider,
|
||||||
private forumProvider: AddonModForumProvider,
|
private forumProvider: AddonModForumProvider,
|
||||||
private forumOffline: AddonModForumOfflineProvider) {}
|
private forumOffline: AddonModForumOfflineProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new discussion.
|
||||||
|
*
|
||||||
|
* @param {number} forumId Forum ID.
|
||||||
|
* @param {string} name Forum name.
|
||||||
|
* @param {number} courseId Course ID the forum belongs to.
|
||||||
|
* @param {string} subject New discussion's subject.
|
||||||
|
* @param {string} message New discussion's message.
|
||||||
|
* @param {any[]} [attachments] New discussion's attachments.
|
||||||
|
* @param {any} [options] Options (subscribe, pin, ...).
|
||||||
|
* @param {number[]} [groupIds] Groups this discussion belongs to.
|
||||||
|
* @param {number} [timeCreated] The time the discussion was created. Only used when editing discussion.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<number[]>} Promise resolved with ids of the created discussions or null if stored offline
|
||||||
|
*/
|
||||||
|
addNewDiscussion(forumId: number, name: string, courseId: number, subject: string, message: string, attachments?: any[],
|
||||||
|
options?: any, groupIds?: number[], timeCreated?: number, siteId?: string): Promise<number[]> {
|
||||||
|
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
groupIds = groupIds && groupIds.length > 0 ? groupIds : [0];
|
||||||
|
|
||||||
|
let saveOffline = false;
|
||||||
|
const attachmentsIds = [];
|
||||||
|
let offlineAttachments: any;
|
||||||
|
|
||||||
|
// Convenience function to store a message to be synchronized later.
|
||||||
|
const storeOffline = (): Promise<number[]> => {
|
||||||
|
// Multiple groups, the discussion is being posted to all groups.
|
||||||
|
const groupId = groupIds.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds[0];
|
||||||
|
|
||||||
|
if (offlineAttachments) {
|
||||||
|
options.attachmentsid = offlineAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.forumOffline.addNewDiscussion(forumId, name, courseId, subject, message, options,
|
||||||
|
groupId, timeCreated, siteId).then(() => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// First try to upload attachments, once per group.
|
||||||
|
let promise;
|
||||||
|
if (attachments && attachments.length > 0) {
|
||||||
|
const promises = groupIds.map(() => {
|
||||||
|
return this.uploadOrStoreNewDiscussionFiles(forumId, timeCreated, attachments, false).then((attach) => {
|
||||||
|
attachmentsIds.push(attach);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
promise = Promise.all(promises).catch(() => {
|
||||||
|
// Cannot upload them in online, save them in offline.
|
||||||
|
saveOffline = true;
|
||||||
|
|
||||||
|
return this.uploadOrStoreNewDiscussionFiles(forumId, timeCreated, attachments, true).then((attach) => {
|
||||||
|
offlineAttachments = attach;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(() => {
|
||||||
|
// If we are editing an offline discussion, discard previous first.
|
||||||
|
let discardPromise;
|
||||||
|
if (timeCreated) {
|
||||||
|
discardPromise = this.forumOffline.deleteNewDiscussion(forumId, timeCreated, siteId);
|
||||||
|
} else {
|
||||||
|
discardPromise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return discardPromise.then(() => {
|
||||||
|
if (saveOffline || !this.appProvider.isOnline()) {
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const discussionIds = [];
|
||||||
|
|
||||||
|
const promises = groupIds.map((groupId, index) => {
|
||||||
|
const grouOptions = this.utils.clone(options);
|
||||||
|
if (attachmentsIds[index]) {
|
||||||
|
grouOptions.attachmentsid = attachmentsIds[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.forumProvider.addNewDiscussionOnline(forumId, subject, message, grouOptions, groupId, siteId)
|
||||||
|
.then((discussionId) => {
|
||||||
|
discussionIds.push(discussionId);
|
||||||
|
}).catch((error) => {
|
||||||
|
errors.push(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
if (errors.length == groupIds.length) {
|
||||||
|
// All requests have failed.
|
||||||
|
for (let i = 0; i < errors.length; i++) {
|
||||||
|
if (this.utils.isWebServiceError(errors[i]) || attachments.length > 0) {
|
||||||
|
// The WebService has thrown an error or offline not supported, reject.
|
||||||
|
return Promise.reject(errors[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't connect to server, store offline.
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
return discussionIds;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert offline reply to online format in order to be compatible with them.
|
* Convert offline reply to online format in order to be compatible with them.
|
||||||
*
|
*
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
|
||||||
import { CoreFileProvider } from '@providers/file';
|
import { CoreFileProvider } from '@providers/file';
|
||||||
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
|
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { AddonModForumProvider } from './forum';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle offline forum.
|
* Service to handle offline forum.
|
||||||
|
@ -248,7 +249,7 @@ export class AddonModForumOfflineProvider {
|
||||||
subject: subject,
|
subject: subject,
|
||||||
message: message,
|
message: message,
|
||||||
options: JSON.stringify(options || {}),
|
options: JSON.stringify(options || {}),
|
||||||
groupid: groupId || -1,
|
groupid: groupId || AddonModForumProvider.ALL_PARTICIPANTS,
|
||||||
userid: userId || site.getUserId(),
|
userid: userId || site.getUserId(),
|
||||||
timecreated: timeCreated || new Date().getTime()
|
timecreated: timeCreated || new Date().getTime()
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploa
|
||||||
import { CoreAppProvider } from '@providers/app';
|
import { CoreAppProvider } from '@providers/app';
|
||||||
import { CoreLoggerProvider } from '@providers/logger';
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
import { CoreEventsProvider } from '@providers/events';
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreGroupsProvider } from '@providers/groups';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
import { CoreSyncProvider } from '@providers/sync';
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
@ -46,6 +47,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider {
|
||||||
appProvider: CoreAppProvider,
|
appProvider: CoreAppProvider,
|
||||||
courseProvider: CoreCourseProvider,
|
courseProvider: CoreCourseProvider,
|
||||||
private eventsProvider: CoreEventsProvider,
|
private eventsProvider: CoreEventsProvider,
|
||||||
|
private groupsProvider: CoreGroupsProvider,
|
||||||
loggerProvider: CoreLoggerProvider,
|
loggerProvider: CoreLoggerProvider,
|
||||||
sitesProvider: CoreSitesProvider,
|
sitesProvider: CoreSitesProvider,
|
||||||
syncProvider: CoreSyncProvider,
|
syncProvider: CoreSyncProvider,
|
||||||
|
@ -222,38 +224,57 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
discussions.forEach((data) => {
|
discussions.forEach((data) => {
|
||||||
data.options = data.options || {};
|
let groupsPromise;
|
||||||
|
if (data.groupid == AddonModForumProvider.ALL_GROUPS) {
|
||||||
|
// Fetch all group ids.
|
||||||
|
groupsPromise = this.forumProvider.getForumById(data.courseid, data.forumid, siteId).then((forum) => {
|
||||||
|
return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((groups) => {
|
||||||
|
return groups.map((group) => group.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
groupsPromise = Promise.resolve([data.groupid]);
|
||||||
|
}
|
||||||
|
|
||||||
// First of all upload the attachments (if any).
|
promises.push(groupsPromise.then((groupIds) => {
|
||||||
const promise = this.uploadAttachments(forumId, data, true, siteId, userId).then((itemId) => {
|
const errors = [];
|
||||||
// Now try to add the discussion.
|
|
||||||
data.options.attachmentsid = itemId;
|
|
||||||
|
|
||||||
return this.forumProvider.addNewDiscussionOnline(forumId, data.subject, data.message,
|
return Promise.all(groupIds.map((groupId) => {
|
||||||
data.options, data.groupid, siteId);
|
// First of all upload the attachments (if any).
|
||||||
});
|
return this.uploadAttachments(forumId, data, true, siteId, userId).then((itemId) => {
|
||||||
|
// Now try to add the discussion.
|
||||||
|
const options = this.utils.clone(data.options || {});
|
||||||
|
options.attachmentsid = itemId;
|
||||||
|
|
||||||
promises.push(promise.then(() => {
|
return this.forumProvider.addNewDiscussionOnline(forumId, data.subject, data.message, options,
|
||||||
result.updated = true;
|
groupId, siteId);
|
||||||
|
}).catch((error) => {
|
||||||
|
errors.push(error);
|
||||||
|
});
|
||||||
|
})).then(() => {
|
||||||
|
if (errors.length == groupIds.length) {
|
||||||
|
// All requests have failed, reject if errors were not returned by WS.
|
||||||
|
for (let i = 0; i < errors.length; i++) {
|
||||||
|
if (!this.utils.isWebServiceError(errors[i])) {
|
||||||
|
return Promise.reject(errors[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this.deleteNewDiscussion(forumId, data.timecreated, siteId, userId);
|
// All requests succeeded, some failed or all failed with a WS error.
|
||||||
}).catch((error) => {
|
|
||||||
if (this.utils.isWebServiceError(error)) {
|
|
||||||
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
|
|
||||||
result.updated = true;
|
result.updated = true;
|
||||||
|
|
||||||
return this.deleteNewDiscussion(forumId, data.timecreated, siteId, userId).then(() => {
|
return this.deleteNewDiscussion(forumId, data.timecreated, siteId, userId).then(() => {
|
||||||
// Responses deleted, add a warning.
|
if (errors.length == groupIds.length) {
|
||||||
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
|
// All requests failed with WS error.
|
||||||
component: this.componentTranslate,
|
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
name: data.name,
|
component: this.componentTranslate,
|
||||||
error: this.textUtils.getErrorMessageFromError(error)
|
name: data.name,
|
||||||
}));
|
error: this.textUtils.getErrorMessageFromError(errors[0])
|
||||||
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
// Couldn't connect to server, reject.
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -503,6 +503,7 @@
|
||||||
"addon.mod_forum.erroremptysubject": "Post subject cannot be empty.",
|
"addon.mod_forum.erroremptysubject": "Post subject cannot be empty.",
|
||||||
"addon.mod_forum.errorgetforum": "Error getting forum data.",
|
"addon.mod_forum.errorgetforum": "Error getting forum data.",
|
||||||
"addon.mod_forum.errorgetgroups": "Error getting group settings.",
|
"addon.mod_forum.errorgetgroups": "Error getting group settings.",
|
||||||
|
"addon.mod_forum.errorposttoallgroups": "Could not create new discussion in all groups.",
|
||||||
"addon.mod_forum.favouriteupdated": "Your star option has been updated.",
|
"addon.mod_forum.favouriteupdated": "Your star option has been updated.",
|
||||||
"addon.mod_forum.forumnodiscussionsyet": "There are no discussions yet in this forum.",
|
"addon.mod_forum.forumnodiscussionsyet": "There are no discussions yet in this forum.",
|
||||||
"addon.mod_forum.group": "Group",
|
"addon.mod_forum.group": "Group",
|
||||||
|
@ -519,6 +520,7 @@
|
||||||
"addon.mod_forum.pinupdated": "The pin option has been updated.",
|
"addon.mod_forum.pinupdated": "The pin option has been updated.",
|
||||||
"addon.mod_forum.postisprivatereply": "This post was made privately and is not visible to all users.",
|
"addon.mod_forum.postisprivatereply": "This post was made privately and is not visible to all users.",
|
||||||
"addon.mod_forum.posttoforum": "Post to forum",
|
"addon.mod_forum.posttoforum": "Post to forum",
|
||||||
|
"addon.mod_forum.posttomygroups": "Post a copy to all groups",
|
||||||
"addon.mod_forum.privatereply": "Reply privately",
|
"addon.mod_forum.privatereply": "Reply privately",
|
||||||
"addon.mod_forum.re": "Re:",
|
"addon.mod_forum.re": "Re:",
|
||||||
"addon.mod_forum.refreshdiscussions": "Refresh discussions",
|
"addon.mod_forum.refreshdiscussions": "Refresh discussions",
|
||||||
|
@ -1219,6 +1221,7 @@
|
||||||
"core.agelocationverification": "Age and location verification",
|
"core.agelocationverification": "Age and location verification",
|
||||||
"core.ago": "{{$a}} ago",
|
"core.ago": "{{$a}} ago",
|
||||||
"core.all": "All",
|
"core.all": "All",
|
||||||
|
"core.allgroups": "All groups",
|
||||||
"core.allparticipants": "All participants",
|
"core.allparticipants": "All participants",
|
||||||
"core.android": "Android",
|
"core.android": "Android",
|
||||||
"core.answer": "Answer",
|
"core.answer": "Answer",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"agelocationverification": "Age and location verification",
|
"agelocationverification": "Age and location verification",
|
||||||
"ago": "{{$a}} ago",
|
"ago": "{{$a}} ago",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
|
"allgroups": "All groups",
|
||||||
"allparticipants": "All participants",
|
"allparticipants": "All participants",
|
||||||
"android": "Android",
|
"android": "Android",
|
||||||
"answer": "Answer",
|
"answer": "Answer",
|
||||||
|
|
Loading…
Reference in New Issue