diff options
Diffstat (limited to 'webclient')
25 files changed, 245 insertions, 53 deletions
diff --git a/webclient/app-controller.js b/webclient/app-controller.js index a77d32a5ac..ea48cbb011 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/app-directive.js b/webclient/app-directive.js index eee0d3842f..75283598ab 100644 --- a/webclient/app-directive.js +++ b/webclient/app-directive.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/app-filter.js b/webclient/app-filter.js index e0e8130e45..27f435674f 100644 --- a/webclient/app-filter.js +++ b/webclient/app-filter.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -97,23 +97,28 @@ angular.module('matrixWebClient') // Else, build the name from its users var room = $rootScope.events.rooms[room_id]; if (room) { - if (room.members) { + var room_name_event = room["m.room.name"]; + + if (room_name_event) { + roomName = room_name_event.content.name; + } + else if (room.members) { // Limit the room renaming to 1:1 room if (2 === Object.keys(room.members).length) { for (var i in room.members) { var member = room.members[i]; - if (member.user_id !== matrixService.config().user_id) { + if (member.state_key !== matrixService.config().user_id) { - if (member.user_id in $rootScope.presence) { + if (member.state_key in $rootScope.presence) { // If the user is available in presence, use the displayname there // as it is the most uptodate - roomName = $rootScope.presence[member.user_id].content.displayname; + roomName = $rootScope.presence[member.state_key].content.displayname; } else if (member.content.displayname) { roomName = member.content.displayname; } else { - roomName = member.user_id; + roomName = member.state_key; } } } @@ -140,7 +145,7 @@ angular.module('matrixWebClient') roomName = $rootScope.presence[userID].content.displayname; } else { - roomName = member.user_id; + roomName = userID; } } } diff --git a/webclient/app.js b/webclient/app.js index dac4f048cd..d25e2a6234 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js index c5e4ae07a8..14e2f772f7 100644 --- a/webclient/components/fileInput/file-input-directive.js +++ b/webclient/components/fileInput/file-input-directive.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js index 699a3cbffc..e0f67b2c6c 100644 --- a/webclient/components/fileUpload/file-upload-service.js +++ b/webclient/components/fileUpload/file-upload-service.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index d6a0600132..ee478d2eb0 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,7 +32,9 @@ angular.module('eventHandlerService', []) var MSG_EVENT = "MSG_EVENT"; var MEMBER_EVENT = "MEMBER_EVENT"; var PRESENCE_EVENT = "PRESENCE_EVENT"; + var POWERLEVEL_EVENT = "POWERLEVEL_EVENT"; var CALL_EVENT = "CALL_EVENT"; + var NAME_EVENT = "NAME_EVENT"; var InitialSyncDeferred = $q.defer(); @@ -95,7 +97,7 @@ angular.module('eventHandlerService', []) } } - $rootScope.events.rooms[event.room_id].members[event.user_id] = event; + $rootScope.events.rooms[event.room_id].members[event.state_key] = event; $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent); }; @@ -107,10 +109,20 @@ angular.module('eventHandlerService', []) var handlePowerLevels = function(event, isLiveEvent) { initRoom(event.room_id); - $rootScope.events.rooms[event.room_id][event.type] = event; + // Keep the latest data. Do not care of events that come when paginating back + if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) { + $rootScope.events.rooms[event.room_id][event.type] = event; + $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent); + } + }; - //TODO - //$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent); + var handleRoomName = function(event, isLiveEvent) { + console.log("handleRoomName " + isLiveEvent); + + initRoom(event.room_id); + + $rootScope.events.rooms[event.room_id][event.type] = event; + $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent); }; var handleCallEvent = function(event, isLiveEvent) { @@ -122,7 +134,9 @@ angular.module('eventHandlerService', []) MSG_EVENT: MSG_EVENT, MEMBER_EVENT: MEMBER_EVENT, PRESENCE_EVENT: PRESENCE_EVENT, + POWERLEVEL_EVENT: POWERLEVEL_EVENT, CALL_EVENT: CALL_EVENT, + NAME_EVENT: NAME_EVENT, handleEvent: function(event, isLiveEvent) { @@ -146,7 +160,9 @@ angular.module('eventHandlerService', []) case 'm.room.power_levels': handlePowerLevels(event, isLiveEvent); break; - + case 'm.room.name': + handleRoomName(event, isLiveEvent); + break; default: console.log("Unable to handle event type " + event.type); console.log(JSON.stringify(event, undefined, 4)); diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js index 441148670e..1c0f7712b4 100644 --- a/webclient/components/matrix/event-stream-service.js +++ b/webclient/components/matrix/event-stream-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index 47b63d7f2f..3e13e4e81f 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js index d9e2e8baa3..ca86b473e7 100644 --- a/webclient/components/matrix/matrix-phone-service.js +++ b/webclient/components/matrix/matrix-phone-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index d399aa3cb9..7c6d4ae50f 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -167,6 +167,29 @@ angular.module('matrixService', []) return doRequest("POST", path, undefined, data); }, + // Change the membership of an another user + setMembership: function(room_id, user_id, membershipValue) { + // The REST path spec + var path = "/rooms/$room_id/state/m.room.member/$user_id"; + path = path.replace("$room_id", encodeURIComponent(room_id)); + path = path.replace("$user_id", user_id); + + return doRequest("PUT", path, undefined, { + membership: membershipValue + }); + }, + + // Bans a user from from a room + ban: function(room_id, user_id, reason) { + var path = "/rooms/$room_id/ban"; + path = path.replace("$room_id", encodeURIComponent(room_id)); + + return doRequest("POST", path, undefined, { + user_id: user_id, + reason: reason + }); + }, + // Retrieves the room ID corresponding to a room alias resolveRoomAlias:function(room_alias) { var path = "/_matrix/client/api/v1/directory/room/$room_alias"; @@ -253,7 +276,7 @@ angular.module('matrixService', []) // get a list of public rooms on your home server publicRooms: function() { - var path = "/publicRooms" + var path = "/publicRooms"; return doRequest("GET", path); }, @@ -309,7 +332,7 @@ angular.module('matrixService', []) // hit the Identity Server for a 3PID request. linkEmail: function(email, clientSecret, sendAttempt) { - var path = "/_matrix/identity/api/v1/validate/email/requestToken" + var path = "/_matrix/identity/api/v1/validate/email/requestToken"; var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt; var headers = {}; headers["Content-Type"] = "application/x-www-form-urlencoded"; @@ -414,7 +437,8 @@ angular.module('matrixService', []) state: presence }); }, - + + /****** Permanent storage of user information ******/ // Returns the current config @@ -514,6 +538,35 @@ angular.module('matrixService', []) } } return powerLevel; + }, + + /** + * Change or reset the power level of a user + * @param {String} room_id the room id + * @param {String} user_id the user id + * @param {Number} powerLevel a value between 0 and 10 + * If undefined, the user power level will be reset, ie he will use the default room user power level + * @returns {promise} an $http promise + */ + setUserPowerLevel: function(room_id, user_id, powerLevel) { + + // Hack: currently, there is no home server API so do it by hand by updating + // the current m.room.power_levels of the room and send it to the server + var room = $rootScope.events.rooms[room_id]; + if (room && room["m.room.power_levels"]) { + var content = angular.copy(room["m.room.power_levels"].content); + content[user_id] = powerLevel; + + var path = "/rooms/$room_id/state/m.room.power_levels"; + path = path.replace("$room_id", encodeURIComponent(room_id)); + + return doRequest("PUT", path, undefined, content); + } + + // The room does not exist or does not contain power_levels data + var deferred = $q.defer(); + deferred.reject({data:{error: "Invalid room: " + room_id}}); + return deferred.promise; } }; diff --git a/webclient/components/matrix/presence-service.js b/webclient/components/matrix/presence-service.js index 555118133b..952c8ec8a9 100644 --- a/webclient/components/matrix/presence-service.js +++ b/webclient/components/matrix/presence-service.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js index 3df2f04458..b417cc5b39 100644 --- a/webclient/components/utilities/utilities-service.js +++ b/webclient/components/utilities/utilities-service.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js index f4ce3053ea..11b3682d34 100644 --- a/webclient/home/home-controller.js +++ b/webclient/home/home-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js index e367b2f0c5..5ef39a7122 100644 --- a/webclient/login/login-controller.js +++ b/webclient/login/login-controller.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js index d70e83c3bd..b7584a7d33 100644 --- a/webclient/login/register-controller.js +++ b/webclient/login/register-controller.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js index d7d3bf4053..3209f2cbdf 100644 --- a/webclient/recents/recents-controller.js +++ b/webclient/recents/recents-controller.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/recents/recents-filter.js b/webclient/recents/recents-filter.js index 45653fca96..d80de6fbeb 100644 --- a/webclient/recents/recents-filter.js +++ b/webclient/recents/recents-filter.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html index db3b0fb32f..9978e08b13 100644 --- a/webclient/recents/recents.html +++ b/webclient/recents/recents.html @@ -23,8 +23,8 @@ <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" > <div ng-switch-when="m.room.member"> {{ room.lastMsg.user_id }} - {{ {"join": "joined", "leave": "left", "invite": "invited"}[room.lastMsg.content.membership] }} - {{ room.lastMsg.content.membership === "invite" ? (room.lastMsg.state_key || '') : '' }} + {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }} + {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }} </div> <div ng-switch-when="m.room.message"> diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 72c290ad73..52c57856ee 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -85,6 +85,14 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) updatePresence(event); } }); + + $scope.$on(eventHandlerService.POWERLEVEL_EVENT, function(ngEvent, event, isLive) { + if (isLive && event.room_id === $scope.room_id) { + for (var user_id in event.content) { + updateUserPowerLevel(user_id); + } + } + }); $scope.memberCount = function() { return Object.keys($scope.members).length; @@ -161,6 +169,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) var updateMemberList = function(chunk) { if (chunk.room_id != $scope.room_id) return; + // Ignore banned people + if ("ban" === chunk.membership) { + return; + } + // set target_user_id to keep things clear var target_user_id = chunk.state_key; @@ -240,6 +253,29 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) var member = $scope.members[user_id]; if (member) { member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id); + + normaliseMembersPowerLevels(); + } + } + + // Normalise users power levels so that the user with the higher power level + // will have a bar covering 100% of the width of his avatar + var normaliseMembersPowerLevels = function() { + // Find the max power level + var maxPowerLevel = 0; + for (var i in $scope.members) { + var member = $scope.members[i]; + if (member.powerLevel) { + maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel); + } + } + + // Normalized them on a 0..100% scale to be use in css width + if (maxPowerLevel) { + for (var i in $scope.members) { + var member = $scope.members[i]; + member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel; + } } } @@ -250,28 +286,93 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) $scope.state.sending = true; - // Send the text message var promise; - // FIXME: handle other commands too - if ($scope.textInput.indexOf("/me") === 0) { - promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4)); - } - else if ($scope.textInput.indexOf("/nick ") === 0) { - // Change user display name - promise = matrixService.setDisplayName($scope.textInput.substr(6)); + + // Check for IRC style commands first + if ($scope.textInput.indexOf("/") === 0) { + var args = $scope.textInput.split(' '); + var cmd = args[0]; + + switch (cmd) { + case "/me": + var emoteMsg = args.slice(1).join(' '); + promise = matrixService.sendEmoteMessage($scope.room_id, emoteMsg); + break; + + case "/nick": + // Change user display name + if (2 === args.length) { + promise = matrixService.setDisplayName(args[1]); + } + break; + + case "/kick": + // Kick a user from the room + if (2 === args.length) { + var user_id = args[1]; + + // Set his state in the room as leave + promise = matrixService.setMembership($scope.room_id, user_id, "leave"); + } + break; + + case "/ban": + // Ban a user from the room + if (2 <= args.length) { + // TODO: The user may have entered the display name + // Need display name -> user_id resolution. Pb: how to manage user with same display names? + var user_id = args[1]; + + // Does the user provide a reason? + if (3 <= args.length) { + var reason = args.slice(2).join(' '); + } + promise = matrixService.ban($scope.room_id, user_id, reason); + } + break; + + case "/unban": + // Unban a user from the room + if (2 === args.length) { + var user_id = args[1]; + + // Reset the user membership to leave to unban him + promise = matrixService.setMembership($scope.room_id, user_id, "leave"); + } + break; + + case "/op": + // Define the power level of a user + if (3 === args.length) { + var user_id = args[1]; + var powerLevel = parseInt(args[2]); + promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel); + } + break; + + case "/deop": + // Reset the power level of a user + if (2 === args.length) { + var user_id = args[1]; + promise = matrixService.setUserPowerLevel($scope.room_id, user_id, undefined); + } + break; + } } - else { + + if (!promise) { + // Send the text message promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput); } promise.then( function() { - console.log("Sent message"); + console.log("Request successfully sent"); $scope.textInput = ""; $scope.state.sending = false; }, function(error) { - $scope.feedback = "Failed to send: " + error.data.error; + $scope.feedback = "Request failed: " + error.data.error; $scope.state.sending = false; }); }; @@ -363,7 +464,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) onInit3(); }, function(reason) { - $scope.feedback = "Can't join room: " + reason; + console.log("Can't join room: " + JSON.stringify(reason)); + $scope.feedback = "You do not have permission to join this room"; }); } else { diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js index 1a99a37abb..659bcbc60f 100644 --- a/webclient/room/room-directive.js +++ b/webclient/room/room-directive.js @@ -1,5 +1,5 @@ /* - Copyright 2014 matrix.org + Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/room/room.html b/webclient/room/room.html index e672b1d7e2..e29f511ecf 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -24,7 +24,7 @@ title="{{ member.id }}" width="80" height="80"/> <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/> - <div class="userPowerLevel" ng-style="{'width': (10 * member.powerLevel) +'%'}"></div> + <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div> <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div> </td> <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')"> @@ -50,8 +50,9 @@ <div class="bubble"> <span ng-show='msg.type === "m.room.member"'> {{ members[msg.user_id].displayname || msg.user_id }} - {{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }} - {{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }} + {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }} + {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }} + </span> <span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/> <span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/> diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js index dc680ef075..7a26367a1b 100644 --- a/webclient/settings/settings-controller.js +++ b/webclient/settings/settings-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html index 03927838d2..49dc603540 100644 --- a/webclient/settings/settings.html +++ b/webclient/settings/settings.html @@ -74,6 +74,21 @@ <div>Access token: {{ config.access_token }} </div> </div> <br/> + + <h3>Commands</h3> + <div class="section"> + The following commands are available in the room chat: + <ul> + <li>/nick <display_name>: change your display name</li> + <li>/me <action>: send the action you are doing. /me will be replaced by your display name</li> + <li>/kick <user_id>: kick the user</li> + <li>/ban <user_id> [<reason>]: ban the user</li> + <li>/unban <user_id>: unban the user</li> + <li>/op <user_id> <power_level>: set user power level</li> + <li>/deop <user_id>: reset user power level to the room default value</li> + </ul> + </div> + <br/> {{ feedback }} diff --git a/webclient/user/user-controller.js b/webclient/user/user-controller.js index b5b2d439a2..3940db6683 100644 --- a/webclient/user/user-controller.js +++ b/webclient/user/user-controller.js @@ -1,5 +1,5 @@ /* -Copyright 2014 matrix.org +Copyright 2014 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. |