forked from CIT/Vmeda.Online
		
	
						commit
						f936bc9bd9
					
				
							
								
								
									
										1
									
								
								.github/workflows/migration.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/migration.yml
									
									
									
									
										vendored
									
									
								
							@ -15,4 +15,5 @@ jobs:
 | 
			
		||||
        node-version: '12.x'
 | 
			
		||||
    - run: npm ci
 | 
			
		||||
    - run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0
 | 
			
		||||
    - run: npm install -D @ionic/v4-migration-tslint
 | 
			
		||||
    - run: npx tslint -c ionic-migration.json -p tsconfig.json
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										186
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										186
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -3334,16 +3334,6 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@ionic/v4-migration-tslint": {
 | 
			
		||||
      "version": "1.7.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@ionic/v4-migration-tslint/-/v4-migration-tslint-1.7.1.tgz",
 | 
			
		||||
      "integrity": "sha512-1vwBmf0czHvG+vKboxHtYtPJ3Stc7wP8tB2i7qmLslKqMqVFzaTkbNiakt40mHJ/UtbfAzFOkBD6RZZHTWEuzQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "codelyzer": "^4.4.4",
 | 
			
		||||
        "tslint": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@istanbuljs/load-nyc-config": {
 | 
			
		||||
      "version": "1.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
 | 
			
		||||
@ -5010,12 +5000,6 @@
 | 
			
		||||
        "picomatch": "^2.0.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "app-root-path": {
 | 
			
		||||
      "version": "2.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "append-buffer": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz",
 | 
			
		||||
@ -6187,12 +6171,6 @@
 | 
			
		||||
      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "builtin-modules": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
 | 
			
		||||
      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "builtin-status-codes": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
 | 
			
		||||
