Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,409 @@
/* globals CallHandler, Contacts, fb, KeypadManager, LazyLoader,
SimplePhoneMatcher, SimSettingsHelper, Utils */
// Suggestion_bar.js will be loaded on init of KeypadManager through
// lazy loader. So we call its init() directly at the end of file.
'use strict';
var SuggestionBar = {
MIN_DIGIT_TO_SHOW: 3,
SKIP_FOR_COUNTRYCODE: 2,
MAX_ITEMS: 50,
_phoneNumber: null,
_contactList: null,
_loaded: false,
_pendingFbRequest: null,
_hasMatchingFbContacts: false,
_hasMatchingLocalContacts: false,
// Visual Elements
bar: null,
barSuggestionItem: null,
countTag: null,
list: null,
overlay: null,
overlayCancel: null,
template: null,
init: function sb_init() {
// When the DOM is absent (in the call screen) we don't need
// to initialize the module.
this.overlay = document.getElementById('contact-list-overlay');
if (!this.overlay) {
return;
}
this.bar = document.getElementById('suggestion-bar');
this.barSuggestionItem = this.bar.querySelector('.js-suggestion-item');
this.countTag = document.getElementById('suggestion-count');
this.template = document.getElementById('contact-in-overlay-template');
this.overlay.addEventListener('click', this);
this.bar.addEventListener('click', this);
this.countTag.addEventListener('click', this.showOverlay.bind(this));
KeypadManager.onValueChanged = this.update.bind(this);
},
handleEvent: function sb_handleEvent(event) {
var node = event.target;
if (!node.classList.contains('js-suggestion-item')) {
return;
}
event.stopPropagation();
var telTag = node.querySelector('.js-tel');
KeypadManager.updatePhoneNumber(telTag.textContent, 'begin', false);
// In the multi-SIM case, we just autocomplete the phone number without
// making the call. The call button is responsible for SIM selection
// behavior.
if (!navigator.mozIccManager ||
navigator.mozIccManager.iccIds.length < 2) {
LazyLoader.load('/shared/js/sim_settings_helper.js', function() {
SimSettingsHelper.getCardIndexFrom('outgoingCall', function(ci) {
CallHandler.call(KeypadManager.phoneNumber(), ci);
});
});
} else {
this.hideOverlay();
}
},
update: function sb_update(number) {
this._phoneNumber = number;
// when first letter of number is "+", we'll change minimum search criteria
// from MIN_DIGIT_TO_SHOW to MIN_DIGIT_TO_SHOW + SKIP_FOR_COUNTRYCODE.
var min = this.MIN_DIGIT_TO_SHOW +
((number.charAt(0) === '+') ? this.SKIP_FOR_COUNTRYCODE : 0);
if (number.length < min) {
this.clear(true);
return;
}
if (this._loaded) {
this._updateByContacts();
return;
}
var self = this;
LazyLoader.load(['/shared/js/async_storage.js',
'/shared/js/dialer/contacts.js',
'/shared/js/simple_phone_matcher.js',
this.barSuggestionItem,
this.template],
function callback() {
self._loaded = true;
self._updateByContacts();
});
},
_renderBar: function sb_renderBar() {
var self = this;
self.bar.classList.remove('hide');
// Create matching index table for reference
self._allMatched = self._getAllMatched(self._contactList);
var totalMatchNum = self._allMatched.totalMatchNum;
var contact = self._contactList[0];
var firstMatch = self._allMatched.allMatches[0][0];
var shouldHideSuggestionBar = false;
// In a multi-SIM setup, tapping on a suggestion in the settings bar doesn't
// place a call, it just fills in the phone number. In this case, we should
// hide the suggestions bar to not confuse the user into thinking that
// tapping it again will place the call.
if (totalMatchNum === 1 &&
contact.tel[firstMatch].value == self._phoneNumber &&
navigator.mozIccManager &&
navigator.mozIccManager.iccIds.length > 1) {
shouldHideSuggestionBar = true;
}
// Don't show any suggestions if we have too many. The user should narrow it
// down further by entering more digits.
if (totalMatchNum > self.MAX_ITEMS) {
shouldHideSuggestionBar = true;
}
if (shouldHideSuggestionBar) {
self.clear();
return 0;
}
self.countTag.textContent = totalMatchNum;
var hasMoreThanOneMatch = (totalMatchNum > 1);
self.countTag.hidden = !hasMoreThanOneMatch;
self.countTag.classList.toggle('more', hasMoreThanOneMatch);
var node = self.barSuggestionItem;
self._fillContacts(contact, firstMatch, node);
self.bar.dataset.lastId = contact.id || contact.uid;
return totalMatchNum;
},
_checkIfCleared: function sb_checkIfCleared() {
var out = false;
if (!this._phoneNumber) {
this.bar.dataset.lastId = '';
this.clear(true);
out = true;
}
return out;
},
_searchCallback: function sb_searchCallback(contacts) {
var self = this;
self._pendingFbRequest = null;
if (self._checkIfCleared()) {
return;
}
var totalMatchNum = 0;
if (Array.isArray(contacts) && contacts.length > 0) {
self._contactList = contacts;
totalMatchNum = self._renderBar();
self._hasMatchingLocalContacts = true;
}
else {
// Avoid to clear the list if we have previous FB Contacts
// This will avoid continous reflows while refreshing
if (!self._hasMatchingFbContacts ||
(self._hasMatchingFbContacts && self._hasMatchingLocalContacts)) {
self.clear(false);
}
self._hasMatchingLocalContacts = false;
self._contactList = null;
}
if (totalMatchNum > self.MAX_ITEMS) {
self._hasMatchingFbContacts = false;
return;
}
var req = fb.contacts.search('phone', self._phoneNumber);
self._pendingFbRequest = req;
req.onsuccess = function() {
// Avoid to overlap FB requests
if (self._pendingFbRequest === req) {
self._pendingFbRequest = null;
self._searchCallbackFb(req.result);
}
};
req.onerror = function() {
window.console.error('Error while searching FB Data: ', req.error.name);
self._hasMatchingFbContacts = false;
};
},
_searchCallbackFb: function sb_searchCallbackFb(contacts) {
if (this._checkIfCleared()) {
return;
}
if (!Array.isArray(contacts) || contacts.length === 0) {
this._hasMatchingFbContacts = false;
if (!this._hasMatchingLocalContacts) {
this.clear(true);
}
return;
}
this._hasMatchingFbContacts = true;
this._contactList = (this._contactList || []).concat(contacts);
this._renderBar();
},
_updateByContacts: function sb_updateByContacts() {
// A search is both launched on mozContacts and on Facebook DS
Contacts.findListByNumber(this._phoneNumber, this.MAX_ITEMS + 1,
this._searchCallback.bind(this));
},
_fillContacts: function sb_fillContacts(contact, matchLocal, node) {
// if first letter of query is '+' and first letter of matchedTel isn't '+'
// we use query without country code instead of original query for
// markedNumber.
var matchedTel = contact.tel[matchLocal];
var markedNumber = this._markMatched(matchedTel.value, this._phoneNumber);
this._setItem(node, markedNumber, matchedTel.type,
contact.name[0]);
},
_createItem: function sb_createItem() {
var itemElm = this.template.cloneNode(true);
itemElm.removeAttribute('id');
itemElm.hidden = false;
itemElm.classList.add('ci--action-menu');
this.list.insertBefore(itemElm, this.overlayCancel);
return itemElm;
},
_setItem: function sb_setItem(node, tel, type, name) {
var typeTag = node.querySelector('.js-tel-type');
var telTag = node.querySelector('.js-tel');
var nameTag = node.querySelector('.js-name');
// Set the contact name
nameTag.textContent = name ? name : null;
// Set the number with the highlighted match
while (telTag.firstChild) {
telTag.removeChild(telTag.firstChild);
}
if (tel) {
telTag.appendChild(tel);
}
// Set the phone type
if (type) {
if (Utils.isPhoneType(type)) {
navigator.mozL10n.setAttributes(typeTag, type);
} else {
// No localization found, use the type string as-is
typeTag.removeAttribute('data-l10n-id');
typeTag.textContent = type;
}
} else {
typeTag.textContent = null;
}
},
clear: function sb_clear(isHardClear) {
if (!this._loaded) {
return;
}
this.countTag.textContent = '';
this.countTag.classList.remove('more');
// Clear contents
var node = this.barSuggestionItem;
this._setItem(node);
this._contactList = null;
this.bar.classList.add('hide');
if (isHardClear) {
this._phoneNumber = null;
delete this.bar.dataset.lastId;
}
},
_markMatched: function sb_markMatched(str, substr) {
var fragment = document.createDocumentFragment();
var sanitized = SimplePhoneMatcher.sanitizedNumber(str);
var digitBeforeMatch = sanitized.indexOf(substr);
if (digitBeforeMatch == -1) {
fragment.appendChild(document.createTextNode(str));
return fragment;
}
// Highlight matched number. We need to count formatting character in.
// we already have sanitized match position above. Then we search from
// head of str, count only valid digits to find corresponding start and
// end position in origin str.
var start = NaN;
var end = NaN;
var validDigits = 0;
for (var i = 0; i < str.length; i++) {
if (/^[\d\+*#]$/.test(str[i])) {
validDigits++;
}
if ((validDigits > digitBeforeMatch) && isNaN(start)) {
start = i;
} else if (validDigits >= (digitBeforeMatch + substr.length) &&
isNaN(end)) {
end = i;
}
}
var startStr = str.substr(0, start);
var middleStr = str.substr(start, end - start + 1);
var endStr = str.substr(end + 1);
var startNode = document.createTextNode(startStr);
var middleNode = document.createElement('mark');
middleNode.textContent = middleStr;
middleNode.classList.add('ci__mark');
var endNode = document.createTextNode(endStr);
fragment.appendChild(startNode);
fragment.appendChild(middleNode);
fragment.appendChild(endNode);
return fragment;
},
_initOverlay: function() {
if (this.list) {
return;
}
this.list = document.getElementById('contact-list');
this.overlayCancel = document.getElementById('contact-list-overlay-cancel');
this.overlayCancel.addEventListener('click', this.hideOverlay.bind(this));
},
_clearOverlay: function() {
while (this.list.firstElementChild != this.overlayCancel) {
this.list.firstElementChild.remove();
}
},
showOverlay: function sb_showOverlay() {
var self = this;
LazyLoader.load(this.overlay, function() {
self._initOverlay();
self._clearOverlay();
var title = self.overlay.querySelector('header');
navigator.mozL10n.setAttributes(
title,
'suggestionMatches',
{
n: +self.countTag.textContent,
matchNumber: self._phoneNumber
}
);
for (var i = 0; i < self._contactList.length; i++) {
for (var j = 0; j < self._allMatched.allMatches[i].length; j++) {
var node = self._createItem();
self._fillContacts(self._contactList[i],
self._allMatched.allMatches[i][j], node);
}
}
self.overlay.setAttribute('aria-hidden', false);
self.overlay.classList.add('display');
});
},
_getAllMatched: function sb_getAllMatched(contacts) {
var variants = SimplePhoneMatcher.generateVariants(this._phoneNumber);
var contactTels = contacts.map(function getTels(contact) {
return contact.tel.map(function getNumber(tel) {
return tel.value;
});
});
return SimplePhoneMatcher.bestMatch(variants, contactTels);
},
hideOverlay: function sb_hideOverlay() {
this.overlay.setAttribute('aria-hidden', true);
this.overlay.classList.remove('display');
}
};
SuggestionBar.init();