diff --git a/scripts/lang_functions.php b/scripts/lang_functions.php new file mode 100644 index 000000000..56158a469 --- /dev/null +++ b/scripts/lang_functions.php @@ -0,0 +1,426 @@ +. + +/** + * Helper functions converting moodle strings to json. + */ + +function detect_languages($languages, $keys) { + echo "\n\n\n"; + + $all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR); + function get_lang_from_dir($dir) { + return str_replace('_', '-', explode('/', $dir)[3]); + } + function get_lang_not_wp($langname) { + return (substr($langname, -3) !== '-wp'); + } + $all_languages = array_map('get_lang_from_dir', $all_languages); + $all_languages = array_filter($all_languages, 'get_lang_not_wp'); + + $detect_lang = array_diff($all_languages, $languages); + $new_langs = array(); + foreach ($detect_lang as $lang) { + reset_translations_strings(); + $new = detect_lang($lang, $keys); + if ($new) { + $new_langs[$lang] = $lang; + } + } + + return $new_langs; +} + +function build_languages($languages, $keys, $added_langs = []) { + // Process the languages. + foreach ($languages as $lang) { + reset_translations_strings(); + $ok = build_lang($lang, $keys); + if ($ok) { + $added_langs[$lang] = $lang; + } + } + + return $added_langs; +} + +function get_langindex_keys() { + // Process the index file, just once. + $keys = file_get_contents('langindex.json'); + $keys = (array) json_decode($keys); + + foreach ($keys as $key => $value) { + $map = new StdClass(); + if ($value == 'local_moodlemobileapp') { + $map->file = $value; + $map->string = $key; + } else { + $exp = explode('/', $value, 2); + $map->file = $exp[0]; + if (count($exp) == 2) { + $map->string = $exp[1]; + } else { + $exp = explode('.', $key, 3); + + if (count($exp) == 3) { + $map->string = $exp[2]; + } else { + $map->string = $exp[1]; + } + } + } + + $keys[$key] = $map; + } + + $total = count($keys); + echo "Total strings to translate $total\n"; + + return $keys; +} + +function add_langs_to_config($langs, $config) { + $changed = false; + $config_langs = get_object_vars($config['languages']); + foreach ($langs as $lang) { + if (!isset($config_langs[$lang])) { + $langfoldername = str_replace('-', '_', $lang); + + $string = []; + include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); + $config['languages']->$lang = $string['thislanguage']; + $changed = true; + } + } + + if ($changed) { + // Sort languages by key. + $config['languages'] = json_decode( json_encode( $config['languages'] ), true ); + ksort($config['languages']); + $config['languages'] = json_decode( json_encode( $config['languages'] ), false ); + file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + } +} + +function get_langfolder($lang) { + $folder = LANGPACKSFOLDER.'/'.str_replace('-', '_', $lang); + if (!is_dir($folder) || !is_file($folder.'/langconfig.php')) { + echo "Cannot translate $folder, folder not found"; + + return false; + } + + return $folder; +} + +function get_translation_strings($langfoldername, $file, $override_folder = false) { + global $strings; + + if (isset($strings[$file])) { + return $strings[$file]; + } + + $string = import_translation_strings($langfoldername, $file); + $string_override = $override_folder ? import_translation_strings($override_folder, $file) : false; + + if ($string) { + $strings[$file] = $string; + if ($string_override) { + $strings[$file] = array_merge($strings[$file], $string_override); + } + } else if ($string_override) { + $strings[$file] = $string_override; + } else { + $strings[$file] = false; + } + + return $strings[$file]; +} + +function import_translation_strings($langfoldername, $file) { + $path = $langfoldername.'/'.$file.'.php'; + // Apply translations. + if (!file_exists($path)) { + return false; + } + + $string = []; + include($path); + + return $string; +} + +function reset_translations_strings() { + global $strings; + $strings = []; +} + +function build_lang($lang, $keys) { + $langfoldername = get_langfolder($lang); + if (!$langfoldername) { + return false; + } + + if (OVERRIDE_LANG_SUFIX) { + $override_langfolder = get_langfolder($lang.OVERRIDE_LANG_SUFIX); + } else { + $override_langfolder = false; + } + + $total = count ($keys); + $local = 0; + + $string = get_translation_strings($langfoldername, 'langconfig'); + $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; + + echo "Processing $lang"; + if ($parent != "" && $parent != $lang) { + echo " ($parent)"; + } + + $langFile = false; + // Not yet translated. Do not override. + if (file_exists(ASSETSPATH.$lang.'.json')) { + // Load lang files just once. + $langFile = file_get_contents(ASSETSPATH.$lang.'.json'); + $langFile = (array) json_decode($langFile); + } + + $translations = []; + // Add the translation to the array. + foreach ($keys as $key => $value) { + $string = get_translation_strings($langfoldername, $value->file, $override_langfolder); + // Apply translations. + if (!$string) { + if (TOTRANSLATE) { + echo "\n\t\To translate $value->string on $value->file"; + } + continue; + } + + if (!isset($string[$value->string]) || ($lang == 'en' && $value->file == 'local_moodlemobileapp')) { + // Not yet translated. Do not override. + if ($langFile && is_array($langFile) && isset($langFile[$key])) { + $translations[$key] = $langFile[$key]; + $local++; + } + if (TOTRANSLATE) { + echo "\n\t\tTo translate $value->string on $value->file"; + } + continue; + } else { + $text = $string[$value->string]; + } + + if ($value->file != 'local_moodlemobileapp') { + $text = str_replace('$a->', '$a.', $text); + $text = str_replace('{$a', '{{$a', $text); + $text = str_replace('}', '}}', $text); + // Prevent double. + $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text); + } else { + $local++; + } + + $translations[$key] = html_entity_decode($text); + } + + // Sort and save. + ksort($translations); + file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); + + $success = count($translations); + $percentage = floor($success/$total * 100); + echo "\t\t$success of $total -> $percentage% ($local local)\n"; + + if ($lang == 'en') { + generate_local_moodlemobileapp($keys, $translations); + override_component_lang_files($keys, $translations); + } + + return true; +} + +function detect_lang($lang, $keys) { + $langfoldername = get_langfolder($lang); + if (!$langfoldername) { + return false; + } + + $total = count ($keys); + $success = 0; + $local = 0; + + $string = get_translation_strings($langfoldername, 'langconfig'); + $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; + if (!isset($string['thislanguage'])) { + echo "Cannot translate $lang, translated name not found"; + return false; + } + + echo "Checking $lang"; + if ($parent != "" && $parent != $lang) { + echo " ($parent)"; + } + $langname = $string['thislanguage']; + echo " ".$langname." -D"; + + // Add the translation to the array. + foreach ($keys as $key => $value) { + $string = get_translation_strings($langfoldername, $value->file); + // Apply translations. + if (!$string) { + continue; + } + + if (!isset($string[$value->string])) { + continue; + } else { + $text = $string[$value->string]; + } + + if ($value->file == 'local_moodlemobileapp') { + $local++; + } + + $success++; + } + + $percentage = floor($success/$total * 100); + echo "\t\t$success of $total -> $percentage% ($local local)"; + if (($percentage > 75 && $local > 50) || ($percentage > 50 && $local > 75)) { + echo " \t DETECTED\n"; + return true; + } + echo "\n"; + + return false; +} + +function save_key($key, $value, $path) { + $filePath = $path . '/en.json'; + + $file = file_get_contents($filePath); + $file = (array) json_decode($file); + $value = html_entity_decode($value); + if ($file[$key] != $value) { + $file[$key] = $value; + ksort($file); + file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); + } +} + +function override_component_lang_files($keys, $translations) { + echo "Override component lang files.\n"; + foreach ($translations as $key => $value) { + $path = '../src/'; + $exp = explode('.', $key, 3); + + $type = $exp[0]; + if (count($exp) == 3) { + $component = $exp[1]; + $plainid = $exp[2]; + } else { + $component = 'moodle'; + $plainid = $exp[1]; + } + switch($type) { + case 'core': + case 'addon': + switch($component) { + case 'moodle': + $path .= 'lang'; + break; + default: + $path .= $type.'/'.str_replace('_', '/', $component).'/lang'; + break; + } + break; + case 'assets': + $path .= $type.'/'.$component; + break; + + } + + if (is_file($path.'/en.json')) { + save_key($plainid, $value, $path); + } + } +} + +/** + * Generates local moodle mobile app file to update languages in AMOS. + * + * @param [array] $keys Translation keys. + * @param [array] $translations English translations. + */ +function generate_local_moodlemobileapp($keys, $translations) { + echo "Generate local_moodlemobileapp.\n"; + $string = '. + +/** + * Version details. + * + * @package local + * @subpackage moodlemobileapp + * @copyright 2014 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string[\'appstoredescription\'] = \'NOTE: This official Moodle Mobile app will ONLY work with Moodle sites that have been set up to allow it. Please talk to your Moodle administrator if you have any problems connecting. + +If your Moodle site has been configured correctly, you can use this app to: + +- browse the content of your courses, even when offline +- receive instant notifications of messages and other events +- quickly find and contact other people in your courses +- upload images, audio, videos and other files from your mobile device +- view your course grades +- and more! + +Please see http://docs.moodle.org/en/Mobile_app for all the latest information. + +We’d really appreciate any good reviews about the functionality so far, and your suggestions on what else you want this app to do! + +The app requires the following permissions: +Record audio - For recording audio to upload to Moodle +Read and modify the contents of your SD card - Contents are downloaded to the SD Card so you can see them offline +Network access - To be able to connect with your Moodle site and check if you are connected or not to switch to offline mode +Run at startup - So you receive local notifications even when the app is running in the background +Prevent phone from sleeping - So you can receive push notifications anytime\';'."\n"; + foreach ($keys as $key => $value) { + if (isset($translations[$key]) && $value->file == 'local_moodlemobileapp') { + $string .= '$string[\''.$key.'\'] = \''.str_replace("'", "\'", $translations[$key]).'\';'."\n"; + } + } + $string .= '$string[\'pluginname\'] = \'Moodle Mobile language strings\';'."\n"; + + file_put_contents('../../moodle-local_moodlemobileapp/lang/en/local_moodlemobileapp.php', $string."\n"); +} diff --git a/scripts/langindex.json b/scripts/langindex.json index 6f1b7f091..c896011af 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -898,7 +898,9 @@ "addon.mod_workshop_assessment_rubric.mustchooseone": "workshopform_rubric", "addon.notes.addnewnote": "notes", "addon.notes.coursenotes": "notes", + "addon.notes.deleteconfirm": "notes", "addon.notes.eventnotecreated": "notes", + "addon.notes.eventnotedeleted": "notes", "addon.notes.nonotes": "notes", "addon.notes.note": "notes", "addon.notes.notes": "notes", diff --git a/scripts/moodle_to_json.php b/scripts/moodle_to_json.php index bc6d128e5..e82eb9523 100644 --- a/scripts/moodle_to_json.php +++ b/scripts/moodle_to_json.php @@ -26,6 +26,10 @@ define('MOODLE_INTERNAL', 1); define('LANGPACKSFOLDER', '../../moodle-langpacks'); define('ASSETSPATH', '../src/assets/lang/'); define('CONFIG', '../src/config.json'); +define('OVERRIDE_LANG_SUFIX', false); + +global $strings; +require_once('lang_functions.php'); $config = file_get_contents(CONFIG); $config = (array) json_decode($config); @@ -42,355 +46,17 @@ if (isset($argv[1]) && !empty($argv[1])) { $languages = $config_langs; } -// Process the index file, just once. -$keys = file_get_contents('langindex.json'); -$keys = (array) json_decode($keys); +$keys = get_langindex_keys(); -foreach ($keys as $key => $value) { - $map = new StdClass(); - if ($value == 'local_moodlemobileapp') { - $map->file = $value; - $map->string = $key; - } else { - $exp = explode('/', $value, 2); - $map->file = $exp[0]; - if (count($exp) == 2) { - $map->string = $exp[1]; - } else { - $exp = explode('.', $key, 3); - - if (count($exp) == 3) { - $map->string = $exp[2]; - } else { - $map->string = $exp[1]; - } - } - } - - $keys[$key] = $map; -} -$total = count ($keys); - -echo "Total strings to translate $total\n"; - -$add_langs = array(); -// Process the languages. -foreach ($languages as $lang) { - $ok = build_lang($lang, $keys, $total); - if ($ok) { - $add_langs[$lang] = $lang; - } -} +$added_langs = build_languages($languages, $keys); if ($forcedetect) { - echo "\n\n\n"; - - $all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR); - function get_lang_from_dir($dir) { - return str_replace('_', '-', explode('/', $dir)[3]); - } - $all_languages = array_map('get_lang_from_dir', $all_languages); - $detect_lang = array_diff($all_languages, $languages); - $new_langs = array(); - foreach ($detect_lang as $lang) { - $new = detect_lang($lang, $keys, $total); - if ($new) { - $new_langs[$lang] = $lang; - } - } + $new_langs = detect_languages($languages, $keys); if (!empty($new_langs)) { echo "\n\n\nThe following languages are going to be added\n\n\n"; - foreach ($new_langs as $lang) { - $ok = build_lang($lang, $keys, $total); - if ($ok) { - $add_langs[$lang] = $lang; - } - } - add_langs_to_config($add_langs, $config); - } -} else { - add_langs_to_config($add_langs, $config); -} - -function add_langs_to_config($langs, $config) { - $changed = false; - $config_langs = get_object_vars($config['languages']); - foreach ($langs as $lang) { - if (!isset($config_langs[$lang])) { - $langfoldername = str_replace('-', '_', $lang); - - $string = []; - include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); - $config['languages']->$lang = $string['thislanguage']; - $changed = true; - } - } - - if ($changed) { - // Sort languages by key. - $config['languages'] = json_decode( json_encode( $config['languages'] ), true ); - ksort($config['languages']); - $config['languages'] = json_decode( json_encode( $config['languages'] ), false ); - file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + $added_langs = build_languages($new_langs, $keys, $added_langs); } } -function build_lang($lang, $keys, $total) { - $local = 0; - $langFile = false; - $translations = []; - $langfoldername = str_replace('-', '_', $lang); - - if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) { - echo "Cannot translate $langfoldername, folder not found"; - return false; - } - - $string = []; - include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); - $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; - - echo "Processing $lang"; - if ($parent != "" && $parent != $lang) { - echo "($parent)"; - } - - - // Add the translation to the array. - foreach ($keys as $key => $value) { - $file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php'; - // Apply translations. - if (!file_exists($file)) { - if (TOTRANSLATE) { - echo "\n\t\To translate $value->string on $value->file"; - } - continue; - } - - $string = []; - include($file); - - if (!isset($string[$value->string]) || ($lang == 'en' && $value->file == 'local_moodlemobileapp')) { - // Not yet translated. Do not override. - if (!$langFile) { - // Load lang files just once. - $langFile = file_get_contents(ASSETSPATH.$lang.'.json'); - $langFile = (array) json_decode($langFile); - } - if (is_array($langFile) && isset($langFile[$key])) { - $translations[$key] = $langFile[$key]; - $local++; - } - if (TOTRANSLATE) { - echo "\n\t\tTo translate $value->string on $value->file"; - } - continue; - } else { - $text = $string[$value->string]; - } - - if ($value->file != 'local_moodlemobileapp') { - $text = str_replace('$a->', '$a.', $text); - $text = str_replace('{$a', '{{$a', $text); - $text = str_replace('}', '}}', $text); - // Prevent double. - $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text); - } else { - $local++; - } - - $translations[$key] = html_entity_decode($text); - } - - // Sort and save. - ksort($translations); - file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); - - $success = count($translations); - $percentage = floor($success/$total *100); - echo "\t\t$success of $total -> $percentage% ($local local)\n"; - - if ($lang == 'en') { - generate_local_moodlemobileapp($keys, $translations); - override_component_lang_files($keys, $translations); - } - - return true; -} - -function detect_lang($lang, $keys, $total) { - $success = 0; - $local = 0; - $langfoldername = str_replace('-', '_', $lang); - - if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) { - echo "Cannot translate $langfoldername, folder not found"; - return false; - } - - $string = []; - include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); - $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; - if (!isset($string['thislanguage'])) { - echo "Cannot translate $langfoldername, name not found"; - return false; - } - - echo "Checking $lang"; - if ($parent != "" && $parent != $lang) { - echo "($parent)"; - } - $langname = $string['thislanguage']; - echo " ".$langname." -D"; - - // Add the translation to the array. - foreach ($keys as $key => $value) { - $file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php'; - // Apply translations. - if (!file_exists($file)) { - continue; - } - - $string = []; - include($file); - - if (!isset($string[$value->string])) { - continue; - } else { - $text = $string[$value->string]; - } - - if ($value->file == 'local_moodlemobileapp') { - $local++; - } - - $success++; - } - - $percentage = floor($success/$total *100); - echo "\t\t$success of $total -> $percentage% ($local local)"; - if (($percentage > 75 && $local > 50) || ($percentage > 50 && $local > 75)) { - echo " \t DETECTED\n"; - return true; - } - echo "\n"; - - return false; -} - -function save_key($key, $value, $path) { - $filePath = $path . '/en.json'; - - $file = file_get_contents($filePath); - $file = (array) json_decode($file); - $value = html_entity_decode($value); - if ($file[$key] != $value) { - $file[$key] = $value; - ksort($file); - file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); - } -} - -function override_component_lang_files($keys, $translations) { - echo "Override component lang files.\n"; - foreach ($translations as $key => $value) { - $path = '../src/'; - $exp = explode('.', $key, 3); - - $type = $exp[0]; - if (count($exp) == 3) { - $component = $exp[1]; - $plainid = $exp[2]; - } else { - $component = 'moodle'; - $plainid = $exp[1]; - } - switch($type) { - case 'core': - case 'addon': - switch($component) { - case 'moodle': - $path .= 'lang'; - break; - default: - $path .= $type.'/'.str_replace('_', '/', $component).'/lang'; - break; - } - break; - case 'assets': - $path .= $type.'/'.$component; - break; - - } - - if (is_file($path.'/en.json')) { - save_key($plainid, $value, $path); - } - } -} - -/** - * Generates local moodle mobile app file to update languages in AMOS. - * - * @param [array] $keys Translation keys. - * @param [array] $translations English translations. - */ -function generate_local_moodlemobileapp($keys, $translations) { - echo "Generate local_moodlemobileapp.\n"; - $string = '. - -/** - * Version details. - * - * @package local - * @subpackage moodlemobileapp - * @copyright 2014 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -$string[\'appstoredescription\'] = \'NOTE: This official Moodle Mobile app will ONLY work with Moodle sites that have been set up to allow it. Please talk to your Moodle administrator if you have any problems connecting. - -If your Moodle site has been configured correctly, you can use this app to: - -- browse the content of your courses, even when offline -- receive instant notifications of messages and other events -- quickly find and contact other people in your courses -- upload images, audio, videos and other files from your mobile device -- view your course grades -- and more! - -Please see http://docs.moodle.org/en/Mobile_app for all the latest information. - -We’d really appreciate any good reviews about the functionality so far, and your suggestions on what else you want this app to do! - -The app requires the following permissions: -Record audio - For recording audio to upload to Moodle -Read and modify the contents of your SD card - Contents are downloaded to the SD Card so you can see them offline -Network access - To be able to connect with your Moodle site and check if you are connected or not to switch to offline mode -Run at startup - So you receive local notifications even when the app is running in the background -Prevent phone from sleeping - So you can receive push notifications anytime\';'."\n"; - foreach ($keys as $key => $value) { - if (isset($translations[$key]) && $value->file == 'local_moodlemobileapp') { - $string .= '$string[\''.$key.'\'] = \''.str_replace("'", "\'", $translations[$key]).'\';'."\n"; - } - } - $string .= '$string[\'pluginname\'] = \'Moodle Mobile language strings\';'."\n"; - - file_put_contents('../../moodle-local_moodlemobileapp/lang/en/local_moodlemobileapp.php', $string."\n"); -} - +add_langs_to_config($added_langs, $config); diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index 2c069ca78..7908279f8 100644 --- a/src/addon/notes/components/list/addon-notes-list.html +++ b/src/addon/notes/components/list/addon-notes-list.html @@ -1,4 +1,7 @@ + @@ -35,8 +38,20 @@

{{note.userfullname}}

-

{{note.lastmodified | coreDateDayOrTime}}

-

{{ 'core.notsent' | translate }}

+

+ {{note.lastmodified | coreDateDayOrTime}} +

+

+ {{ 'core.notsent' | translate }}

+

+ {{ 'core.deletedoffline' | translate }} +

+ +
diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts index 0150edbfd..e9b6ffd7a 100644 --- a/src/addon/notes/components/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -14,12 +14,15 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Content, ModalController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUserProvider } from '@core/user/providers/user'; +import { coreSlideInOut } from '@classes/animations'; import { AddonNotesProvider } from '../../providers/notes'; +import { AddonNotesOfflineProvider } from '../../providers/notes-offline'; import { AddonNotesSyncProvider } from '../../providers/notes-sync'; /** @@ -28,6 +31,7 @@ import { AddonNotesSyncProvider } from '../../providers/notes-sync'; @Component({ selector: 'addon-notes-list', templateUrl: 'addon-notes-list.html', + animations: [coreSlideInOut] }) export class AddonNotesListComponent implements OnInit, OnDestroy { @Input() courseId: number; @@ -44,11 +48,15 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { hasOffline = false; notesLoaded = false; user: any; + showDelete = false; + canDeleteNotes = false; + currentUserId: number; constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private modalCtrl: ModalController, private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider, - private userProvider: CoreUserProvider) { + private userProvider: CoreUserProvider, private translate: TranslateService, + private notesOffline: AddonNotesOfflineProvider) { // Refresh data if notes are synchronized automatically. this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { if (data.courseId == this.courseId) { @@ -64,6 +72,8 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { this.fetchNotes(false); } }, sitesProvider.getCurrentSiteId()); + + this.currentUserId = sitesProvider.getCurrentSiteUserId(); } /** @@ -93,24 +103,35 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { return this.notesProvider.getNotes(this.courseId, this.userId).then((notes) => { notes = notes[this.type + 'notes'] || []; - this.hasOffline = notes.some((note) => note.offline); + return this.notesProvider.setOfflineDeletedNotes(notes, this.courseId).then((notes) => { - if (this.userId) { - this.notes = notes; + this.hasOffline = notes.some((note) => note.offline || note.deleted); - // Get the user profile to retrieve the user image. - return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { - this.user = user; - }); - } else { - return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + if (this.userId) { this.notes = notes; - }); - } + + // Get the user profile to retrieve the user image. + return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { + this.user = user; + }); + } else { + return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + this.notes = notes; + }); + } + }); }); }).catch((message) => { this.domUtils.showErrorModal(message); }).finally(() => { + let canDelete = this.notes && this.notes.length > 0; + if (canDelete && this.type == 'personal') { + canDelete = this.notes.find((note) => { + return note.usermodified == this.currentUserId; + }); + } + this.canDeleteNotes = canDelete; + this.notesLoaded = true; this.refreshIcon = 'refresh'; this.syncIcon = 'sync'; @@ -151,6 +172,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { /** * Add a new Note to user and course. + * * @param {Event} e Event. */ addNote(e: Event): void { @@ -173,6 +195,53 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { modal.present(); } + /** + * Delete a note. + * + * @param {Event} e Click event. + * @param {any} note Note to delete. + */ + deleteNote(e: Event, note: any): void { + e.preventDefault(); + e.stopPropagation(); + + this.domUtils.showConfirm(this.translate.instant('addon.notes.deleteconfirm')).then(() => { + this.notesProvider.deleteNote(note, this.courseId).then(() => { + this.showDelete = false; + + this.refreshNotes(true); + + this.domUtils.showToast('addon.notes.eventnotedeleted', true, 3000); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Delete note failed.'); + }); + }).catch(() => { + // User cancelled, nothing to do. + }); + } + + /** + * Restore a note. + * + * @param {Event} e Click event. + * @param {any} note Note to delete. + */ + undoDeleteNote(e: Event, note: any): void { + e.preventDefault(); + e.stopPropagation(); + + this.notesOffline.undoDeleteNote(note.id).then(() => { + this.refreshNotes(true); + }); + } + + /** + * Toggle delete. + */ + toggleDelete(): void { + this.showDelete = !this.showDelete; + } + /** * Tries to synchronize course notes. * diff --git a/src/addon/notes/lang/en.json b/src/addon/notes/lang/en.json index 3317484cd..c8256d0c4 100644 --- a/src/addon/notes/lang/en.json +++ b/src/addon/notes/lang/en.json @@ -1,7 +1,9 @@ { "addnewnote": "Add a new note", "coursenotes": "Course notes", + "deleteconfirm": "Delete this note?", "eventnotecreated": "Note created", + "eventnotedeleted": "Note deleted", "nonotes": "There are no notes of this type yet", "note": "Note", "notes": "Notes", diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts index 486fa0111..28bae97bf 100644 --- a/src/addon/notes/providers/notes-offline.ts +++ b/src/addon/notes/providers/notes-offline.ts @@ -26,9 +26,10 @@ export class AddonNotesOfflineProvider { // Variables for database. static NOTES_TABLE = 'addon_notes_offline_notes'; + static NOTES_DELETED_TABLE = 'addon_notes_deleted_offline_notes'; protected siteSchema: CoreSiteSchema = { name: 'AddonNotesOfflineProvider', - version: 1, + version: 2, tables: [ { name: AddonNotesOfflineProvider.NOTES_TABLE, @@ -63,6 +64,24 @@ export class AddonNotesOfflineProvider { } ], primaryKeys: ['userid', 'content', 'created'] + }, + { + name: AddonNotesOfflineProvider.NOTES_DELETED_TABLE, + columns: [ + { + name: 'noteid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'deleted', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + } + ] } ] }; @@ -73,7 +92,7 @@ export class AddonNotesOfflineProvider { } /** - * Delete a note. + * Delete an offline note. * * @param {number} userId User ID the note is about. * @param {string} content The note content. @@ -81,7 +100,7 @@ export class AddonNotesOfflineProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if deleted, rejected if failure. */ - deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { + deleteOfflineNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_TABLE, { userid: userId, @@ -91,6 +110,31 @@ export class AddonNotesOfflineProvider { }); } + /** + * Get all offline deleted notes. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getAllDeletedNotes(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE); + }); + } + + /** + * Get course offline deleted notes. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getCourseDeletedNotes(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, {courseid: courseId}); + }); + } + /** * Get all offline notes. * @@ -246,4 +290,40 @@ export class AddonNotesOfflineProvider { }); }); } + + /** + * Delete a note offline to be sent later. + * + * @param {number} noteId Note ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteNote(noteId: number, courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + noteid: noteId, + courseid: courseId, + deleted: now + }; + + return site.getDb().insertRecord(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, data).then(() => { + return data; + }); + }); + } + + /** + * Undo delete a note. + * + * @param {number} noteId Note ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + undoDeleteNote(noteId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, { noteid: noteId }); + }); + } } diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts index 77b7039c3..cebf92d73 100644 --- a/src/addon/notes/providers/notes-sync.ts +++ b/src/addon/notes/providers/notes-sync.ts @@ -63,18 +63,24 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. */ private syncAllNotesFunc(siteId: string, force: boolean): Promise { - return this.notesOffline.getAllNotes(siteId).then((notes) => { - // Get all the courses to be synced. - const courseIds = []; - notes.forEach((note) => { - if (courseIds.indexOf(note.courseid) == -1) { - courseIds.push(note.courseid); - } - }); + const proms = []; + proms.push(this.notesOffline.getAllNotes(siteId)); + proms.push(this.notesOffline.getAllDeletedNotes(siteId)); + + return Promise.all(proms).then((notesArray) => { + // Get all the courses to be synced. + const courseIds = {}; + notesArray.forEach((notes) => { + notes.forEach((note) => { + courseIds[note.courseid] = note.courseid; + }); + }); // Sync all courses. - const promises = courseIds.map((courseId) => { - const promise = force ? this.syncNotes(courseId, siteId) : this.syncNotesIfNeeded(courseId, siteId); + const promises = Object.keys(courseIds).map((courseId) => { + const cId = parseInt(courseIds[courseId], 10); + + const promise = force ? this.syncNotes(cId, siteId) : this.syncNotesIfNeeded(cId, siteId); return promise.then((warnings) => { if (typeof warnings != 'undefined') { @@ -124,9 +130,12 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { this.logger.debug('Try to sync notes for course ' + courseId); const warnings = []; + const errors = []; + + const proms = []; // Get offline notes to be sent. - const syncPromise = this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { + proms.push(this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { if (!notes.length) { // Nothing to sync. return; @@ -157,12 +166,6 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { } }); - // Fetch the notes from server to be sure they're up to date. - return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => { - return this.notesProvider.getNotes(courseId, undefined, false, true, siteId); - }).catch(() => { - // Ignore errors. - }); }).catch((error) => { if (this.utils.isWebServiceError(error)) { // It's a WebService error, this means the user cannot send notes. @@ -174,26 +177,69 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { }).then(() => { // Notes were sent, delete them from local DB. const promises = notes.map((note) => { - return this.notesOffline.deleteNote(note.userid, note.content, note.created, siteId); + return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); }); return Promise.all(promises); - }).then(() => { - if (errors && errors.length) { - // At least an error occurred, get course name and add errors to warnings array. - return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { - // Ignore errors. - return {}; - }).then((course) => { - errors.forEach((error) => { - warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { - course: course.fullname ? course.fullname : courseId, - error: error - })); - }); - }); - } }); + })); + + // Get offline notes to be sent. + proms.push(this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((notes) => { + if (!notes.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + // Format the notes to be sent. + const notesToDelete = notes.map((note) => { + return note.noteid; + }); + + // Delete the notes. + return this.notesProvider.deleteNotesOnline(notesToDelete, courseId, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, this means the user cannot send notes. + errors.push(error); + } else { + // Not a WebService error, reject the synchronization to try again. + return Promise.reject(error); + } + }).then(() => { + // Notes were sent, delete them from local DB. + const promises = notes.map((noteId) => { + return this.notesOffline.undoDeleteNote(noteId, siteId); + }); + + return Promise.all(promises); + }); + })); + + const syncPromise = Promise.all(proms).then(() => { + // Fetch the notes from server to be sure they're up to date. + return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => { + return this.notesProvider.getNotes(courseId, undefined, false, true, siteId); + }).catch(() => { + // Ignore errors. + }); + }).then(() => { + if (errors && errors.length) { + // At least an error occurred, get course name and add errors to warnings array. + return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { + // Ignore errors. + return {}; + }).then((course) => { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { + course: course.fullname ? course.fullname : courseId, + error: error + })); + }); + }); + } }).then(() => { // All done, return the warnings. return warnings; diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts index 006a78093..82f095e41 100644 --- a/src/addon/notes/providers/notes.ts +++ b/src/addon/notes/providers/notes.ts @@ -133,6 +133,72 @@ export class AddonNotesProvider { }); } + /** + * Delete a note. + * + * @param {any} note Note object to delete. + * @param {number} courseId Course ID where the note belongs. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes + * have been deleted, the resolve param can contain errors for notes not deleted. + */ + deleteNote(note: any, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (note.offline) { + return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); + } + + // Convenience function to store the action to be synchronized later. + const storeOffline = (): Promise => { + return this.notesOffline.deleteNote(note.id, courseId, siteId).then(() => { + return false; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the note. + return storeOffline(); + } + + // Send note to server. + return this.deleteNotesOnline([note.id], courseId, siteId).then(() => { + return true; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the note so don't store it. + return Promise.reject(error); + } + + // Error sending note, store it to retry later. + return storeOffline(); + }); + } + + /** + * Delete a note. It will fail if offline or cannot connect. + * + * @param {number[]} noteIds Note IDs to delete. + * @param {number} courseId Course ID where the note belongs. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes + * have been deleted, the resolve param can contain errors for notes not deleted. + */ + deleteNotesOnline(noteIds: number[], courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + notes: noteIds + }; + + return site.write('core_notes_delete_notes', data).then((response) => { + // A note was deleted, invalidate the course notes. + return this.invalidateNotes(courseId, undefined, siteId).catch(() => { + // Ignore errors. + }); + }); + }); + } + /** * Returns whether or not the notes plugin is enabled for a certain site. * @@ -267,6 +333,24 @@ export class AddonNotesProvider { }); } + /** + * Get offline deleted notes and set the state. + * + * @param {any[]} notes Array of notes. + * @param {number} courseId ID of the course the notes belong to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} [description] + */ + setOfflineDeletedNotes(notes: any[], courseId: number, siteId?: string): Promise { + return this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((deletedNotes) => { + notes.forEach((note) => { + note.deleted = deletedNotes.some((n) => n.noteid == note.id); + }); + + return notes; + }); + } + /** * Get user data for notes since they only have userid. * diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 6069bde0b..d87fa7cc4 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -898,7 +898,9 @@ "addon.mod_workshop_assessment_rubric.mustchooseone": "You have to select one of these items", "addon.notes.addnewnote": "Add a new note", "addon.notes.coursenotes": "Course notes", + "addon.notes.deleteconfirm": "Delete this note?", "addon.notes.eventnotecreated": "Note created", + "addon.notes.eventnotedeleted": "Note deleted", "addon.notes.nonotes": "There are no notes of this type yet", "addon.notes.note": "Note", "addon.notes.notes": "Notes",