@ -7380,34 +7358,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
 | 
			
		||||
      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
 | 
			
		||||
    },
 | 
			
		||||
    "codelyzer": {
 | 
			
		||||
      "version": "4.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-oO6vCkjqsVrEsmh58oNlnJkRXuA30hF8cdNAQV9DytEalDwyOFRvHMnlKFzmOStNerOmPGZU9GAHnBo4tGvtiQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "app-root-path": "^2.1.0",
 | 
			
		||||
        "css-selector-tokenizer": "^0.7.0",
 | 
			
		||||
        "cssauron": "^1.4.0",
 | 
			
		||||
        "semver-dsl": "^1.0.1",
 | 
			
		||||
        "source-map": "^0.5.7",
 | 
			
		||||
        "sprintf-js": "^1.1.1"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "source-map": {
 | 
			
		||||
          "version": "0.5.7",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
 | 
			
		||||
          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "sprintf-js": {
 | 
			
		||||
          "version": "1.1.2",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
 | 
			
		||||
          "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "collect-v8-coverage": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
 | 
			
		||||
@ -8502,16 +8452,6 @@
 | 
			
		||||
      "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "css-selector-tokenizer": {
 | 
			
		||||
      "version": "0.7.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz",
 | 
			
		||||
      "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "cssesc": "^3.0.0",
 | 
			
		||||
        "fastparse": "^1.1.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "css-tree": {
 | 
			
		||||
      "version": "1.0.0-alpha.37",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
 | 
			
		||||
@ -8536,15 +8476,6 @@
 | 
			
		||||
      "integrity": "sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "cssauron": {
 | 
			
		||||
      "version": "1.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz",
 | 
			
		||||
      "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "through": "X.X.X"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "cssesc": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
 | 
			
		||||
@ -10500,12 +10431,6 @@
 | 
			
		||||
      "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "fastparse": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "fastq": {
 | 
			
		||||
      "version": "1.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz",
 | 
			
		||||
@ -19850,23 +19775,6 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "semver-dsl": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz",
 | 
			
		||||
      "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "semver": "^5.3.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "semver": {
 | 
			
		||||
          "version": "5.7.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
 | 
			
		||||
          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "semver-greatest-satisfied-range": {
 | 
			
		||||
      "version": "1.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz",
 | 
			
		||||
@ -21881,100 +21789,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "tslint": {
 | 
			
		||||
      "version": "5.20.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
 | 
			
		||||
      "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@babel/code-frame": "^7.0.0",
 | 
			
		||||
        "builtin-modules": "^1.1.1",
 | 
			
		||||
        "chalk": "^2.3.0",
 | 
			
		||||
        "commander": "^2.12.1",
 | 
			
		||||
        "diff": "^4.0.1",
 | 
			
		||||
        "glob": "^7.1.1",
 | 
			
		||||
        "js-yaml": "^3.13.1",
 | 
			
		||||
        "minimatch": "^3.0.4",
 | 
			
		||||
        "mkdirp": "^0.5.1",
 | 
			
		||||
        "resolve": "^1.3.2",
 | 
			
		||||
        "semver": "^5.3.0",
 | 
			
		||||
        "tslib": "^1.8.0",
 | 
			
		||||
        "tsutils": "^2.29.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "ansi-styles": {
 | 
			
		||||
          "version": "3.2.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
 | 
			
		||||
          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "color-convert": "^1.9.0"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "chalk": {
 | 
			
		||||
          "version": "2.4.2",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
 | 
			
		||||
          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "ansi-styles": "^3.2.1",
 | 
			
		||||
            "escape-string-regexp": "^1.0.5",
 | 
			
		||||
            "supports-color": "^5.3.0"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "color-convert": {
 | 
			
		||||
          "version": "1.9.3",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
 | 
			
		||||
          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "color-name": "1.1.3"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "color-name": {
 | 
			
		||||
          "version": "1.1.3",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
 | 
			
		||||
          "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "has-flag": {
 | 
			
		||||
          "version": "3.0.0",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
 | 
			
		||||
          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "semver": {
 | 
			
		||||
          "version": "5.7.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
 | 
			
		||||
          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "supports-color": {
 | 
			
		||||
          "version": "5.5.0",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
 | 
			
		||||
          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "has-flag": "^3.0.0"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "tslib": {
 | 
			
		||||
          "version": "1.14.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
 | 
			
		||||
          "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "tsutils": {
 | 
			
		||||
          "version": "2.29.0",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
 | 
			
		||||
          "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "tslib": "^1.8.1"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tsutils": {
 | 
			
		||||
      "version": "3.17.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
 | 
			
		||||
 | 
			
		||||
@ -137,7 +137,6 @@
 | 
			
		||||
    "@angular/language-service": "~10.0.0",
 | 
			
		||||
    "@ionic/angular-toolkit": "^2.3.0",
 | 
			
		||||
    "@ionic/cli": "^6.14.1",
 | 
			
		||||
    "@ionic/v4-migration-tslint": "^1.7.1",
 | 
			
		||||
    "@types/faker": "^5.1.3",
 | 
			
		||||
    "@types/node": "^12.12.64",
 | 
			
		||||
    "@types/resize-observer-browser": "^0.1.5",
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,7 @@
 | 
			
		||||
@import "~theme/components/discussion.scss";
 | 
			
		||||
@import "~theme/globals.scss";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    ion-content {
 | 
			
		||||
        --background: var(--background-alternative);
 | 
			
		||||
 | 
			
		||||
        &::part(scroll) {
 | 
			
		||||
            padding-bottom: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-messages-discussion-container {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        padding-bottom: 15px;
 | 
			
		||||
        background: var(--background-alternative);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-messages-date {
 | 
			
		||||
        font-weight: normal;
 | 
			
		||||
        font-size: 0.9rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-messages-unreadfrom {
 | 
			
		||||
        color: var(--ion-color-primary);
 | 
			
		||||
@ -33,156 +15,6 @@
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Message item.
 | 
			
		||||
    ion-item.addon-message {
 | 
			
		||||
        border: 0;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        padding: 0 8px 0 8px;
 | 
			
		||||
        margin: 10px 8px 0 8px;
 | 
			
		||||
        --background: var(--addon-messages-message-bg);
 | 
			
		||||
        background: var(--background);
 | 
			
		||||
        align-self: flex-start;
 | 
			
		||||
        width: 90%;
 | 
			
		||||
        max-width: 90%;
 | 
			
		||||
        --min-height: var(--a11y-min-target-size);
 | 
			
		||||
        position: relative;
 | 
			
		||||
        @include core-transition(width);
 | 
			
		||||
        // This is needed to display bubble tails.
 | 
			
		||||
        overflow: visible;
 | 
			
		||||
 | 
			
		||||
        &::part(native) {
 | 
			
		||||
            --inner-border-width: 0;
 | 
			
		||||
            --inner-padding-end: 0;
 | 
			
		||||
            padding: 0;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        core-format-text > p:only-child {
 | 
			
		||||
            display: inline;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-message-user {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: row;
 | 
			
		||||
            justify-content: space-between;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            margin-bottom: .5rem;
 | 
			
		||||
            margin-top: 0;
 | 
			
		||||
            color: var(--ion-text-color);
 | 
			
		||||
 | 
			
		||||
            core-user-avatar {
 | 
			
		||||
                display: block;
 | 
			
		||||
                --core-avatar-size: var(--addon-messages-avatar-size);
 | 
			
		||||
                margin: 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            div {
 | 
			
		||||
                font-weight: 500;
 | 
			
		||||
                flex-grow: 1;
 | 
			
		||||
                @include padding-horizontal(.5rem);
 | 
			
		||||
                overflow: hidden;
 | 
			
		||||
                text-overflow: ellipsis;
 | 
			
		||||
                white-space: nowrap;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ion-note {
 | 
			
		||||
            color: var(--addon-messages-message-note-text);
 | 
			
		||||
            font-size: var(--addon-messages-message-note-font-size);
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            padding: 0 0 8px 0;
 | 
			
		||||
            align-self: flex-end;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &:active {
 | 
			
		||||
            --background: var(--addon-messages-message-activated-bg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ion-label {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            padding: 8px 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-message-text {
 | 
			
		||||
            display: inline-flex;
 | 
			
		||||
            * {
 | 
			
		||||
                color: var(--ion-text-color);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-messages-delete-button {
 | 
			
		||||
            min-height: initial;
 | 
			
		||||
            line-height: initial;
 | 
			
		||||
            @include margin(0, null, 0, null);
 | 
			
		||||
            height: var(--a11y-min-target-size) !important;
 | 
			
		||||
            align-self: flex-end;
 | 
			
		||||
 | 
			
		||||
            ion-icon {
 | 
			
		||||
                font-size: 1.4em;
 | 
			
		||||
                line-height: initial;
 | 
			
		||||
                color: var(--ion-color-danger);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .tail {
 | 
			
		||||
            content: '';
 | 
			
		||||
            width: 0;
 | 
			
		||||
            height: 0;
 | 
			
		||||
            border: 0.5rem solid transparent;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            touch-action: none;
 | 
			
		||||
            bottom: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Defines when an item-message is the user's.
 | 
			
		||||
        &.addon-message-mine {
 | 
			
		||||
            --background: var(--addon-messages-message-mine-bg);
 | 
			
		||||
            align-self: flex-end;
 | 
			
		||||
 | 
			
		||||
            &:active {
 | 
			
		||||
                --background: var(--addon-messages-message-mine-activated-bg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .spinner {
 | 
			
		||||
                @include float(end);
 | 
			
		||||
                @include margin(2px, -3px, -2px, 5px);
 | 
			
		||||
 | 
			
		||||
                svg {
 | 
			
		||||
                    width: 16px;
 | 
			
		||||
                    height: 16px;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .tail {
 | 
			
		||||
                @include position(null, -8px, null, null);
 | 
			
		||||
                @include margin-horizontal(null, -0.5rem);
 | 
			
		||||
                border-bottom-color: var(--addon-messages-message-mine-bg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            &:active .tail {
 | 
			
		||||
                border-bottom-color: var(--addon-messages-message-mine-activated-bg);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.addon-message-not-mine .tail {
 | 
			
		||||
            border-bottom-color: var(--addon-messages-message-bg);
 | 
			
		||||
            @include position(null, null, null, -8px);
 | 
			
		||||
            @include margin-horizontal(-0.5rem, null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.addon-message-not-mine.activated .tail {
 | 
			
		||||
            border-bottom-color: var(--addon-messages-message-activated-bg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ion-item.addon-message.addon-message-mine + ion-item.addon-message.addon-message-no-user.addon-message-mine,
 | 
			
		||||
    ion-item.addon-message.addon-message-not-mine + ion-item.addon-message.addon-message-no-user.addon-message-not-mine {
 | 
			
		||||
        .item-heading {
 | 
			
		||||
            margin-bottom: 0;
 | 
			
		||||
        }
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
   .has-fab .scroll-content {
 | 
			
		||||
        padding-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
@ -232,9 +64,4 @@
 | 
			
		||||
    ion-header ion-toolbar h1 {
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ion-footer .toolbar:last-child {
 | 
			
		||||
        padding-bottom: 4px;
 | 
			
		||||
        min-height: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </h1>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
            <ion-button fill="clear" (click)="save()">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
 | 
			
		||||
    <core-navbar-buttons slot="end">
 | 
			
		||||
        <ion-button [hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()" [attr.aria-label]="'core.done' | translate">
 | 
			
		||||
        <ion-button [hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()">
 | 
			
		||||
            {{ 'core.done' | translate }}
 | 
			
		||||
        </ion-button>
 | 
			
		||||
    </core-navbar-buttons>
 | 
			
		||||
 | 
			
		||||
@ -86,11 +86,10 @@
 | 
			
		||||
                    [@coreSlideInOut]="message.userid == currentUserId ? '' : 'fromLeft'">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <!-- User data. -->
 | 
			
		||||
                        <h2 class="addon-message-user">
 | 
			
		||||
                            <core-user-avatar slot="start" [user]="message" [linkProfile]="false" *ngIf="message.showUserData">
 | 
			
		||||
                        <h2 class="addon-message-user" *ngIf="message.showUserData">
 | 
			
		||||
                            <core-user-avatar slot="start" [user]="message" [linkProfile]="false">
 | 
			
		||||
                            </core-user-avatar>
 | 
			
		||||
                            <div *ngIf="message.showUserData">{{ message.userfullname }}</div>
 | 
			
		||||
                            <ion-note>{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
 | 
			
		||||
                            <div>{{ message.userfullname }}</div>
 | 
			
		||||
                        </h2>
 | 
			
		||||
 | 
			
		||||
                        <p class="addon-message-text">
 | 
			
		||||
@ -99,6 +98,7 @@
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-note>{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
 | 
			
		||||
                    <div class="tail" *ngIf="message.showTail"></div>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
@import "~theme/components/discussion.scss";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    .addon-mod_chat-notice {
 | 
			
		||||
        margin-top: 8px;
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ import { AddonModChatFormattedMessage, AddonModChatHelper } from '../../services
 | 
			
		||||
    selector: 'page-addon-mod-chat-chat',
 | 
			
		||||
    templateUrl: 'chat.html',
 | 
			
		||||
    animations: [CoreAnimations.SLIDE_IN_OUT],
 | 
			
		||||
    styleUrls: ['chat.scss', '../../../../messages/pages/discussion/discussion.scss'],
 | 
			
		||||
    styleUrls: ['chat.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,6 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    cmId!: number;
 | 
			
		||||
 | 
			
		||||
    protected logger;
 | 
			
		||||
    protected chatId!: number;
 | 
			
		||||
    protected sessionId?: string;
 | 
			
		||||
    protected lastTime = 0;
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="loaded" class="safe-area-page">
 | 
			
		||||
        <ion-list class="addon-messages-discussion-container" aria-live="polite">
 | 
			
		||||
        <ion-list class="addon-messages-discussion-container">
 | 
			
		||||
            <ng-container *ngFor="let message of messages; index as index;">
 | 
			
		||||
 | 
			
		||||
                <div class="ion-text-center addon-messages-date" *ngIf="message.showDate">
 | 
			
		||||
@ -83,7 +83,6 @@
 | 
			
		||||
                            <core-user-avatar slot="start" [user]="message" [linkProfile]="false" *ngIf="message.showUserData">
 | 
			
		||||
                            </core-user-avatar>
 | 
			
		||||
                            <div *ngIf="message.showUserData">{{ message.userfullname }}</div>
 | 
			
		||||
                            <ion-note>{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
 | 
			
		||||
                        </h2>
 | 
			
		||||
 | 
			
		||||
                        <p class="addon-message-text">
 | 
			
		||||
@ -91,6 +90,7 @@
 | 
			
		||||
                                [courseId]="courseId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <ion-note>{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <div class="tail" *ngIf="message.showTail"></div>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
@import "~theme/components/discussion.scss";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    .addon-mod_chat-notice {
 | 
			
		||||
        margin-top: 8px;
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ import { AddonModChatFormattedSessionMessage, AddonModChatHelper } from '../../s
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-chat-session-messages',
 | 
			
		||||
    templateUrl: 'session-messages.html',
 | 
			
		||||
    styleUrls: ['session-messages.scss', '../../../../messages/pages/discussion/discussion.scss'],
 | 
			
		||||
    styleUrls: ['session-messages.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModChatSessionMessagesPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </h1>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button *ngIf="entry" fill="clear" (click)="save($event)" [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
            <ion-button *ngIf="entry" fill="clear" (click)="save($event)">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
        </h1>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
            <ion-button fill="clear" (click)="save()">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </h1>
 | 
			
		||||
        <ion-buttons slot="end" [hidden]="!evaluating">
 | 
			
		||||
            <ion-button fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
            <ion-button fill="clear" (click)="saveEvaluation()">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
                [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
            <ion-button *ngIf="canAddFeedback" fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
            <ion-button *ngIf="canAddFeedback" fill="clear" (click)="saveEvaluation()">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,21 @@
 | 
			
		||||
<form #messageForm>
 | 
			
		||||
    <textarea class="core-send-message-input" [core-auto-focus]="showKeyboard" [placeholder]="placeholder" rows="1" core-auto-rows
 | 
			
		||||
        [(ngModel)]="message" name="message" (onResize)="textareaResized()" (keyup.enter)="enterClicked($event)"
 | 
			
		||||
        (keyup.control.enter)="enterClicked($event, 'control')" (keyup.meta.enter)="enterClicked($event, 'meta')"></textarea>
 | 
			
		||||
    <textarea
 | 
			
		||||
        class="core-send-message-input"
 | 
			
		||||
        [core-auto-focus]="showKeyboard"
 | 
			
		||||
        [placeholder]="placeholder"
 | 
			
		||||
        rows="1"
 | 
			
		||||
        core-auto-rows
 | 
			
		||||
        [(ngModel)]="message"
 | 
			
		||||
        name="message"
 | 
			
		||||
        (onResize)="textareaResized()"
 | 
			
		||||
        (keyup.enter)="enterKeyUp($event)"
 | 
			
		||||
        (keyup.control.enter)="enterKeyUp($event, 'control')"
 | 
			
		||||
        (keyup.meta.enter)="enterKeyUp($event, 'meta')"
 | 
			
		||||
        (keydown.enter)="enterKeyDown($event)"
 | 
			
		||||
        (keydown.control.enter)="enterKeyDown($event, 'control')"
 | 
			
		||||
        (keydown.meta.enter)="enterKeyDown($event, 'meta')"
 | 
			
		||||
    >
 | 
			
		||||
    </textarea>
 | 
			
		||||
    <ion-button fill="clear" size="large" type="submit" [disabled]="!message || sendDisabled"
 | 
			
		||||
        [attr.aria-label]="'core.send' | translate" [core-suppress-events] (click)="submitForm($event)">
 | 
			
		||||
        <ion-icon name="send" color="dark" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
 | 
			
		||||
@ -20,5 +20,6 @@
 | 
			
		||||
    line-height: 20px;
 | 
			
		||||
    padding: 9px 12px 11px;
 | 
			
		||||
    margin: 5px 10px;
 | 
			
		||||
    resize: vertical;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -103,13 +103,34 @@ export class CoreSendMessageFormComponent implements OnInit {
 | 
			
		||||
        this.onResize.emit();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A11y key functionality that prevents keyDown events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event.
 | 
			
		||||
     */
 | 
			
		||||
    enterKeyDown(e: KeyboardEvent, other?: string): void {
 | 
			
		||||
        if (this.sendDisabled) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.sendOnEnter && !other) {
 | 
			
		||||
            // Enter clicked, send the message.
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
        } else if (!this.sendOnEnter && !CoreApp.isMobile() && other == 'control') {
 | 
			
		||||
            // Cmd+Enter or Ctrl+Enter, send message.
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Enter key clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event.
 | 
			
		||||
     * @param other The name of the other key that was clicked, undefined if no other key.
 | 
			
		||||
     */
 | 
			
		||||
    enterClicked(e: Event, other?: string): void {
 | 
			
		||||
    enterKeyUp(e: Event, other?: string): void {
 | 
			
		||||
        if (this.sendDisabled) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ export class CoreLongPressDirective implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    element: HTMLElement;
 | 
			
		||||
    pressGesture?: Gesture;
 | 
			
		||||
    protected moved = false;
 | 
			
		||||
 | 
			
		||||
    @Output() longPress = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
@ -42,8 +43,11 @@ export class CoreLongPressDirective implements OnInit, OnDestroy {
 | 
			
		||||
        this.pressGesture = GestureController.create({
 | 
			
		||||
            el: this.element,
 | 
			
		||||
            threshold: 0,
 | 
			
		||||
            disableScroll: true,
 | 
			
		||||
            gestureName: 'longpress',
 | 
			
		||||
            onEnd: ev => this.longPress.emit(ev.event),
 | 
			
		||||
            onStart: () => this.moved = false,
 | 
			
		||||
            onMove: () => this.moved = true,
 | 
			
		||||
            onEnd: ev => !this.moved && this.longPress.emit(ev.event),
 | 
			
		||||
        }, true);
 | 
			
		||||
 | 
			
		||||
        this.pressGesture.enable();
 | 
			
		||||
 | 
			
		||||
@ -1,95 +0,0 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// 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, ViewChild, ElementRef, Input } from '@angular/core';
 | 
			
		||||
import { CoreComments } from '@features/comments/services/comments';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreForms } from '@singletons/form';
 | 
			
		||||
import { ModalController } from '@singletons';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays a text area for composing a comment.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-comments-add',
 | 
			
		||||
    templateUrl: 'add.html',
 | 
			
		||||
})
 | 
			
		||||
export class CoreCommentsAddComponent {
 | 
			
		||||
 | 
			
		||||
    @ViewChild('commentForm') formElement?: ElementRef;
 | 
			
		||||
 | 
			
		||||
    @Input() protected contextLevel!: string;
 | 
			
		||||
    @Input() protected instanceId!: number;
 | 
			
		||||
    @Input() protected componentName!: string;
 | 
			
		||||
    @Input() protected itemId!: number;
 | 
			
		||||
    @Input() protected area = '';
 | 
			
		||||
    @Input() content = '';
 | 
			
		||||
 | 
			
		||||
    processing = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send the comment or store it offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event.
 | 
			
		||||
     */
 | 
			
		||||
    async addComment(e: Event): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        CoreApp.closeKeyboard();
 | 
			
		||||
        const loadingModal = await CoreDomUtils.showModalLoading('core.sending', true);
 | 
			
		||||
        // Freeze the add comment button.
 | 
			
		||||
        this.processing = true;
 | 
			
		||||
        try {
 | 
			
		||||
            const commentsResponse = await CoreComments.addComment(
 | 
			
		||||
                this.content,
 | 
			
		||||
                this.contextLevel,
 | 
			
		||||
                this.instanceId,
 | 
			
		||||
                this.componentName,
 | 
			
		||||
                this.itemId,
 | 
			
		||||
                this.area,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            CoreForms.triggerFormSubmittedEvent(
 | 
			
		||||
                this.formElement,
 | 
			
		||||
                !!commentsResponse,
 | 
			
		||||
                CoreSites.getCurrentSiteId(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            ModalController.dismiss(commentsResponse).finally(() => {
 | 
			
		||||
                CoreDomUtils.showToast(
 | 
			
		||||
                    commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline',
 | 
			
		||||
                    true,
 | 
			
		||||
                    3000,
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
            this.processing = false;
 | 
			
		||||
        } finally {
 | 
			
		||||
            loadingModal.dismiss();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close modal.
 | 
			
		||||
     */
 | 
			
		||||
    closeModal(): void {
 | 
			
		||||
        CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
 | 
			
		||||
        ModalController.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <h2>{{ 'core.comments.addcomment' | translate }}</h2>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
 | 
			
		||||
                <ion-icon name="fas-times" slot="icon-only" aria-hidden=true></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <form name="itemEdit" (ngSubmit)="addComment($event)" #commentForm>
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <ion-textarea placeholder="{{ 'core.comments.addcomment' | translate }}" rows="5" [(ngModel)]="content"
 | 
			
		||||
                    name="content" required="required">
 | 
			
		||||
                </ion-textarea>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
        <div class="ion-padding">
 | 
			
		||||
            <ion-button expand="block" type="submit" [disabled]="processing || content.length < 1">
 | 
			
		||||
                {{ 'core.comments.savecomment' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
</ion-content>
 | 
			
		||||
@ -14,20 +14,17 @@
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { CoreCommentsAddComponent } from './add/add-modal';
 | 
			
		||||
import { CoreCommentsCommentsComponent } from './comments/comments';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        CoreCommentsCommentsComponent,
 | 
			
		||||
        CoreCommentsAddComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        CoreCommentsCommentsComponent,
 | 
			
		||||
        CoreCommentsAddComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class CoreCommentsComponentsModule {}
 | 
			
		||||
 | 
			
		||||
@ -34,75 +34,95 @@
 | 
			
		||||
            [message]="'core.comments.nocomments' | translate">
 | 
			
		||||
        </core-empty-box>
 | 
			
		||||
 | 
			
		||||
        <ion-card class="core-warning-card" *ngIf="hasOffline">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
        <!-- Load previous messages. -->
 | 
			
		||||
        <core-infinite-loading [enabled]="canLoadMore" position="top" (action)="loadPrevious($event)" [error]="loadMoreError">
 | 
			
		||||
        </core-infinite-loading>
 | 
			
		||||
 | 
			
		||||
        <ion-card *ngIf="offlineComment" (click)="addComment($event)">
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
                <core-user-avatar [user]="offlineComment" slot="start"></core-user-avatar>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p class="item-heading">{{ offlineComment.fullname }}</p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        <ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'core.notsent' | translate }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-button *ngIf="showDelete" slot="end" fill="clear" [@coreSlideInOut]="'fromRight'" color="danger"
 | 
			
		||||
                    (click)="deleteComment($event, offlineComment)" [attr.aria-label]="'core.delete' | translate">
 | 
			
		||||
                    <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <core-format-text clean="true" [text]="offlineComment.content" [contextLevel]="contextLevel"
 | 
			
		||||
                        [contextInstanceId]="instanceId" [courseId]="courseId">
 | 
			
		||||
                    </core-format-text>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
        <ion-list class="addon-messages-discussion-container">
 | 
			
		||||
            <ng-container *ngFor="let comment of comments; index as index; last as last">
 | 
			
		||||
 | 
			
		||||
        <ion-card *ngFor="let comment of comments">
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
                <core-user-avatar [user]="comment" slot="start"></core-user-avatar>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p class="item-heading">{{ comment.fullname }}</p>
 | 
			
		||||
                    <p *ngIf="!comment.deleted">{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}</p>
 | 
			
		||||
                    <p *ngIf="comment.deleted">
 | 
			
		||||
                        <ion-icon name="fas-trash" aria-hidden="true"></ion-icon> <span class="ion-text-wrap">
 | 
			
		||||
                            {{ 'core.deletedoffline' | translate }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                <p class="ion-text-center addon-messages-date" *ngIf="comment.showDate">
 | 
			
		||||
                    {{ comment.timecreated * 1000 | coreFormatDate: "strftimedayshort" }}
 | 
			
		||||
                </p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-button *ngIf="showDelete && !comment.deleted && comment.delete" slot="end" fill="clear"
 | 
			
		||||
                    [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, comment)"
 | 
			
		||||
                    [attr.aria-label]="'core.delete' | translate">
 | 
			
		||||
                    <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
                <ion-button *ngIf="showDelete && comment.deleted" slot="end" fill="clear" color="danger"
 | 
			
		||||
                    (click)="undoDeleteComment($event, comment)" [attr.aria-label]="'core.restore' | translate">
 | 
			
		||||
                    <ion-icon name="fas-undo-alt" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap addon-message"
 | 
			
		||||
                    [class.addon-message-mine]="comment.userid == currentUserId"
 | 
			
		||||
                    [class.addon-message-not-mine]="comment.userid != currentUserId"
 | 
			
		||||
                    [class.addon-message-no-user]="!comment.showUserData"
 | 
			
		||||
                    [@coreSlideInOut]="comment.userid == currentUserId ? '' : 'fromLeft'">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <!-- User data. -->
 | 
			
		||||
                        <h2 class="addon-message-user" *ngIf="comment.showUserData">
 | 
			
		||||
                            <core-user-avatar slot="start" [user]="comment" [linkProfile]="false">
 | 
			
		||||
                            </core-user-avatar>
 | 
			
		||||
                            <div>{{ comment.fullname }}</div>
 | 
			
		||||
                        </h2>
 | 
			
		||||
 | 
			
		||||
                        <p class="addon-message-text">
 | 
			
		||||
                            <core-format-text [text]="comment.content" [contextLevel]="contextLevel" [contextInstanceId]="instanceId"
 | 
			
		||||
                                [courseId]="courseId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-note>
 | 
			
		||||
                        <ng-container *ngIf="!comment.deleted">
 | 
			
		||||
                            {{ comment.timecreated * 1000 | coreFormatDate: 'strftimetime' }}
 | 
			
		||||
                        </ng-container>
 | 
			
		||||
                        <ng-container *ngIf="comment.deleted">
 | 
			
		||||
                            <ion-icon name="fas-trash" aria-hidden="true"></ion-icon> <span class="ion-text-wrap">
 | 
			
		||||
                                {{ 'core.deletedoffline' | translate }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </ng-container>
 | 
			
		||||
                    </ion-note>
 | 
			
		||||
                    <div class="tail" *ngIf="comment.showTail"></div>
 | 
			
		||||
                    <ion-button *ngIf="showDelete && !comment.deleted && comment.delete" slot="end" fill="clear"
 | 
			
		||||
                        [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, comment)"
 | 
			
		||||
                        [attr.aria-label]="'core.delete' | translate" class="addon-messages-delete-button">
 | 
			
		||||
                        <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                    <ion-button *ngIf="showDelete && comment.deleted" slot="end" fill="clear" color="danger"
 | 
			
		||||
                        (click)="undoDeleteComment($event, comment)" [attr.aria-label]="'core.restore' | translate"
 | 
			
		||||
                        class="addon-messages-delete-button">
 | 
			
		||||
                        <ion-icon name="fas-undo-alt" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <ion-item
 | 
			
		||||
                *ngIf="offlineComment"
 | 
			
		||||
                class="ion-text-wrap addon-message addon-message-mine"
 | 
			
		||||
            >
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <!-- User data. -->
 | 
			
		||||
                    <p class="ion-text-center">
 | 
			
		||||
                        <ion-icon name="fas-exclamation-triangle" aria-hidden="true"></ion-icon>
 | 
			
		||||
                        {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
 | 
			
		||||
                    </p>
 | 
			
		||||
 | 
			
		||||
                    <p class="addon-message-text">
 | 
			
		||||
                        <core-format-text [text]="offlineComment.content" [contextLevel]="contextLevel" [contextInstanceId]="instanceId"
 | 
			
		||||
                            [courseId]="courseId">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                    </p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-note>
 | 
			
		||||
                    <ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'core.notsent' | translate }}
 | 
			
		||||
                </ion-note>
 | 
			
		||||
                <div class="tail"></div>
 | 
			
		||||
                <ion-button *ngIf="showDelete" slot="end" fill="clear"
 | 
			
		||||
                    [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, offlineComment)"
 | 
			
		||||
                    [attr.aria-label]="'core.delete' | translate" class="addon-messages-delete-button">
 | 
			
		||||
                    <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
 | 
			
		||||
        <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
 | 
			
		||||
    <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAddComments">
 | 
			
		||||
        <ion-fab-button (click)="addComment($event)" [attr.aria-label]="'core.comments.addcomment' | translate">
 | 
			
		||||
            <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>
 | 
			
		||||
        </ion-fab-button>
 | 
			
		||||
    </ion-fab>
 | 
			
		||||
</ion-content>
 | 
			
		||||
<ion-footer color="light" class="footer-adjustable" *ngIf="commentsLoaded">
 | 
			
		||||
    <ion-toolbar color="light">
 | 
			
		||||
        <core-send-message-form [sendDisabled]="sending" [message]="newComment"
 | 
			
		||||
            (onSubmit)="addComment($event)" [placeholder]="'core.comments.addcomment' | translate">
 | 
			
		||||
        </core-send-message-form>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-footer>
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreAnimations } from '@components/animations';
 | 
			
		||||
import { ActivatedRoute, Params } from '@angular/router';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import {
 | 
			
		||||
    CoreComments,
 | 
			
		||||
@ -33,13 +33,14 @@ import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { CoreUser } from '@features/user/services/user';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreCommentsOffline } from '@features/comments/services/comments-offline';
 | 
			
		||||
import { CoreCommentsDBRecord } from '@features/comments/services/database/comments';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreCommentsAddComponent } from '@features/comments/components/add/add-modal';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays comments.
 | 
			
		||||
@ -48,12 +49,13 @@ import { CoreCommentsAddComponent } from '@features/comments/components/add/add-
 | 
			
		||||
    selector: 'page-core-comments-viewer',
 | 
			
		||||
    templateUrl: 'viewer.html',
 | 
			
		||||
    animations: [CoreAnimations.SLIDE_IN_OUT],
 | 
			
		||||
    styleUrls: ['viewer.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonContent) content?: IonContent;
 | 
			
		||||
 | 
			
		||||
    comments: CoreCommentsDataWithUser[] = [];
 | 
			
		||||
    comments: CoreCommentsDataToDisplay[] = [];
 | 
			
		||||
    commentsLoaded = false;
 | 
			
		||||
    contextLevel!: ContextLevel;
 | 
			
		||||
    instanceId!: number;
 | 
			
		||||
@ -73,10 +75,12 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
    syncIcon = CoreConstants.ICON_LOADING;
 | 
			
		||||
    offlineComment?: CoreCommentsOfflineWithUser;
 | 
			
		||||
    currentUserId: number;
 | 
			
		||||
    sending = false;
 | 
			
		||||
    newComment = '';
 | 
			
		||||
 | 
			
		||||
    protected addDeleteCommentsAvailable = false;
 | 
			
		||||
    protected syncObserver?: CoreEventObserver;
 | 
			
		||||
    protected currentUser?: CoreUserProfile;
 | 
			
		||||
    protected viewDestroyed = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
@ -95,8 +99,6 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
                this.refreshIcon = CoreConstants.ICON_LOADING;
 | 
			
		||||
                this.syncIcon = CoreConstants.ICON_LOADING;
 | 
			
		||||
 | 
			
		||||
                this.content?.scrollToTop();
 | 
			
		||||
 | 
			
		||||
                this.page = 0;
 | 
			
		||||
                this.comments = [];
 | 
			
		||||
                this.fetchComments(false);
 | 
			
		||||
@ -151,9 +153,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
            );
 | 
			
		||||
            this.canAddComments = this.addDeleteCommentsAvailable && !!commentsResponse.canpost;
 | 
			
		||||
 | 
			
		||||
            let comments = commentsResponse.comments.sort((a, b) => b.timecreated - a.timecreated);
 | 
			
		||||
            let comments = commentsResponse.comments.sort((a, b) => a.timecreated - b.timecreated);
 | 
			
		||||
            if (typeof commentsResponse.count != 'undefined') {
 | 
			
		||||
                this.canLoadMore = (this.comments.length + comments.length) > commentsResponse.count;
 | 
			
		||||
                this.canLoadMore = (this.comments.length + comments.length) < commentsResponse.count;
 | 
			
		||||
            } else {
 | 
			
		||||
                // Old style.
 | 
			
		||||
                this.canLoadMore = commentsResponse.comments.length > 0 &&
 | 
			
		||||
@ -162,7 +164,13 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            comments = await Promise.all(comments.map((comment) => this.loadCommentProfile(comment)));
 | 
			
		||||
 | 
			
		||||
            this.comments = this.comments.concat(comments);
 | 
			
		||||
            this.comments = comments.concat(this.comments);
 | 
			
		||||
 | 
			
		||||
            this.comments.forEach((comment, index) => {
 | 
			
		||||
                comment.showDate = this.showDate(comment, this.comments[index - 1]);
 | 
			
		||||
                comment.showUserData = this.showUserData(comment, this.comments[index - 1]);
 | 
			
		||||
                comment.showTail = this.showTail(comment, this.comments[index + 1]);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.canDeleteComments = this.addDeleteCommentsAvailable &&
 | 
			
		||||
                (this.hasOffline || this.comments.some((comment) => !!comment.delete));
 | 
			
		||||
@ -179,6 +187,10 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
            this.commentsLoaded = true;
 | 
			
		||||
            this.refreshIcon = CoreConstants.ICON_REFRESH;
 | 
			
		||||
            this.syncIcon = CoreConstants.ICON_SYNC;
 | 
			
		||||
 | 
			
		||||
            if (this.page == 0) {
 | 
			
		||||
                this.scrollToBottom();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
@ -189,7 +201,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    loadMore(infiniteComplete?: () => void): Promise<void> {
 | 
			
		||||
    loadPrevious(infiniteComplete?: () => void): Promise<void> {
 | 
			
		||||
        this.page++;
 | 
			
		||||
        this.canLoadMore = false;
 | 
			
		||||
 | 
			
		||||
@ -262,34 +274,41 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a new comment to the list.
 | 
			
		||||
     * Send the comment or store it offline.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event.
 | 
			
		||||
     * @param text Comment text to add.
 | 
			
		||||
     */
 | 
			
		||||
    async addComment(e: Event): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
    async addComment(text: string): Promise<void> {
 | 
			
		||||
        CoreApp.closeKeyboard();
 | 
			
		||||
        const loadingModal = await CoreDomUtils.showModalLoading('core.sending', true);
 | 
			
		||||
        // Freeze the add comment button.
 | 
			
		||||
        this.sending = true;
 | 
			
		||||
        try {
 | 
			
		||||
            const commentsResponse = await CoreComments.addComment(
 | 
			
		||||
                text,
 | 
			
		||||
                this.contextLevel,
 | 
			
		||||
                this.instanceId,
 | 
			
		||||
                this.componentName,
 | 
			
		||||
                this.itemId,
 | 
			
		||||
                this.area,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        const params: Params = {
 | 
			
		||||
            contextLevel: this.contextLevel,
 | 
			
		||||
            instanceId: this.instanceId,
 | 
			
		||||
            componentName: this.componentName,
 | 
			
		||||
            itemId: this.itemId,
 | 
			
		||||
            area: this.area,
 | 
			
		||||
            content: this.offlineComment ? this.offlineComment!.content : '',
 | 
			
		||||
        };
 | 
			
		||||
            CoreDomUtils.showToast(
 | 
			
		||||
                commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline',
 | 
			
		||||
                true,
 | 
			
		||||
                3000,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        const comment = await CoreDomUtils.openModal<CoreCommentsDataWithUser>({
 | 
			
		||||
            component: CoreCommentsAddComponent,
 | 
			
		||||
            componentProps: params,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (comment) {
 | 
			
		||||
            if (commentsResponse) {
 | 
			
		||||
                this.invalidateComments();
 | 
			
		||||
 | 
			
		||||
            const addedComments = await this.loadCommentProfile(comment);
 | 
			
		||||
                const addedComments = await this.loadCommentProfile(commentsResponse);
 | 
			
		||||
                addedComments.showDate = this.showDate(addedComments, this.comments[this.comments.length - 1]);
 | 
			
		||||
                addedComments.showUserData = this.showUserData(addedComments, this.comments[this.comments.length - 1]);
 | 
			
		||||
                addedComments.showTail = this.showTail(addedComments, this.comments[this.comments.length + 1]);
 | 
			
		||||
 | 
			
		||||
                // Add the comment to the top.
 | 
			
		||||
            this.comments = [addedComments].concat(this.comments);
 | 
			
		||||
                this.comments = this.comments.concat([addedComments]);
 | 
			
		||||
                this.canDeleteComments = this.addDeleteCommentsAvailable;
 | 
			
		||||
 | 
			
		||||
                CoreEvents.trigger(CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT, {
 | 
			
		||||
@ -301,9 +320,18 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
                    countChange: 1,
 | 
			
		||||
                }, CoreSites.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        } else if (comment === false) {
 | 
			
		||||
            } else if (commentsResponse === false) {
 | 
			
		||||
                // Comments added in offline mode.
 | 
			
		||||
            return this.loadOfflineData();
 | 
			
		||||
                await this.loadOfflineData();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
        } finally {
 | 
			
		||||
            loadingModal.dismiss();
 | 
			
		||||
            this.sending = false;
 | 
			
		||||
 | 
			
		||||
            // New comments.
 | 
			
		||||
            this.scrollToBottom();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -313,7 +341,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param e Click event.
 | 
			
		||||
     * @param comment Comment to delete.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteComment(e: Event, comment: CoreCommentsDataWithUser | CoreCommentsOfflineWithUser): Promise<void> {
 | 
			
		||||
    async deleteComment(e: Event, comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
@ -349,8 +377,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
            const deletedOnline = await CoreComments.deleteComment(deleteComment);
 | 
			
		||||
            this.showDelete = false;
 | 
			
		||||
 | 
			
		||||
            if (deletedOnline) {
 | 
			
		||||
                const index = this.comments.findIndex((comment) => comment.id == comment.id);
 | 
			
		||||
            if (deletedOnline && 'id' in comment) {
 | 
			
		||||
                const index = this.comments.findIndex((commentinList) => commentinList.id == comment.id);
 | 
			
		||||
 | 
			
		||||
                if (index >= 0) {
 | 
			
		||||
                    this.comments.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
@ -396,7 +425,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param comment Comment object.
 | 
			
		||||
     * @return Promise resolved with modified comment when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadCommentProfile(comment: CoreCommentsDataWithUser): Promise<CoreCommentsDataWithUser> {
 | 
			
		||||
    protected async loadCommentProfile(comment: CoreCommentsDataToDisplay): Promise<CoreCommentsDataToDisplay> {
 | 
			
		||||
        // Get the user profile image.
 | 
			
		||||
        try {
 | 
			
		||||
            const user = await CoreUser.getProfile(comment.userid!, undefined, true);
 | 
			
		||||
@ -410,6 +439,54 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the user info should be displayed for the current message.
 | 
			
		||||
     * User data is only displayed if the previous message was from another user.
 | 
			
		||||
     *
 | 
			
		||||
     * @param comment Comment object.
 | 
			
		||||
     * @param prevComment Previous comment object.
 | 
			
		||||
     * @return Whether user data should be shown.
 | 
			
		||||
     */
 | 
			
		||||
    protected showUserData(
 | 
			
		||||
        comment: CoreCommentsDataToDisplay,
 | 
			
		||||
        prevComment?: CoreCommentsDataToDisplay,
 | 
			
		||||
    ): boolean {
 | 
			
		||||
        return comment.userid != this.currentUserId && (!prevComment || prevComment.userid != comment.userid || !!comment.showDate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a css tail should be shown.
 | 
			
		||||
     *
 | 
			
		||||
     * @param comment Comment object.
 | 
			
		||||
     * @param nextComment Previous comment object.
 | 
			
		||||
     * @return Whether user data should be shown.
 | 
			
		||||
     */
 | 
			
		||||
    protected showTail(
 | 
			
		||||
        comment: CoreCommentsDataToDisplay,
 | 
			
		||||
        nextComment?: CoreCommentsDataToDisplay,
 | 
			
		||||
    ): boolean {
 | 
			
		||||
        return !nextComment || nextComment.userid != comment.userid || !!nextComment.showDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the date should be displayed between messages (when the day changes at midnight for example).
 | 
			
		||||
     *
 | 
			
		||||
     * @param comment Comment object.
 | 
			
		||||
     * @param prevComment Previous comment object.
 | 
			
		||||
     * @return True if messages are from diferent days, false othetwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected showDate(
 | 
			
		||||
        comment: CoreCommentsDataToDisplay,
 | 
			
		||||
        prevComment?: CoreCommentsDataToDisplay,
 | 
			
		||||
    ): boolean {
 | 
			
		||||
        if (!prevComment) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if day has changed.
 | 
			
		||||
        return !moment(comment.timecreated * 1000).isSame(prevComment.timecreated * 1000, 'day');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load offline comments.
 | 
			
		||||
     *
 | 
			
		||||
@ -433,14 +510,10 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!this.currentUser) {
 | 
			
		||||
                this.currentUser = await CoreUser.getProfile(this.currentUserId, undefined, true);
 | 
			
		||||
            if (this.newComment == '') {
 | 
			
		||||
                this.newComment = this.offlineComment!.content;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.currentUser) {
 | 
			
		||||
                this.offlineComment!.profileimageurl = this.currentUser.profileimageurl;
 | 
			
		||||
                this.offlineComment!.fullname = this.currentUser.fullname;
 | 
			
		||||
            }
 | 
			
		||||
            this.offlineComment!.userid = this.currentUserId;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
@ -480,7 +553,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param e Click event.
 | 
			
		||||
     * @param comment Comment to delete.
 | 
			
		||||
     */
 | 
			
		||||
    async undoDeleteComment(e: Event, comment: CoreCommentsDataWithUser): Promise<void> {
 | 
			
		||||
    async undoDeleteComment(e: Event, comment: CoreCommentsDataToDisplay): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
@ -490,6 +563,18 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
        this.showDelete = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll bottom when render has finished.
 | 
			
		||||
     */
 | 
			
		||||
    protected scrollToBottom(): void {
 | 
			
		||||
        // Need a timeout to leave time to the view to be rendered.
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            if (!this.viewDestroyed) {
 | 
			
		||||
                this.content?.scrollToBottom();
 | 
			
		||||
            }
 | 
			
		||||
        }, 100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Toggle delete.
 | 
			
		||||
     */
 | 
			
		||||
@ -501,15 +586,19 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
 | 
			
		||||
     * Page destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.syncObserver && this.syncObserver.off();
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
        this.viewDestroyed = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CoreCommentsDataWithUser = CoreCommentsData & {
 | 
			
		||||
export type CoreCommentsDataToDisplay = CoreCommentsData & {
 | 
			
		||||
    profileimageurl?: string;
 | 
			
		||||
    fullname?: string;
 | 
			
		||||
    deleted?: boolean;
 | 
			
		||||
    showDate?: boolean;
 | 
			
		||||
    showTail?: boolean;
 | 
			
		||||
    showUserData?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreCommentsOfflineWithUser = CoreCommentsDBRecord & {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								src/core/features/comments/pages/viewer/viewer.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/core/features/comments/pages/viewer/viewer.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
@import "~theme/components/discussion.scss";
 | 
			
		||||
@ -85,11 +85,11 @@ export class CoreCommentsProvider {
 | 
			
		||||
        itemId: number,
 | 
			
		||||
        area: string = '',
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreCommentsData | boolean> {
 | 
			
		||||
    ): Promise<CoreCommentsData | false> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        // Convenience function to store a comment to be synchronized later.
 | 
			
		||||
        const storeOffline = async (): Promise<boolean> => {
 | 
			
		||||
        const storeOffline = async (): Promise<false> => {
 | 
			
		||||
            await CoreCommentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId);
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										185
									
								
								src/theme/components/discussion.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/theme/components/discussion.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,185 @@
 | 
			
		||||
@import "~theme/globals.scss";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    ion-content {
 | 
			
		||||
        --background: var(--background-alternative);
 | 
			
		||||
 | 
			
		||||
        &::part(scroll) {
 | 
			
		||||
            padding-bottom: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-messages-discussion-container {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        padding-bottom: 15px;
 | 
			
		||||
        background: var(--background-alternative);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-messages-date {
 | 
			
		||||
        font-weight: normal;
 | 
			
		||||
        font-size: 0.9rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Message item.
 | 
			
		||||
    ion-item.addon-message {
 | 
			
		||||
        border: 0;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        padding: 0 8px 0 8px;
 | 
			
		||||
        margin: 10px 8px 0 8px;
 | 
			
		||||
        --background: var(--addon-messages-message-bg);
 | 
			
		||||
        background: var(--background);
 | 
			
		||||
        align-self: flex-start;
 | 
			
		||||
        width: 90%;
 | 
			
		||||
        max-width: 90%;
 | 
			
		||||
        --min-height: var(--a11y-min-target-size);
 | 
			
		||||
        position: relative;
 | 
			
		||||
        @include core-transition(width);
 | 
			
		||||
        // This is needed to display bubble tails.
 | 
			
		||||
        overflow: visible;
 | 
			
		||||
 | 
			
		||||
        &::part(native) {
 | 
			
		||||
            --inner-border-width: 0;
 | 
			
		||||
            --inner-padding-end: 0;
 | 
			
		||||
            padding: 0;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        core-format-text > p:only-child {
 | 
			
		||||
            display: inline;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-message-user {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: row;
 | 
			
		||||
            justify-content: space-between;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            margin-bottom: .5rem;
 | 
			
		||||
            margin-top: 0;
 | 
			
		||||
            color: var(--ion-text-color);
 | 
			
		||||
 | 
			
		||||
            core-user-avatar {
 | 
			
		||||
                display: block;
 | 
			
		||||
                --core-avatar-size: var(--addon-messages-avatar-size);
 | 
			
		||||
                margin: 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            div {
 | 
			
		||||
                font-weight: 500;
 | 
			
		||||
                flex-grow: 1;
 | 
			
		||||
                @include padding-horizontal(.5rem);
 | 
			
		||||
                overflow: hidden;
 | 
			
		||||
                text-overflow: ellipsis;
 | 
			
		||||
                white-space: nowrap;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ion-note {
 | 
			
		||||
            color: var(--addon-messages-message-note-text);
 | 
			
		||||
            font-size: var(--addon-messages-message-note-font-size);
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            padding: 0 0 8px 0;
 | 
			
		||||
            align-self: flex-end;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &[tappable]:active {
 | 
			
		||||
            --background: var(--addon-messages-message-activated-bg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ion-label {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            padding: 8px 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-message-text {
 | 
			
		||||
            display: inline-flex;
 | 
			
		||||
            * {
 | 
			
		||||
                color: var(--ion-text-color);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .tail {
 | 
			
		||||
            content: '';
 | 
			
		||||
            width: 0;
 | 
			
		||||
            height: 0;
 | 
			
		||||
            border: 0.5rem solid transparent;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            touch-action: none;
 | 
			
		||||
            bottom: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Defines when an item-message is the user's.
 | 
			
		||||
        &.addon-message-mine {
 | 
			
		||||
            --background: var(--addon-messages-message-mine-bg);
 | 
			
		||||
            align-self: flex-end;
 | 
			
		||||
 | 
			
		||||
            &[tappable]:active {
 | 
			
		||||
                --background: var(--addon-messages-message-mine-activated-bg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .spinner {
 | 
			
		||||
                @include float(end);
 | 
			
		||||
                @include margin(2px, -3px, -2px, 5px);
 | 
			
		||||
 | 
			
		||||
                svg {
 | 
			
		||||
                    width: 16px;
 | 
			
		||||
                    height: 16px;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .tail {
 | 
			
		||||
                @include position(null, -8px, null, null);
 | 
			
		||||
                @include margin-horizontal(null, -0.5rem);
 | 
			
		||||
                border-bottom-color: var(--addon-messages-message-mine-bg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            &[tappable]:active .tail {
 | 
			
		||||
                border-bottom-color: var(--addon-messages-message-mine-activated-bg);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.addon-message-not-mine .tail {
 | 
			
		||||
            border-bottom-color: var(--addon-messages-message-bg);
 | 
			
		||||
            @include position(null, null, null, -8px);
 | 
			
		||||
            @include margin-horizontal(-0.5rem, null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &[tappable].addon-message-not-mine.activated .tail {
 | 
			
		||||
            border-bottom-color: var(--addon-messages-message-activated-bg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-messages-delete-button {
 | 
			
		||||
            min-height: initial;
 | 
			
		||||
            line-height: initial;
 | 
			
		||||
            @include margin(0, null, 0, null);
 | 
			
		||||
            height: var(--a11y-min-target-size) !important;
 | 
			
		||||
            align-self: flex-end;
 | 
			
		||||
 | 
			
		||||
            ion-icon {
 | 
			
		||||
                font-size: 1.4em;
 | 
			
		||||
                line-height: initial;
 | 
			
		||||
                color: var(--ion-color-danger);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.addon-message-no-user {
 | 
			
		||||
            margin-top: 8px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ion-item.addon-message.addon-message-mine + ion-item.addon-message.addon-message-no-user.addon-message-mine,
 | 
			
		||||
    ion-item.addon-message.addon-message-not-mine + ion-item.addon-message.addon-message-no-user.addon-message-not-mine {
 | 
			
		||||
        .item-heading {
 | 
			
		||||
            margin-bottom: 0;
 | 
			
		||||
        }
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(.ios) {
 | 
			
		||||
    ion-footer .toolbar:last-child {
 | 
			
		||||
        padding-bottom: 4px;
 | 
			
		||||
        min-height: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -364,7 +364,7 @@ ion-toolbar {
 | 
			
		||||
 | 
			
		||||
.item-dimmed {
 | 
			
		||||
    opacity: 0.7;
 | 
			
		||||
    --background: var(--gray-lighter);
 | 
			
		||||
    --background: var(--ion-color-light);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extra text colors.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user