User permissions

Fix forward layout
This commit is contained in:
Eduard Kuzmenko 2021-03-19 16:14:42 +04:00
parent 3d1ff73611
commit 2d299c41fc
24 changed files with 880 additions and 348 deletions

View File

@ -91,6 +91,8 @@ export class AppNavigationController {
}, options);
}
history.scrollRestoration = 'manual';
this.pushState(); // * push init state
}

View File

@ -9,7 +9,7 @@ export class SearchGroup {
list: HTMLUListElement;
constructor(public name: string, public type: string, private clearable = true, className?: string, clickable = true, public autonomous = true, public onFound?: () => void) {
this.list = document.createElement('ul');
this.list = appDialogsManager.createChatList();
this.container = document.createElement('div');
if(className) this.container.className = className;

View File

@ -9,15 +9,19 @@ import { cancelEvent, findUpAttribute, findUpClassName } from "../helpers/dom";
import Scrollable from "./scrollable";
import { FocusDirection } from "../helpers/fastSmoothScroll";
import CheckboxField from "./checkboxField";
import appProfileManager from "../lib/appManagers/appProfileManager";
type PeerType = 'contacts' | 'dialogs';
type PeerType = 'contacts' | 'dialogs' | 'channelParticipants';
// TODO: правильная сортировка для addMembers, т.е. для peerType: 'contacts', потому что там идут сначала контакты - потом неконтакты, а должно всё сортироваться по имени
let loadedAllDialogs = false, loadAllDialogsPromise: Promise<any>;
export default class AppSelectPeers {
public container = document.createElement('div');
public list = document.createElement('ul');
public list = appDialogsManager.createChatList(/* {
handheldsSize: 66,
avatarSize: 48
} */);
public chatsContainer = document.createElement('div');
public scrollable: Scrollable;
public selectedScrollable: Scrollable;
@ -37,7 +41,7 @@ export default class AppSelectPeers {
private query = '';
private cachedContacts: number[];
private loadedWhat: Partial<{[k in 'dialogs' | 'archived' | 'contacts']: true}> = {};
private loadedWhat: Partial<{[k in 'dialogs' | 'archived' | 'contacts' | 'channelParticipants']: true}> = {};
private renderedPeerIds: Set<number> = new Set();
@ -48,16 +52,22 @@ export default class AppSelectPeers {
private chatRightsAction: ChatRights;
private multiSelect = true;
private rippleEnabled = true;
private avatarSize = 48;
private tempIds: {[k in keyof AppSelectPeers['loadedWhat']]: number} = {};
private peerId = 0;
constructor(options: {
appendTo: AppSelectPeers['appendTo'],
onChange?: AppSelectPeers['onChange'],
peerType?: AppSelectPeers['peerType'],
peerId?: number,
onFirstRender?: () => void,
renderResultsFunc?: AppSelectPeers['renderResultsFunc'],
chatRightsAction?: AppSelectPeers['chatRightsAction'],
multiSelect?: AppSelectPeers['multiSelect'],
rippleEnabled?: boolean
rippleEnabled?: boolean,
avatarSize?: AppSelectPeers['avatarSize'],
}) {
for(let i in options) {
// @ts-ignore
@ -66,8 +76,19 @@ export default class AppSelectPeers {
this.container.classList.add('selector');
this.peerType.forEach(type => {
this.tempIds[type] = 0;
});
let needSwitchList = false;
const f = (this.renderResultsFunc || this.renderResults).bind(this);
this.renderResultsFunc = (peerIds: number[]) => {
if(needSwitchList) {
this.scrollable.splitUp.replaceWith(this.list);
this.scrollable.setVirtualContainer(this.list);
needSwitchList = false;
}
peerIds = peerIds.filter(peerId => {
const notRendered = !this.renderedPeerIds.has(peerId);
if(notRendered) this.renderedPeerIds.add(peerId);
@ -149,21 +170,26 @@ export default class AppSelectPeers {
const value = this.input.value;
if(this.query !== value) {
if(this.peerType.includes('contacts')) {
delete this.loadedWhat.contacts;
this.cachedContacts = null;
}
//if(this.peerType.includes('dialogs')) {
delete this.loadedWhat.dialogs;
delete this.loadedWhat.archived;
this.folderId = 0;
this.offsetIndex = 0;
//}
for(let i in this.tempIds) {
// @ts-ignore
++this.tempIds[i];
}
this.list = appDialogsManager.createChatList();
this.promise = null;
this.list.innerHTML = '';
this.loadedWhat = {};
this.query = value;
this.renderedPeerIds.clear();
needSwitchList = true;
//console.log('selectPeers input:', this.query);
this.getMoreResults();
@ -204,10 +230,16 @@ export default class AppSelectPeers {
// в десктопе - сначала без группы, потом архивные, потом контакты без сообщений
const pageCount = appPhotosManager.windowH / 72 * 1.25 | 0;
const tempId = ++this.tempIds.dialogs;
this.promise = appMessagesManager.getConversations(this.query, this.offsetIndex, pageCount, this.folderId);
const value = await this.promise;
this.promise = null;
if(this.tempIds.dialogs !== tempId) {
return;
}
let dialogs = value.dialogs as Dialog[];
if(dialogs.length) {
const newOffsetIndex = dialogs[dialogs.length - 1].index || 0;
@ -260,8 +292,13 @@ export default class AppSelectPeers {
this.promise = Promise.all(promises);
this.cachedContacts = (await this.promise)[0].slice(); */
const tempId = ++this.tempIds.contacts;
this.promise = appUsersManager.getContacts(this.query);
this.cachedContacts = (await this.promise).slice();
if(this.tempIds.contacts !== tempId) {
return;
}
this.cachedContacts.findAndSplice(userId => userId === rootScope.myId); // no my account
this.promise = null;
}
@ -282,6 +319,31 @@ export default class AppSelectPeers {
}
}
private async getMoreChannelParticipants() {
if(this.promise) return this.promise;
if(this.loadedWhat.channelParticipants) {
return;
}
const pageCount = 50; // same as in group permissions to use cache
const tempId = ++this.tempIds.channelParticipants;
const promise = appProfileManager.getChannelParticipants(-this.peerId, {_: 'channelParticipantsSearch', q: this.query}, pageCount, this.list.childElementCount);
const participants = await promise;
if(this.tempIds.channelParticipants !== tempId) {
return;
}
const userIds = participants.participants.map(participant => participant.user_id);
userIds.findAndSplice(u => u === rootScope.myId);
this.renderResultsFunc(userIds);
if(this.list.childElementCount >= participants.count || participants.participants.length < pageCount) {
this.loadedWhat.channelParticipants = true;
}
}
checkForTriggers = () => {
this.scrollable.checkForTriggers();
};
@ -290,13 +352,15 @@ export default class AppSelectPeers {
const get = () => {
const promises: Promise<any>[] = [];
if(!loadedAllDialogs && !loadAllDialogsPromise) {
loadAllDialogsPromise = appMessagesManager.getConversationsAll()
.then(() => {
loadedAllDialogs = true;
}, () => {
loadAllDialogsPromise = null;
});
if(!loadedAllDialogs && (this.peerType.includes('dialogs') || this.peerType.includes('contacts'))) {
if(!loadAllDialogsPromise) {
loadAllDialogsPromise = appMessagesManager.getConversationsAll()
.then(() => {
loadedAllDialogs = true;
}).finally(() => {
loadAllDialogsPromise = null;
});
}
promises.push(loadAllDialogsPromise);
}
@ -312,6 +376,10 @@ export default class AppSelectPeers {
if(this.peerType.includes('contacts') && !this.loadedWhat.contacts) {
promises.push(this.getMoreContacts());
}
if(this.peerType.includes('channelParticipants') && !this.loadedWhat.channelParticipants) {
promises.push(this.getMoreChannelParticipants());
}
return promises;
};
@ -340,8 +408,8 @@ export default class AppSelectPeers {
dialog: peerId,
container: this.scrollable,
drawStatus: false,
rippleEnabled: true,
avatarSize: 48
rippleEnabled: this.rippleEnabled,
avatarSize: this.avatarSize
});
if(this.multiSelect) {
@ -449,4 +517,4 @@ export default class AppSelectPeers {
this.selectedScrollable.scrollIntoViewNew(this.input, 'center', undefined, undefined, FocusDirection.Static);
});
}
}
}

View File

@ -8,7 +8,7 @@ export default class PopupPeer extends PopupElement {
description: string,
buttons: Array<PopupButton>
}> = {}) {
super('popup-peer' + (className ? ' ' + className : ''), options.buttons);
super('popup-peer' + (className ? ' ' + className : ''), options.buttons, {overlayClosable: true});
let avatarEl = new AvatarElement();
avatarEl.setAttribute('dialog', '1');

View File

@ -10,7 +10,8 @@ export default class PopupPickUser extends PopupElement {
onSelect?: (peerId: number) => Promise<void> | void,
onClose?: () => void,
placeholder: string,
chatRightsAction?: AppSelectPeers['chatRightsAction']
chatRightsAction?: AppSelectPeers['chatRightsAction'],
peerId?: number,
}) {
super('popup-forward', null, {closable: true, overlayClosable: true, body: true});
@ -42,7 +43,9 @@ export default class PopupPickUser extends PopupElement {
},
chatRightsAction: options.chatRightsAction,
multiSelect: false,
rippleEnabled: false
rippleEnabled: false,
avatarSize: 46,
peerId: options.peerId,
});
//this.scrollable = new Scrollable(this.body);

View File

@ -22,7 +22,7 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
//let animationEndPromise: Promise<number>;
const drawRipple = (clientX: number, clientY: number) => {
const startTime = Date.now();
const span = document.createElement('span');
const elem = document.createElement('div');
const clickId = rippleClickId++;
@ -40,18 +40,18 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
let elapsedTime = Date.now() - startTime;
if(elapsedTime < duration) {
let delay = Math.max(duration - elapsedTime, duration / 2);
setTimeout(() => span.classList.add('hiding'), Math.max(delay - duration / 2, 0));
setTimeout(() => elem.classList.add('hiding'), Math.max(delay - duration / 2, 0));
setTimeout(() => {
//console.log('ripple elapsedTime total pre-remove:', Date.now() - startTime);
span.remove();
elem.remove();
if(onEnd) onEnd(clickId);
}, delay);
} else {
span.classList.add('hiding');
elem.classList.add('hiding');
setTimeout(() => {
//console.log('ripple elapsedTime total pre-remove:', Date.now() - startTime);
span.remove();
elem.remove();
if(onEnd) onEnd(clickId);
}, duration / 2);
}
@ -82,7 +82,7 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
window.requestAnimationFrame(() => {
let rect = r.getBoundingClientRect();
span.classList.add('c-ripple__circle');
elem.classList.add('c-ripple__circle');
let clickX = clientX - rect.left;
let clickY = clientY - rect.top;
@ -106,9 +106,9 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
//console.log('ripple click', offsetFromCenter, size, clickX, clickY);
span.style.width = span.style.height = size + 'px';
span.style.left = x + 'px';
span.style.top = y + 'px';
elem.style.width = elem.style.height = size + 'px';
elem.style.left = x + 'px';
elem.style.top = y + 'px';
// нижний код выполняется с задержкой
/* animationEndPromise = new Promise((resolve) => {
@ -124,7 +124,7 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
duration = +window.getComputedStyle(span).getPropertyValue('animation-duration').replace('s', '') * 1000;
span.style.display = ''; */
r.append(span);
r.append(elem);
//r.classList.add('active');
//handler();
@ -190,4 +190,4 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
window.addEventListener('contextmenu', handler, {once: true});
});
}
}
}

View File

@ -39,7 +39,7 @@ export default class AppBlockedUsersTab extends SliderSuperTab {
});
}, {listenerSetter: this.listenerSetter});
const list = document.createElement('ul');
const list = appDialogsManager.createChatList();
this.scrollable.container.classList.add('chatlist-container');
this.scrollable.append(list);
@ -122,7 +122,7 @@ export default class AppBlockedUsersTab extends SliderSuperTab {
add(peerId, true);
}
if(res.peerIds.length < LOAD_COUNT) {
if(res.peerIds.length < LOAD_COUNT || list.childElementCount === res.count) {
this.scrollable.onScrolledBottom = null;
}

View File

@ -18,7 +18,7 @@ export default class AppContactsTab extends SliderSuperTab {
init() {
this.container.id = 'contacts-container';
this.list = document.createElement('ul');
this.list = appDialogsManager.createChatList(/* {avatarSize: 48, handheldsSize: 66} */);
this.list.id = 'contacts';
this.list.classList.add('contacts-container');

View File

@ -254,7 +254,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
(['include_peers', 'exclude_peers'] as ['include_peers', 'exclude_peers']).forEach(key => {
const container = this[key];
const ul = document.createElement('ul');
const ul = appDialogsManager.createChatList();
const peers = filter[key].slice();

View File

@ -113,7 +113,7 @@ export default class AppEditGroupTab extends SliderSuperTab {
});
const setPermissionsLength = () => {
permissionsRow.subtitle.innerHTML = flags.reduce((acc, f) => acc + +appChatsManager.hasRights(this.chatId, f, 0), 0) + '/' + flags.length;
permissionsRow.subtitle.innerHTML = flags.reduce((acc, f) => acc + +appChatsManager.hasRights(this.chatId, f, chat.default_banned_rights), 0) + '/' + flags.length;
};
setPermissionsLength();
@ -180,7 +180,8 @@ export default class AppEditGroupTab extends SliderSuperTab {
if(appChatsManager.hasRights(this.chatId, 'change_permissions')) {
const showChatHistoryCheckboxField = new CheckboxField({
text: 'Show chat history for new members'
text: 'Show chat history for new members',
withRipple: true
});
if(appChatsManager.isChannel(this.chatId) && !(chatFull as ChatFull.channelFull).pFlags.hidden_prehistory) {

View File

@ -1,10 +1,116 @@
import { attachClickEvent, cancelEvent, findUpTag } from "../../../helpers/dom";
import ListenerSetter from "../../../helpers/listenerSetter";
import { Chat, ChatBannedRights } from "../../../layer";
import ScrollableLoader from "../../../helpers/listLoader";
import { ChannelParticipant, Chat, ChatBannedRights, Update } from "../../../layer";
import appChatsManager, { ChatRights } from "../../../lib/appManagers/appChatsManager";
import appDialogsManager from "../../../lib/appManagers/appDialogsManager";
import appProfileManager from "../../../lib/appManagers/appProfileManager";
import appUsersManager from "../../../lib/appManagers/appUsersManager";
import rootScope from "../../../lib/rootScope";
import CheckboxField from "../../checkboxField";
import PopupPickUser from "../../popups/pickUser";
import Row from "../../row";
import { SettingSection } from "../../sidebarLeft";
import { SliderSuperTabEventable } from "../../sliderTab";
import { toast } from "../../toast";
import AppUserPermissionsTab from "./userPermissions";
export class ChatPermissions {
public v: Array<{
flags: ChatRights[],
text: string,
checkboxField?: CheckboxField
}>;
private toggleWith: Partial<{[chatRight in ChatRights]: ChatRights[]}>;
constructor(options: {
chatId: number,
listenerSetter: ListenerSetter,
appendTo: HTMLElement,
participant?: ChannelParticipant.channelParticipantBanned
}) {
this.v = [
{flags: ['send_messages'], text: 'Send Messages'},
{flags: ['send_media'], text: 'Send Media'},
{flags: ['send_stickers', 'send_gifs'], text: 'Send Stickers & GIFs'},
{flags: ['send_polls'], text: 'Send Polls'},
{flags: ['embed_links'], text: 'Send Links'},
{flags: ['invite_users'], text: 'Add Users'},
{flags: ['pin_messages'], text: 'Pin Messages'},
{flags: ['change_info'], text: 'Change Chat Info'}
];
this.toggleWith = {
'send_messages': ['send_media', 'send_stickers', 'send_polls', 'embed_links']
};
const chat: Chat.chat = appChatsManager.getChat(options.chatId);
const defaultBannedRights = chat.default_banned_rights;
const rights = options.participant ? appChatsManager.combineParticipantBannedRights(options.chatId, options.participant.banned_rights) : defaultBannedRights;
for(const info of this.v) {
const mainFlag = info.flags[0];
info.checkboxField = new CheckboxField({
text: info.text,
checked: appChatsManager.hasRights(options.chatId, mainFlag, rights),
restriction: true,
withRipple: true
});
// @ts-ignore
if(options.participant && defaultBannedRights.pFlags[mainFlag]) {
info.checkboxField.input.disabled = true;
/* options.listenerSetter.add(info.checkboxField.input, 'change', (e) => {
if(!e.isTrusted) {
return;
}
cancelEvent(e);
toast('This option is disabled for all members in Group Permissions.');
info.checkboxField.checked = false;
}); */
attachClickEvent(info.checkboxField.label, (e) => {
toast('This option is disabled for all members in Group Permissions.');
}, {listenerSetter: options.listenerSetter});
}
if(this.toggleWith[mainFlag]) {
options.listenerSetter.add(info.checkboxField.input, 'change', () => {
if(!info.checkboxField.checked) {
const other = this.v.filter(i => this.toggleWith[mainFlag].includes(i.flags[0]));
other.forEach(info => {
info.checkboxField.checked = false;
});
}
});
}
options.appendTo.append(info.checkboxField.label);
}
}
public takeOut() {
const rights: ChatBannedRights = {
_: 'chatBannedRights',
until_date: 0x7FFFFFFF,
pFlags: {}
};
for(const info of this.v) {
const banned = !info.checkboxField.checked;
if(banned) {
info.flags.forEach(flag => {
// @ts-ignore
rights.pFlags[flag] = true;
});
}
}
return rights;
}
}
export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
public chatId: number;
@ -13,93 +119,20 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
this.container.classList.add('edit-peer-container', 'group-permissions-container');
this.title.innerHTML = 'Permissions';
class ChatPermissions {
private v: Array<{
flags: ChatRights[],
text: string,
checkboxField?: CheckboxField
}>;
private toggleWith: Partial<{[chatRight in ChatRights]: ChatRights[]}>;
constructor(options: {
chatId: number,
listenerSetter: ListenerSetter,
appendTo: HTMLElement,
userId: number
}) {
this.v = [
{flags: ['send_messages'], text: 'Send Messages'},
{flags: ['send_media'], text: 'Send Media'},
{flags: ['send_stickers', 'send_gifs'], text: 'Send Stickers & GIFs'},
{flags: ['send_polls'], text: 'Send Polls'},
{flags: ['embed_links'], text: 'Send Links'},
{flags: ['invite_users'], text: 'Add Users'},
{flags: ['pin_messages'], text: 'Pin Messages'},
{flags: ['change_info'], text: 'Change Chat Info'}
];
this.toggleWith = {
'send_messages': ['send_media', 'send_stickers', 'send_polls', 'embed_links']
};
for(const info of this.v) {
const mainFlag = info.flags[0];
info.checkboxField = new CheckboxField({
text: info.text,
checked: appChatsManager.hasRights(options.chatId, mainFlag, options.userId),
restriction: true
});
if(this.toggleWith[mainFlag]) {
options.listenerSetter.add(info.checkboxField.input, 'change', () => {
if(!info.checkboxField.checked) {
const other = this.v.filter(i => this.toggleWith[mainFlag].includes(i.flags[0]));
other.forEach(info => {
info.checkboxField.checked = false;
});
}
});
}
options.appendTo.append(info.checkboxField.label);
}
}
public takeOut() {
const rights: ChatBannedRights = {
_: 'chatBannedRights',
until_date: 0x7FFFFFFF,
pFlags: {}
};
for(const info of this.v) {
const banned = !info.checkboxField.checked;
if(banned) {
info.flags.forEach(flag => {
// @ts-ignore
rights.pFlags[flag] = true;
});
}
}
return rights;
}
}
let chatPermissions: ChatPermissions;
{
const section = new SettingSection({
name: 'What can members of this group do?',
});
const p = new ChatPermissions({
chatPermissions = new ChatPermissions({
chatId: this.chatId,
listenerSetter: this.listenerSetter,
appendTo: section.content,
userId: 0
});
this.eventListener.addEventListener('destroy', () => {
appChatsManager.editChatDefaultBannedRights(this.chatId, p.takeOut());
appChatsManager.editChatDefaultBannedRights(this.chatId, chatPermissions.takeOut());
});
this.scrollable.append(section.container);
@ -110,6 +143,44 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
name: 'Exceptions'
});
const addExceptionRow = new Row({
title: 'Add Exception',
subtitle: 'Loading...',
icon: 'adduser',
clickable: () => {
new PopupPickUser({
peerTypes: ['channelParticipants'],
onSelect: (peerId) => {
setTimeout(() => {
openPermissions(peerId);
}, 0);
},
placeholder: 'Add Exception...',
peerId: -this.chatId,
});
}
});
const openPermissions = async(peerId: number) => {
let participant: AppUserPermissionsTab['participant'];
try {
participant = await appProfileManager.getChannelParticipant(this.chatId, peerId) as any;
if(participant._ !== 'channelParticipantBanned') {
participant = undefined;
}
} catch(err) {
toast('User is no longer participant');
return;
}
const tab = new AppUserPermissionsTab(this.slider);
tab.participant = participant;
tab.chatId = this.chatId;
tab.userId = peerId;
tab.open();
};
const removedUsersRow = new Row({
title: 'Removed Users',
subtitle: 'No removed users',
@ -117,13 +188,126 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
clickable: true
});
section.content.append(removedUsersRow.container);
section.content.append(addExceptionRow.container, removedUsersRow.container);
const c = section.generateContentElement();
c.classList.add('chatlist-container');
const list = appDialogsManager.createChatList();
c.append(list);
attachClickEvent(list, (e) => {
const target = findUpTag(e.target, 'LI');
if(!target) return;
const peerId = +target.dataset.peerId;
openPermissions(peerId);
}, {listenerSetter: this.listenerSetter});
const setSubtitle = (li: Element, participant: ChannelParticipant.channelParticipantBanned) => {
const bannedRights = participant.banned_rights;//appChatsManager.combineParticipantBannedRights(this.chatId, participant.banned_rights);
const defaultBannedRights = (appChatsManager.getChat(this.chatId) as Chat.channel).default_banned_rights;
const combinedRights = appChatsManager.combineParticipantBannedRights(this.chatId, bannedRights);
const cantWhat: string[] = [], canWhat: string[] = [];
chatPermissions.v.forEach(info => {
const mainFlag = info.flags[0];
// @ts-ignore
if(bannedRights.pFlags[mainFlag] && !defaultBannedRights.pFlags[mainFlag]) {
cantWhat.push(info.text);
// @ts-ignore
} else if(!combinedRights.pFlags[mainFlag]) {
canWhat.push(info.text);
}
});
const el = li.querySelector('.user-last-message');
let str: string;
if(cantWhat.length) {
str = 'Can\'t ' + cantWhat.join(cantWhat.length === 2 ? ' and ' : ', ');
} else if(canWhat.length) {
str = 'Can ' + canWhat.join(canWhat.length === 2 ? ' and ' : ', ');
}
//const user = appUsersManager.getUser(participant.user_id);
if(str) {
el.innerHTML = str;
}
el.classList.toggle('hide', !str);
};
const add = (participant: ChannelParticipant.channelParticipantBanned, append: boolean) => {
const {dom} = appDialogsManager.addDialogNew({
dialog: participant.user_id,
container: list,
drawStatus: false,
rippleEnabled: true,
avatarSize: 48,
append
});
setSubtitle(dom.listEl, participant);
//dom.titleSpan.innerHTML = 'Chinaza Akachi';
//dom.lastMessageSpan.innerHTML = 'Can Add Users and Pin Messages';
};
this.listenerSetter.add(rootScope, 'apiUpdate', (update: Update) => {
if(update._ === 'updateChannelParticipant') {
const needAdd = update.new_participant?._ === 'channelParticipantBanned';
const li = list.querySelector(`[data-peer-id="${update.user_id}"]`);
if(needAdd) {
if(!li) {
add(update.new_participant as ChannelParticipant.channelParticipantBanned, false);
} else {
setSubtitle(li, update.new_participant as ChannelParticipant.channelParticipantBanned);
}
if(update.prev_participant?._ !== 'channelParticipantBanned') {
++exceptionsCount;
}
} else {
if(li) {
li.remove();
}
if(update.prev_participant?._ === 'channelParticipantBanned') {
--exceptionsCount;
}
}
setLength();
}
});
const setLength = () => {
addExceptionRow.subtitle.innerHTML = exceptionsCount ? exceptionsCount + ' exceptions' : 'None';
};
let exceptionsCount = 0;
const LOAD_COUNT = 50;
const loader = new ScrollableLoader({
scrollable: this.scrollable,
getPromise: () => {
return appProfileManager.getChannelParticipants(this.chatId, {_: 'channelParticipantsBanned', q: ''}, LOAD_COUNT, list.childElementCount).then(res => {
for(const participant of res.participants) {
add(participant as ChannelParticipant.channelParticipantBanned, true);
}
exceptionsCount = res.count;
setLength();
return res.participants.length < LOAD_COUNT || res.count === list.childElementCount;
});
}
});
this.scrollable.append(section.container);
}
}
onOpenAfterTimeout() {
this.scrollable.onScroll();
}
}

View File

@ -51,7 +51,7 @@ export default class AppPollResultsTab extends SliderSuperTab {
answerEl.append(answerTitle, answerPercents);
// Humans
const list = document.createElement('ul');
const list = appDialogsManager.createChatList();
list.classList.add('poll-results-voters');
appDialogsManager.setListClickListener(list, () => {

View File

@ -0,0 +1,95 @@
import { attachClickEvent } from "../../../helpers/dom";
import { deepEqual } from "../../../helpers/object";
import { ChannelParticipant } from "../../../layer";
import appChatsManager from "../../../lib/appManagers/appChatsManager";
import appDialogsManager from "../../../lib/appManagers/appDialogsManager";
import appProfileManager from "../../../lib/appManagers/appProfileManager";
import appUsersManager from "../../../lib/appManagers/appUsersManager";
import Button from "../../button";
import { SettingSection } from "../../sidebarLeft";
import { SliderSuperTabEventable } from "../../sliderTab";
import { ChatPermissions } from "./groupPermissions";
export default class AppUserPermissionsTab extends SliderSuperTabEventable {
public participant: ChannelParticipant.channelParticipantBanned;
public chatId: number;
public userId: number;
protected init() {
this.container.classList.add('edit-peer-container', 'user-permissions-container');
this.title.innerHTML = 'User Permissions';
{
const section = new SettingSection({
name: 'What can this user do?',
});
const div = document.createElement('div');
div.classList.add('chatlist-container');
section.content.insertBefore(div, section.title);
const list = appDialogsManager.createChatList();
div.append(list);
const {dom} = appDialogsManager.addDialogNew({
dialog: this.userId,
container: list,
drawStatus: false,
rippleEnabled: true,
avatarSize: 48
});
dom.lastMessageSpan.innerHTML = appUsersManager.getUserStatusString(this.userId);
const p = new ChatPermissions({
chatId: this.chatId,
listenerSetter: this.listenerSetter,
appendTo: section.content,
participant: this.participant
});
this.eventListener.addEventListener('destroy', () => {
//appChatsManager.editChatDefaultBannedRights(this.chatId, p.takeOut());
const rights = p.takeOut();
if(deepEqual(this.participant.banned_rights.pFlags, rights.pFlags)) {
return;
}
appChatsManager.editBanned(this.chatId, this.participant, rights);
});
this.scrollable.append(section.container);
}
{
const section = new SettingSection({});
const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'deleteuser', text: 'Ban and Remove From Group'});
attachClickEvent(btnDelete, () => {
/* new PopupPeer('popup-delete-group', {
peerId: -this.chatId,
title: 'Delete Group?',
description: `Are you sure you want to delete this group? All members will be removed, and all messages will be lost.`,
buttons: addCancelButton([{
text: 'DELETE',
callback: () => {
toggleDisability([btnDelete], true);
appChatsManager.deleteChannel(this.chatId).then(() => {
this.close();
}, () => {
toggleDisability([btnDelete], false);
});
},
isDanger: true
}])
}).show(); */
}, {listenerSetter: this.listenerSetter});
section.content.append(btnDelete);
this.scrollable.append(section.container);
}
}
}

28
src/helpers/listLoader.ts Normal file
View File

@ -0,0 +1,28 @@
import Scrollable from "../components/scrollable";
export default class ScrollableLoader {
constructor(options: {
scrollable: Scrollable,
getPromise: () => Promise<any>
}) {
let loading = false;
options.scrollable.onScrolledBottom = () => {
if(loading) {
return;
}
loading = true;
options.getPromise().then(done => {
loading = false;
if(done) {
options.scrollable.onScrolledBottom = null;
} else {
options.scrollable.checkForTriggers();
}
}, () => {
loading = false;
});
};
}
}

View File

@ -121,7 +121,7 @@
</div>
<div class="tabs-container" id="folders-container">
<div>
<ul id="dialogs"></ul>
<ul id="dialogs" class="chatlist chatlist-72"></ul>
</div>
</div>
</div>

View File

@ -1,7 +1,8 @@
import { MOUNT_CLASS_TO } from "../../config/debug";
import { numberThousandSplitter } from "../../helpers/number";
import { isObject, safeReplaceObject, copy, deepEqual } from "../../helpers/object";
import { Chat, ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipants, InputChannel, InputChatPhoto, InputFile, InputPeer, SendMessageAction, Update, Updates } from "../../layer";
import { ChannelParticipant, Chat, ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipant, ChatParticipants, InputChannel, InputChatPhoto, InputFile, InputPeer, SendMessageAction, Update, Updates } from "../../layer";
import apiManagerProxy from "../mtproto/mtprotoworker";
import apiManager from '../mtproto/mtprotoworker';
import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope";
@ -40,6 +41,13 @@ export class AppChatsManager {
break;
}
case 'updateChannelParticipant': {
apiManagerProxy.clearCache('channels.getParticipants', (params) => {
return (params.channel as InputChannel.inputChannel).channel_id === update.channel_id;
});
break;
}
case 'updateChatDefaultBannedRights': {
const chatId = -appPeersManager.getPeerId(update.peer);
const chat: Chat = this.getChat(chatId);
@ -182,7 +190,22 @@ export class AppChatsManager {
return this.chats[id] || {_: 'chatEmpty', id, deleted: true, access_hash: '', pFlags: {}/* this.channelAccess[id] */};
}
public hasRights(id: number, action: ChatRights, userId?: number) {
public combineParticipantBannedRights(id: number, rights: ChatBannedRights) {
const chat: Chat.channel = this.getChat(id);
if(chat.default_banned_rights) {
rights = copy(rights);
const defaultRights = chat.default_banned_rights.pFlags;
for(let i in defaultRights) {
// @ts-ignore
rights.pFlags[i] = defaultRights[i];
}
}
return rights;
}
public hasRights(id: number, action: ChatRights, rights?: ChatAdminRights | ChatBannedRights) {
const chat: Chat = this.getChat(id);
if(chat._ === 'chatEmpty') return false;
@ -193,15 +216,14 @@ export class AppChatsManager {
return false;
}
if(userId !== undefined && appUsersManager.getSelf().id === userId) {
userId = undefined;
}
if(chat.pFlags.creator && userId === undefined) {
if(chat.pFlags.creator && rights === undefined) {
return true;
}
const rights = (userId === undefined && (chat.admin_rights || (chat as Chat.channel).banned_rights)) || chat.default_banned_rights;
if(!rights) {
rights = chat.admin_rights || (chat as Chat.channel).banned_rights || chat.default_banned_rights;
}
if(!rights) {
return false;
}
@ -641,6 +663,47 @@ export class AppChatsManager {
rootScope.broadcast('peer_bio_edit', -id);
});
}
public editBanned(id: number, participant: number | ChannelParticipant, banned_rights: ChatBannedRights) {
const userId = typeof(participant) === 'number' ? participant : participant.user_id;
return apiManager.invokeApi('channels.editBanned', {
channel: this.getChannelInput(id),
user_id: appUsersManager.getUserInput(userId),
banned_rights
}).then((updates) => {
this.onChatUpdated(id, updates);
if(typeof(participant) !== 'number') {
const timestamp = Date.now() / 1000 | 0;
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updateChannelParticipant',
channel_id: id,
date: timestamp,
//qts: 0,
user_id: userId,
prev_participant: participant,
new_participant: Object.keys(banned_rights.pFlags).length ? {
_: 'channelParticipantBanned',
date: timestamp,
banned_rights,
kicked_by: appUsersManager.getSelf().id,
user_id: userId,
pFlags: {}
} : undefined
} as Update.updateChannelParticipant
});
}
});
}
public kickFromChannel(id: number, userId: number) {
return this.editBanned(id, userId, {
_: 'chatBannedRights',
until_date: 0
});
}
}
const appChatsManager = new AppChatsManager();

View File

@ -39,7 +39,7 @@ type DialogDom = {
lastTimeSpan: HTMLSpanElement,
unreadMessagesSpan: HTMLSpanElement,
lastMessageSpan: HTMLSpanElement,
containerEl: HTMLDivElement,
containerEl: HTMLElement,
listEl: HTMLLIElement,
muteAnimationTimeout?: number
};
@ -222,7 +222,7 @@ export class AppDialogsManager {
private lastActiveElements: Set<HTMLElement> = new Set();
constructor() {
this.chatListArchived = document.createElement('ul');
this.chatListArchived = this.createChatList();
this.chatListArchived.id = 'dialogs-archived';
this.chatLists = {
@ -686,7 +686,7 @@ export class AppDialogsManager {
positionElementByIndex(menuTab, containerToAppend, filter.orderIndex);
//containerToAppend.append(li);
const ul = document.createElement('ul');
const ul = this.createChatList();
const div = document.createElement('div');
div.append(ul);
div.dataset.filterId = '' + filter.id;
@ -914,17 +914,15 @@ export class AppDialogsManager {
//cancelEvent(e);
this.log('dialogs click list');
let target = e.target as HTMLElement;
let elem = target.classList.contains('rp') ? target : findUpClassName(target, 'rp');
const target = e.target as HTMLElement;
const elem = findUpTag(target, 'LI');
if(!elem) {
return;
}
elem = elem.parentElement;
if(autonomous) {
let sameElement = lastActiveListElement === elem;
const sameElement = lastActiveListElement === elem;
if(lastActiveListElement && !sameElement) {
lastActiveListElement.classList.remove('active');
}
@ -939,8 +937,8 @@ export class AppDialogsManager {
if(elem) {
if(onFound) onFound();
let peerId = +elem.dataset.peerId;
let lastMsgId = +elem.dataset.mid || undefined;
const peerId = +elem.dataset.peerId;
const lastMsgId = +elem.dataset.mid || undefined;
appImManager.setPeer(peerId, lastMsgId);
} else {
@ -963,6 +961,22 @@ export class AppDialogsManager {
}
}
public createChatList(/* options: {
avatarSize?: number,
handheldsSize?: number,
//size?: number,
} = {} */) {
const list = document.createElement('ul');
list.classList.add('chatlist'/* ,
'chatlist-avatar-' + (options.avatarSize || 54) *//* , 'chatlist-' + (options.size || 72) */);
/* if(options.handheldsSize) {
list.classList.add('chatlist-handhelds-' + options.handheldsSize);
} */
return list;
}
private reorderDialogs() {
//const perf = performance.now();
if(this.reorderDialogsTimeout) {
@ -1291,16 +1305,12 @@ export class AppDialogsManager {
//captionDiv.append(titleSpan);
//captionDiv.append(span);
const paddingDiv = document.createElement('div');
paddingDiv.classList.add('rp');
paddingDiv.append(avatarEl, captionDiv);
const li = document.createElement('li');
if(rippleEnabled) {
ripple(paddingDiv);
ripple(li);
}
const li = document.createElement('li');
li.append(paddingDiv);
li.append(avatarEl, captionDiv);
li.dataset.peerId = '' + peerId;
const statusSpan = document.createElement('span');
@ -1335,7 +1345,7 @@ export class AppDialogsManager {
lastTimeSpan,
unreadMessagesSpan,
lastMessageSpan: span,
containerEl: paddingDiv,
containerEl: li,
listEl: li
};

View File

@ -53,6 +53,21 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
private hashes: {[method: string]: HashOptions} = {};
private apiPromisesSingle: {
[q: string]: Promise<any>
} = {};
private apiPromisesCacheable: {
[method: string]: {
[queryJSON: string]: {
timestamp: number,
promise: Promise<any>,
fulfilled: boolean,
timeout?: number,
params: any
}
}
} = {};
private isSWRegistered = true;
private debug = DEBUG /* && false */;
@ -354,6 +369,71 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
});
}
public invokeApiSingle<T extends keyof MethodDeclMap>(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions = {}): Promise<MethodDeclMap[T]['res']> {
const q = method + '-' + JSON.stringify(params);
if(this.apiPromisesSingle[q]) {
return this.apiPromisesSingle[q];
}
return this.apiPromisesSingle[q] = this.invokeApi(method, params, options).finally(() => {
delete this.apiPromisesSingle[q];
});
}
public invokeApiCacheable<T extends keyof MethodDeclMap>(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions & Partial<{cacheSeconds: number, override: boolean}> = {}): Promise<MethodDeclMap[T]['res']> {
const cache = this.apiPromisesCacheable[method] ?? (this.apiPromisesCacheable[method] = {});
const queryJSON = JSON.stringify(params);
const item = cache[queryJSON];
if(item && (!options.override || !item.fulfilled)) {
return item.promise;
}
if(options.override) {
if(item && item.timeout) {
clearTimeout(item.timeout);
delete item.timeout;
}
delete options.override;
}
let timeout: number;
if(options.cacheSeconds) {
timeout = window.setTimeout(() => {
delete cache[queryJSON];
}, options.cacheSeconds * 1000);
delete options.cacheSeconds;
}
const promise = this.invokeApi(method, params, options);
cache[queryJSON] = {
timestamp: Date.now(),
fulfilled: false,
timeout,
promise,
params
};
return promise;
}
public clearCache<T extends keyof MethodDeclMap>(method: T, verify: (params: MethodDeclMap[T]['req']) => boolean) {
const cache = this.apiPromisesCacheable[method];
if(cache) {
for(const queryJSON in cache) {
const item = cache[queryJSON];
if(verify(item.params)) {
if(item.timeout) {
clearTimeout(item.timeout);
}
delete cache[queryJSON];
}
}
}
}
/* private computeHash(smth: any[]) {
smth = smth.slice().sort((a, b) => a.id - b.id);
//return smth.reduce((hash, v) => (((hash * 0x4F25) & 0x7FFFFFFF) + v.id) & 0x7FFFFFFF, 0);

View File

@ -7,25 +7,124 @@
}
}
ul {
margin: 0;
display: flex;
flex-direction: column;
position: relative;
.search-group {
width: 100%;
//border-bottom: 1px solid #DADCE0;
padding: 1rem 0 .5rem;
margin-bottom: 17px;
user-select: none;
-webkit-user-select: none; /* disable selection/Copy of UIWebView */
-webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */
@include respond-to(handhelds) {
margin-bottom: 0;
}
&__name {
color: $color-gray;
padding: 0 23px;
padding-bottom: 1rem;
font-weight: 500;
user-select: none;
@include respond-to(handhelds) {
padding: 5px 9px 0 16px;
font-size: 15px;
}
}
&-contacts {
border-bottom: 1px solid #dadce0;
@include respond-to(handhelds) {
padding: 0px 0 2px;
}
// .search-group__name {
// padding-bottom: 17px;
// @include respond-to(handhelds) {
// padding-bottom: 0;
// }
// }
}
&-people.search-group-contacts {
padding: 5px 0 5px !important;
}
&:last-child {
border-bottom: none;
}
}
.search-super {
.search-group {
margin-bottom: 0px;
padding: 4px 0 0;
&__name {
padding-top: 1rem;
display: flex;
justify-content: space-between;
}
}
}
}
ul.chatlist {
padding: 0 .5rem;
@include respond-to(handhelds) {
padding: 0;
}
}
.chatlist {
//--avatarSize: 54px;
//--height: 72px;
margin: 0;
display: flex;
flex-direction: column;
position: relative;
width: 100%;
user-select: none;
-webkit-user-select: none; /* disable selection/Copy of UIWebView */
-webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */
/* &.chatlist-avatar-48 {
--avatarSize: 48px;
}
@include respond-to(handhelds) {
&.chatlist-handhelds-66 {
--height: 66px;
}
} */
li {
background-color: #fff;
//height: var(--height);
height: 72px;
//max-height: var(--height);
border-radius: $border-radius-medium;
display: flex;
align-items: flex-start; // TODO: проверить разницу в производительности с align-items: center;
flex-direction: row;
position: relative;
cursor: pointer;
padding: 9px 8.5px;
/* padding-top: calc((var(--height) - var(--avatarSize)) / 2);
padding-bottom: calc((var(--height) - var(--avatarSize)) / 2);
padding-right: 8.5px;
padding-left: 8.5px; */
overflow: hidden;
@include respond-to(handhelds) {
padding-bottom: 0px;
border-radius: 0;
}
@include hover-background-effect();
&.is-muted {
.user-title {
&:after {
@ -87,44 +186,15 @@
margin-left: 0;
}
} */
}
li > .rp {
height: 72px;
max-height: 72px;
border-radius: $border-radius-medium;
display: flex;
align-items: flex-start; // TODO: проверить разницу в производительности с align-items: center;
flex-direction: row;
position: relative;
cursor: pointer;
padding: 9px 8.5px;
margin: 0 8px;
overflow: hidden;
/* html.is-safari & {
margin-right: 3px;
} */
@include respond-to(handhelds) {
padding: 9px 12px 9px 9px !important;
border-radius: 0;
margin: 0;
overflow: hidden;
}
@include hover-background-effect();
}
li.menu-open {
> .rp {
&.menu-open {
background: var(--color-gray-hover);
}
}
@include respond-to(not-handhelds) {
li.active > .rp {
background: var(--color-gray-hover);
@include respond-to(not-handhelds) {
&.active {
background: var(--color-gray-hover);
}
}
}
@ -261,96 +331,25 @@
li.is-muted .unread {
background: #c5c9cc;
}
.search-group {
width: 100%;
//border-bottom: 1px solid #DADCE0;
padding: 1rem 0 .5rem;
margin-bottom: 17px;
@include respond-to(handhelds) {
margin-bottom: 0;
}
&__name {
color: $color-gray;
padding: 0 23px;
padding-bottom: 1rem;
font-weight: 500;
user-select: none;
@include respond-to(handhelds) {
padding: 5px 9px 0 16px;
font-size: 15px;
}
}
&-contacts {
border-bottom: 1px solid #dadce0;
@include respond-to(handhelds) {
padding: 0px 0 2px;
}
// .search-group__name {
// padding-bottom: 17px;
// @include respond-to(handhelds) {
// padding-bottom: 0;
// }
// }
}
&-people.search-group-contacts {
padding: 5px 0 5px !important;
}
&:last-child {
border-bottom: none;
}
}
.search-super {
.search-group {
margin-bottom: 0px;
padding: 4px 0 0;
&__name {
padding-top: 1rem;
display: flex;
justify-content: space-between;
}
}
}
}
// use together like class="chatlist-container contacts-container"
.contacts-container, .search-group-contacts {
li {
//margin-bottom: 2px;
padding-bottom: 4px;
padding-top: 2px;
padding: .75rem;
@include respond-to(handhelds) {
padding: 0;
height: 66px;
padding-top: 9px;
padding-bottom: 9px;
}
}
li > .rp {
padding: 9px 11.5px !important;
height: 66px;
//@include respond-to(handhelds) {
//height: 62px;
//}
}
.user-caption {
padding: 1px 3.5px 1px 13px;
@include respond-to(handhelds) {
padding: 0px 4px 0px 14px;
padding: 0 4px 0 14px;
}
}

View File

@ -95,6 +95,10 @@
.checkbox-ripple {
overflow: hidden;
border-radius: $border-radius-medium;
.checkbox-box, .checkbox-caption {
pointer-events: none;
}
}
.checkbox-field-round {
@ -168,7 +172,6 @@
&::before {
border: 2px solid #707579;
border-radius: 50%;
background-color: white;
opacity: 1;
transition: border-color .1s ease, opacity .1s ease;
}

View File

@ -263,7 +263,7 @@
}
.search-group-people {
ul {
.chatlist {
display: flex;
flex-direction: row;
padding-left: 4px;
@ -272,13 +272,7 @@
}
li {
margin-right: 5px;
padding: 0;
}
.rp {
height: 98px;
max-height: 98px;
border-radius: 10px;
max-width: 78px;
width: 78px;
@ -286,7 +280,8 @@
display: flex;
flex-direction: column;
padding: 12px 0 0 !important;
margin: 0;
margin: 0 5px 0 0;
flex: 0 0 auto;
@include respond-to(handhelds) {
width: 77px;
@ -294,7 +289,7 @@
}
}
.dialog-title-details {
.dialog-title-details, .dialog-subtitle {
display: none;
}
@ -628,16 +623,8 @@
.folder-list {
li {
padding-bottom: 2px;
.rp {
padding: 8px 11px !important;
height: 48px !important;
@include respond-to(handhelds) {
padding: 8px 12px !important;
}
}
padding: 9px 11px;
height: 50px;
}
.user-caption {
@ -680,16 +667,10 @@
.popup-forward, .included-chatlist-container {
.selector {
ul {
li > .rp {
margin: 0 .5rem;
.chatlist {
li {
padding: 7px .75rem !important;
height: 3.75rem;
max-height: 3.75rem;
@include respond-to(handhelds) {
margin: 0;
}
}
.user-caption {
@ -705,13 +686,6 @@
}
}
.popup-forward {
li > .rp {
height: 3.875rem !important;
max-height: 3.875rem !important;
}
}
.included-chatlist-container {
.sidebar-left-h2 {
padding: 6px 24px 8px 24px;
@ -776,7 +750,9 @@
@include respond-to(handhelds) {
li {
padding-top: 0;
height: 62px;
padding-top: 7px;
padding-bottom: 7px;
}
.user-caption {
@ -791,16 +767,12 @@
--size: 46px;
--multiplier: 1.173913;
}
li > .rp {
height: 62px;
}
}
}
@include respond-to(handhelds) {
.search-group-recent.search-group.search-group-contacts ul {
margin-top: -2px;
margin-top: 0;
}
.search-group.search-group-contacts ul, .search-group.search-group-messages ul {
@ -1045,9 +1017,12 @@
}
.blocked-users-container {
li > .rp {
li {
height: 66px;
max-height: 66px;
padding-top: 9px;
padding-bottom: 9px;
border-radius: $border-radius-medium;
}
.user-caption {
@ -1061,7 +1036,7 @@
ul {
margin-top: .3125rem;
padding: 0 3px;
padding: 0 .6875rem;
}
}

View File

@ -696,7 +696,6 @@
color: #707579;
padding: 0 16px 8px 16px;
margin: 0;
padding-bottom: 8px;
font-weight: 500;
justify-content: space-between;
display: flex;
@ -753,15 +752,11 @@
}
li {
padding-bottom: 2px;
height: 50px;
padding: 9px;
> .rp {
padding: 8px 5px;
height: 48px;
@include respond-to(not-handhelds) {
padding: 8px 12px;
}
@include respond-to(not-handhelds) {
padding: 9px 12px;
}
}
}
@ -789,8 +784,35 @@
}
}
.checkbox-field {
margin: 0 1.1875rem;
// * supernew and correct layout
.chatlist {
padding: 0;
li {
height: 72px;
padding: 0 .75rem;
align-items: center;
}
.user-caption {
padding-left: .75rem;
}
p {
height: auto;
}
span {
line-height: 1.3125;
}
.dialog-subtitle {
margin-top: .125rem;
}
.user-last-message {
font-size: .875rem;
}
}
}
@ -817,7 +839,6 @@
.group-type-container {
.sidebar-left-section-caption {
font-size: .875rem;
line-height: 1rem;
margin-top: .8125rem;
}

View File

@ -125,9 +125,21 @@
}
}
ul {
.chatlist {
li {
padding-top: .75rem;
padding-bottom: .75rem;
@include respond-to(handhelds) {
height: 66px;
padding-top: 9px;
padding-bottom: 9px;
}
}
.user-caption {
padding: 1px 3.5px 1px 12px;
padding-left: .75rem;
padding-right: 0;
}
p {
@ -137,25 +149,6 @@
span.user-last-message {
font-size: 14px;
}
li {
padding-bottom: 0;
> .rp {
margin: 0px 9px 0px 8px;
padding: 12px 8.5px;
@include respond-to(handhelds) {
height: 66px;
max-height: 66px;
margin: 0;
}
/* html.is-safari & {
margin-right: 4px;
} */
}
}
}
hr {
@ -194,4 +187,4 @@
--offset: 6px;
}
}
}
}

View File

@ -6,14 +6,14 @@
width: 420px;
max-width: 420px;
//padding: 12px 20px 32.5px;
padding: 9px 0 0 0;
padding: 7px 0 0 0;
max-height: unquote('min(40.625rem, 100%)');
height: 40.625rem;
}
&-header {
flex: 0 0 auto;
margin-bottom: 4px;
margin-bottom: 3px;
padding: 0 1rem;
}
@ -35,10 +35,17 @@
font-size: 1.25rem;
padding: .5rem 1.5rem;
width: 100%;
line-height: 1.3125;
}
/* ul li > .rp {
margin-left: 0;
} */
.chatlist {
margin-top: 0 !important;
li {
height: 3.875rem !important;
padding-top: .5rem !important;
padding-bottom: .5rem !important;
}
}
}
}
}